NCTF 2024 Reverse WP

前言

这次也是擦边#6拿下奖金,组队的师傅们都十分强大,合力拿下。

Re这次摸了三题,剩一题安卓不太熟悉安卓逆向看了一眼就没做,但是赛后发现其实没那么难(。ezDOS 8086汇编题刚好撞上我GHCTF出的题,很熟悉直接秒了,偷摸了个一血。然后gogo这题的golang vm研究了挺久,拿下了三血,实际加密不是很复杂。总体下来这几题re出的都挺不错,收获也很多,对vm的题型也更加熟悉了。

ezDOS

分析

程序中间出现的花指令都直接nop去除,然后Apply patch到程序。

alt text

程序要求输入38长度字符串,然后进行一系列变种类RC4算法加密,然后与0x141地址的38字节数据进行比对。

alt text

使用dosbox动调,在FA代码处是对取出的al对输入的字符串进行xor加密。0x32是取出的异或值,0x31是输入的字符’1’。

alt text

alt text

直接debug一直循环执行这边,即可拿到对输入字符串异或的一系列异或值。

32 7d 59 7a f3 0d b3 7b 64 8c eb 28 c4 a4 50 30 a0 ed 27 6a e3 76 69 0c da 28 f8 08 ba a6 17 3e 12 59 45 06 4e f1

取出0x142地址的38字节,进行异或即可得到解密flag。

解密

alt text

flag

NCTF{Y0u+Ar3_Assemb1y_M4st3r_5d0b497e}

SafeProgram

分析

核心加密是一个SM4加密。

alt text

查看byte_14002A0D0数组交叉引用,发现在其他函数被访问过。

alt text

alt text

发现是在VEH异常Handler里面调用的,第一个AddVectoredExceptionHandler得直接nop,不然运行就直接退出。

alt text

alt text

alt text

然后在这个函数开头断点,使用ScyllaHide插件一键去除反调试,防止其他地方的反调试。

alt text

main函数可以看到要求输入格式为NCTF{…}的长度38字符串,然后运行到箭头函数会触发除0异常,然后就会触发VEH那个Handler导致那个byte数组被修改,需要提取的数据是被改后的。

alt text

动调时提取这三个数组数据,进行解密即可。

alt text

key是main函数两次memcpy那边的数据,结果如下。

alt text

解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include <iostream>
#include <string>
#include <windows.h>
#include <vector>

unsigned char byte_7FF699A4A0D0[256] = {
0xD1, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62,
0xE4, 0xB3, 0x17, 0xA9, 0x1C, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6,
0x47, 0x07, 0xA7, 0x4F, 0xF3, 0x73, 0x71, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0xD6, 0xA8,
0x68, 0x6B, 0x81, 0xB2, 0xFC, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35,
0x1E, 0x24, 0x0E, 0x78, 0x63, 0x58, 0x9F, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0xC9, 0x87,
0xD4, 0x00, 0x46, 0x57, 0x5E, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E,
0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1,
0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F,
0xD5, 0xDB, 0x37, 0x45, 0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51,
0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F, 0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8,
0x0A, 0xC1, 0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12, 0xB8, 0xE5, 0xB4, 0xB0,
0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE, 0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48
};

unsigned int dword_7FF699A4A040[32] = {
0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269, 0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249, 0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229, 0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209, 0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279
};

unsigned int dword_7FF699A4A028[4] = {
0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC
};

static inline uint32_t rotate_left(uint32_t x, int n)
{
return (x << n) | (x >> (32 - n));
}

static void generate_buf(const uint8_t* key, uint32_t* buf)
{
for (int i = 0; i < 4; i++)
{
uint32_t key_dword;
memcpy(&key_dword, key + 4 * i, 4);
uint32_t converted_key = ((key_dword & 0xFF) << 24) |
((key_dword & 0xFF00) << 8) |
((key_dword >> 8) & 0xFF00) |
((key_dword >> 24) & 0xFF);
buf[i] = dword_7FF699A4A028[i] ^ converted_key;
}
for (int j = 0; j < 32; j++)
{
uint32_t v12 = dword_7FF699A4A040[j] ^ buf[j + 3] ^ buf[j + 2] ^ buf[j + 1];
uint8_t* v12_bytes = (uint8_t*)&v12;
for (int k = 0; k < 4; k++)
{
v12_bytes[k] = byte_7FF699A4A0D0[v12_bytes[k]];
}
uint32_t rot1 = rotate_left(v12, 23);
uint32_t rot2 = rotate_left(v12, 13);
buf[j + 4] = (rot1 ^ rot2 ^ v12) ^ buf[j];
}
}

void decrypt(const uint8_t* ciphertext, const uint8_t* key, uint8_t* plaintext)
{
uint32_t buf[36] = { 0 };
uint32_t buf_1[36] = { 0 };

generate_buf(key, buf);

for (int ii = 0; ii < 4; ii++)
{
uint32_t cipher_dword;
memcpy(&cipher_dword, ciphertext + 4 * ii, 4);
buf_1[35 - ii] = ((cipher_dword >> 24) & 0xFF) |
((cipher_dword >> 8) & 0xFF00) |
((cipher_dword << 8) & 0xFF0000) |
((cipher_dword << 24) & 0xFF000000);
}

for (int m = 31; m >= 0; m--)
{
uint32_t v12_0 = buf[m + 4] ^ buf_1[m + 3] ^ buf_1[m + 2] ^ buf_1[m + 1];
uint8_t* v12_bytes = (uint8_t*)&v12_0;
for (int n = 0; n < 4; n++)
{
v12_bytes[n] = byte_7FF699A4A0D0[v12_bytes[n]];
}
uint32_t rot1 = rotate_left(v12_0, 24);
uint32_t rot2 = rotate_left(v12_0, 18);
uint32_t rot3 = rotate_left(v12_0, 10);
uint32_t rot4 = rotate_left(v12_0, 2);
uint32_t L_result = rot1 ^ rot2 ^ rot3 ^ rot4 ^ v12_0;
buf_1[m] = buf_1[m + 4] ^ L_result;
}

for (int i = 0; i < 4; i++)
{
uint32_t v11 = buf_1[i];
uint8_t b0 = (v11 >> 24) & 0xFF;
uint8_t b1 = (v11 >> 16) & 0xFF;
uint8_t b2 = (v11 >> 8) & 0xFF;
uint8_t b3 = v11 & 0xFF;
uint32_t le = (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
memcpy(plaintext + 4 * i, &le, 4);
}
}

int main_safe()
{
uint8_t key[] = "NCTF24nctfNCTF24";
unsigned char enc[32] = {
0xFB, 0x97, 0x3C, 0x3B, 0xF1, 0x99, 0x12, 0xDF, 0x13, 0x30, 0xF7, 0xD8, 0x7F, 0xEB, 0xA0, 0x6C,
0x14, 0x5B, 0xA6, 0x2A, 0xA8, 0x05, 0xA5, 0xF3, 0x76, 0xBE, 0xC9, 0x01, 0xF9, 0x36, 0x7B, 0x46
};

unsigned char flag[32]{};
decrypt(enc, key, flag);
decrypt(enc+16, key, flag+16);

printf("NCTF{%.32s}\n", flag);

return 0;
}

flag

NCTF{58cb925e0cd823c0d0b54fd06b820b7e}

gogo

分析

main_main函数可以看到是将输入分块通过channel进行协程通信。

alt text

在main_main函数附近有一个带VM名字的函数,里面就是接收main那边发送的数据,然后底下有个函数执行,是通过操作数进行调用VM函数计算。这边一系列计算函数也印证想法。

alt text

alt text

对每个vm的函数都下断点输出执行的指令以及操作数,方便分析整个加密流程,如main_XOR函数断点:

alt text

alt text

运行输入38个’1’(数据简单方便分析算法),可以得到一堆我们输出的伪代码计算过程,通过看到9e3779b9以及计算的特征,可以发现是XXTEA变种。

31313131就是我们输入的字符串的字节,不过是以4字节为单位进行运算。

通过分析可得知是将输入的flag分成两份,20字节为一组,分别进行两种变种XXTEA计算,Key也不一样,不过都在这里面可以找到Key。

alt text

alt text

可以根据伪代码计算流程进行还原成代码,基于标准XXTEA进行编写,中间可以对比标准算法发现异或Key的值在哪,以及根据加密计算的一些中间值来验证加密代码,最终加密值就可以根据伪代码尾部最终值来验证。(这边不具体赘述)

alt text

还原加密代码如下,基于标准XXTEA进行修改的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#define DELTA 0x9e3779b9

#define MX (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))

#define MX2 (((z << 5 ^ y >> 2) + (y << 3 ^ z >> 4)) ^ ((sum ^ y) + (key2[(p & 3) ^ e] ^ z)))

uint32_t key[]{ 0x6e637466, 0x62ef0ed ,0xa78c0b4f, 0x32303234 };

uint32_t key2[]{ 0x32303234, 0xd6eb12c3, 0x9f1cf72e, 0x4e435446 };

void xxtea_1(uint32_t* v, int n)
{
uint32_t y, z, sum;
unsigned p, rounds, e;
rounds = 16;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < 5; p++)
{
y = v[(p + 1) % 5];
z = v[p] += MX;
}
} while (--rounds);
}

void xxtea_2(uint32_t* v, int n)
{
uint32_t y, z, sum;
unsigned p, rounds, e;
rounds = 16;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < 5; p++)
{
y = v[(p + 1) % 5];
z = v[p] += MX2;
}
} while (--rounds);
}

在main_RET函数可以看到两组字符串的比对,都是20长度的比对,这两个数据就是加密后的flag了,提取出来分别进行解密即可。

alt text

解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
#define DELTA 0x9e3779b9

#define MX (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))

#define MX2 (((z << 5 ^ y >> 2) + (y << 3 ^ z >> 4)) ^ ((sum ^ y) + (key2[(p & 3) ^ e] ^ z)))

uint32_t key[]{ 0x6e637466, 0x62ef0ed ,0xa78c0b4f, 0x32303234 };

uint32_t key2[]{ 0x32303234, 0xd6eb12c3, 0x9f1cf72e, 0x4e435446 };

void xxtea_decrypt1(uint32_t* v, int n)
{
uint32_t y, z, sum;
unsigned rounds, e;
rounds = 16;
sum = DELTA * rounds;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (int p = 5 - 1; p >= 0; p--)
{
if (p == 0)
z = v[4];
else
z = v[(p-1)%5];

if (p == 4)
y = v[0];
else
y = v[p + 1];

v[p] -= MX;

}
sum -= DELTA;
} while (--rounds);
}

void xxtea_decrypt2(uint32_t* v, int n)
{
uint32_t y, z, sum;
unsigned rounds, e;
rounds = 16;
sum = DELTA * rounds;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (int p = 5 - 1; p >= 0; p--)
{
if (p == 0)
z = v[4];
else
z = v[(p - 1) % 5];

if (p == 4)
y = v[0];
else
y = v[p + 1];

v[p] -= MX2;

}
sum -= DELTA;
} while (--rounds);
}

int main()
{
unsigned char enc1[] =
{
0x5D, 0x45, 0xD5, 0xB9, 0x8C, 0x95, 0x9C, 0x38, 0x3B, 0xB1,
0x3E, 0x1E, 0x5F, 0xC8, 0xE8, 0xBB, 0x64, 0x38, 0x48, 0x69
};
unsigned char enc2[] =
{
0xDE, 0x81, 0xD8, 0xAD, 0xC2, 0xC4, 0xA6, 0x32, 0x1C, 0xAB,
0x61, 0x3E, 0xCB, 0xFF, 0xEF, 0xF1, 0x27, 0x30, 0x7A, 0x16
};

xxtea_decrypt1((uint32_t*)enc1, 5);
xxtea_decrypt2((uint32_t*)enc2, 5);

printf("%.20s%.20s\n", enc1,enc2);

return 0;
}

flag

NCTF{H4rd_VM_with_Gor0ut1n3_5fc4b0be7ad}

XLogin

分析

jadx加载发现代码中调用了Decstr函数来解密一些字符串,可以在libsimple.so里面找到DecStr函数实现。

具体解密字符串函数就是Base64换表解密后再异或上当前字符串长度值。

alt text

就可以解密得到这些实际字符串。

alt text

alt text

发现doCheck是在native里面,ida分析libnative.so,发现有个loadDex,应该是动态加载一个dex。

alt text

在loadDex下面就是doCheck函数实际实现。

alt text

通过特征可以发现是使用了3des加密,先猜测他没有魔改,毕竟那一坨代码没人想看。上面的ptr的两个数据(xmmword_1804和unk_1814共24字节数据)就是被加密的flag,3des加密后下面的while循环就是对加密后的数据与flag密文进行比对。

alt text

alt text

在apk的Assets里面会发现又有个libsimple.so,ida分析没有东西,直接拖入010发现存在一个dex结构头,前面的这64字节应该是没用的。

alt text

libnative.so里面loadDex跟到的函数验证了这个猜想,v13是Assets里的libsimple.so文件大小,减去了64,然后下面v17是读取起始指针,也加上了64,说明读取实际文件跳过了前64字节。

alt text

把前64字节去除,使用jadx分析。发现是Check函数,检测了username和password,和题目描述flag格式对应。username这边可以直接解密出来是X1c@dM1n1$t,然后使用username的MD5值作为密钥去调用doCheck加密。

alt text

密钥:7d53ecd36a43d3d237e7dd633dcf8497

然后提取密文进行3des解密即可,然后这边有个端序问题卡了不少师傅一段时间,密文以及密钥都要以8字节位一组转换到大端序,才能解密,然后解密完的明文也要从大端序转回到小端序。

解密

alt text

解密完将明文以8字节一组倒序得到明文SafePWD~5y$x?YM+5U05Gm6=

flag

NCTF{X1c@dM1n1$t_SafePWD~5y$x?YM+5U05Gm6=}