题目附件:https://pan.baidu.com/s/1qEbEZ_xdS33hOiExvLR7ng?pwd=3457

一个exe程序,直接拖到ida里,发现没有main,很混乱,直接猜测上了自解密 + 混淆花指令;

根本看不到伪代码,硬啃汇编罢;

image.png

不打算修,太多了,直接调,函数列表搜索scan,可以拿到库函数:

?scan_optional_field_width@?$format_string_parser@D@__crt_stdio_input@@AEAA_NXZ

在这个函数里打上断点,可以在输入内容的时候断下,然后一层一层的往上层函数断点测试:

可以找到最终的scanf调用实际上使用了下面这个函数:

??$?RV_lambda_a81aa23bb2c9577c1e55b9d0b57d9de4_@@AEAV_lambda_9a20e10065b92b5193c3597a66cba9d4_@@V_lambda_cb3a421ff86d8a5f008440ee6b28fa9c_@@@?$__crt_seh_guarded_call@H@@QEAAH$$QEAV_lambda_a81aa23bb2c9577c1e55b9d0b57d9de4_@@AEAV_lambda_9a20e10065b92b5193c3597a66cba9d4_@@$$QEAV_lambda_cb3a421ff86d8a5f008440ee6b28fa9c_@@@Z_1

也是一个库函数,接着这个函数往下走,可以走到一个神奇的地方,我称之为中转站,也是找到虚拟机的特征,操作码一类的东西:

image.png

这个rax实际上就是一个操作码,每次都有不同的功能,在前几次调试测试会发现,第一次调用call之后,就会打印wrong flag;

而当rip在这个地方的时候往栈上看,能发现我们输入的内容:

image.png

对着我们输入的地址按x查看引用能找到一个地方,调过去能发现这是第一次call会执行的,再次调试能够知道实际上是在调用strlen函数,它将strlen给到rax,然后call rax,过了中转站之后第二次call就会打印wrong flag:

image.png

对着strlen再继续跟下去会得知flag长度是56;

如果长度是正确的,第一次call完之后,剩下的call就开始循环了,貌似在操作输入的字符串;

通过每三次call,可以发现它是一个动作,每三次rdx都会加2,当加到70h之后变成新的循环;

实际上也就是以56为一个循环,一共要循环两次,但是调试的时候发现第一次循环没有对输入的内容做修改,而第二次循环会对输入的内容替换为用7F去减去它本身的值(通过观察栈上的值);

这56长度的循环结束之后,会有一个短循环,一共call 26次,第25次就会输出wrong flag,第26次进入结束程序;

通过调试可以发现,这26次里面,在对输入的内容进行分组加密,每8字节为一组,应该是电码本模式,因为56个a加密的东西分组的很明显:

image.png

对8个字节的分组加密部分也是3次call为一个动作,一共执行21次,把7个分组都加密完,剩下的4次call很可能就是用来进行判断的;

跟踪剩下的call可以发现很难看,很多都是没用的跳转,所以结合着打上内存断点,可以找到如下内容:

WTJ4QTO33P_YL308F_A~_CJ.png

image.png

它会依次获取加密后的输入数据,以及比较数据?这个比较数据每次都在变化,然后进行比较,通过调试改值,可以一路改下去,然后就能够使得程序输出right flag的字符串,打到这个时候其实已经就很有信心能出了;

为什么直接引用outandin地方的地址不能直接x获取到这些被引用的地方呢?

是因为这个混淆做了一个表,它每次要获取地址的时候,都是用代码段上的立即数去加或者减这个表里的无意义的数据,得到一个可用的地址,这样就防止了地址引用的查询,但是还是逃不过内存断点;

使用如下ida的python脚本进行更改比对值且输出比较内容:

1
2
3
ea=get_reg_value("r8")
print(hex(ea))
set_reg_value(ea, "rdx")

对打印的内容进行整理拿到如下比较hex:

1
BE 44 7B 02 BA 95 4B 8C E3 A8 F1 90 FB CD A4 3C 2F EE 9E 68 79 AA 6D ED 85 B0 77 2F 27 3F 41 FF 1F C1 CF 43 AA 00 AC FA 71 43 57 09 51 BA F7 B2 67 96 52 47 A0 50 40 C7

现在要做的就是查看分组加密算法了,回到当时加密的部分,一步一步的跟踪看过去,发现程序会去操作两个寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
算法逻辑:

ecx 给到12345678h 异或 ffffffff 与上 4C4CDB01 -> 4C488901 xor ffffffff -> B3B776FE

eax 给到12345678h 加上 B3B324FE 异或 ffffffff -> EDCFFB87

ecx = ecx & eax = A1877286 = B3B776FE & EDCFFB87

eax = ecx = ecx xor 0xffffffff = 5E788D79

eax xor 0xffffffff = A1877286

ecx = ecx & 4CD6DA30 xor 0xffffffff = B3AF77CF -> 12AE5749 -> EDD9AEF6

eax = eax & 0B32925CF xor 0xffffffff = 5EFEDF79 xor ffff -> ED51A8B6 -> 8C1128B0 -> 61C88646 ->9E3779B9

那最终经过一段又臭又长的小丑代码膨胀之后,你会拿到eax会变成 0x9e3779b9;

包是tea里面的delta,弟弟;

通过进一步调试我能拿到如下栈帧内容:

image

能够拿到key和轮次,那么直接把三个tea都拿来试,试完可以发现是xtea(不是tea和xxtea);

写逆运算,直接拿到不可见字符,太棒了,我逐渐理解一切;

思考是否是因为有反调试在搞我,于是尝试用ida附加进程,发现附加不上,很大可能;

拿CE进行附加调试,可以发现最终的比较数据发生了变化,同时我们输入计算的结果也发生了变化(千万别用其他算法搞我):

image

第一个思路是找idata段(iat表)用到了反调试的哪些函数,可以找到疑似的如下:

1
2
.idata:00007FF704979060 20 7F B1 DD F8 7F 00 00       IsDebuggerPresent 
.idata:00007FF704979038 00 11 B1 DD F8 7F 00 00 GetSystemTimeAsFileTime

对其进行x引用反查并下断点,能够发现不是他们的原因(不会断下来);

接着对ida里进行search,搜索字节块,直接搜索hex 60 (对于静态反调试而言,PEB结构很重要,32位是fs:30 64位是gs:60)

然后在搜索的结果里ctrl+f筛选gs,果然给找到了:

image.png

之后在这里给下个断点,ida调试一启动程序就到这里来,先把eax改成0,然后把上面一条句子改成xor eax,eax nop nop;

就可以不用管这个地方了;

之后还是一样的调试,能够发现第一轮56长度循环的时候对输入做改动了,对输入的每个字节进行加40h,第二轮循环还是老样子,分组tea也是老样子(还好没变,变了我要把出题人给草草了),然后比较的数据也变了,内容就是ce里面的,说明ce附加调试是正确的数据,用之前的方法再提一遍数据:

1
a1 e3 51 98 86 56 76 49 6f 6b 2b 81 cf ce 12 96 a2 70 35 3c 31 62 5c f1 fa 77 6b aa 9e 6d 05 be e8 24 a4 f8 db 23 3a 0b 16 20 cc 03 ad b5 2b a9 34 9f 78 1d 2e b9 f9 9e

这次数据就是正确的了;

反思:拿到怪玩意儿先找反调试,不然后期恶心死我;

之后写脚本进行flag获取:

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
128
#include<iostream>
using namespace std;

typedef unsigned int DWORD;

void teaE(DWORD* EntryData, DWORD* Key)
{
//分别加密数组中的前四个字节与后4个字节,4个字节为一组每次加密两组
DWORD x = EntryData[0];
DWORD y = EntryData[1];

DWORD sum = 0;
DWORD delta = 0x9E3779B9;
//总共加密32轮
for (int i = 0; i < 0x66; i++)
{
sum += delta;
x += ((y << 4) + Key[0]) ^ (y + sum) ^ ((y >> 5) + Key[1]);
y += ((x << 4) + Key[2]) ^ (x + sum) ^ ((x >> 5) + Key[3]);
if(i==0)
printf("%d轮:v0: %x v1: %x\n", i, x, y);
}
//最后加密的结果重新写入到数组中
EntryData[0] = x;
EntryData[1] = y;
}

void xteaE(DWORD v[2], DWORD const key[4]) {
unsigned int i;
DWORD v0 = v[0], v1 = v[1], sum = 0, delta = 0x9E3779B9;
for (i = 0; i < 0x66; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
//if (i == 0)
//printf("%d轮:v0: %x v1: %x\n", i, v0, v1);
}
v[0] = v0; v[1] = v1;
}

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

void xxtea(DWORD* v, int n, DWORD const key[4])
{
DWORD y, z, sum;
unsigned p, rounds, e;

rounds = 0x66;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < n - 1; p++)
{
y = v[p + 1];
z = v[p] += MX;
}
y = v[0];
z = v[n - 1] += MX;
if (rounds == 0x66)
printf("%d轮:v0: %x v1: %x\n", rounds, y, z);
} while (--rounds);

}

//上面这三个都是在尝试
//比较数据
//unsigned int ans[14] = { 假的,出生
// 0x027B44BE, 0x8C4B95BA, 0x90F1A8E3, 0x3CA4CDFB, 0x689EEE2F, 0xED6DAA79, 0x2F77B085, 0xFF413F27,
// 0x43CFC11F, 0xFAAC00AA, 0x09574371, 0xB2F7BA51, 0x47529667, 0xC74050A0
//};

unsigned int ans[14] = {
0x9851E3A1, 0x49765686, 0x812B6B6F, 0x9612CECF, 0x3C3570A2, 0xF15C6231, 0xAA6B77FA, 0xBE056D9E,
0xF8A424E8, 0x0B3A23DB, 0x03CC2016, 0xA92BB5AD, 0x1D789F34, 0x9EF9B92E
};

//unsigned char ans[] = "\xde\xde\xde\xde\xde\xde\xde\xde";
//DWORD res[] = { 0x027B44BE ,0x8C4B95BA };
//DWORD k[4] = { 2,2,3,4 };

DWORD k[4] = { 0xEF6FD9DB, 0xD2C273D3, 0x6F97E412, 0x72BFD624 };

void xteaD(DWORD v[2], DWORD const key[4]) {
unsigned int i;
DWORD v0 = v[0], v1 = v[1], delta = 0x9E3779B9, sum = delta * 0x66;
for (i = 0; i < 0x66; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0] = v0; v[1] = v1;
}

int main()
{
// printf("%x %x\n", ((DWORD*)ans)[0], ((DWORD*)ans)[1]);
//xteaE((DWORD*)ans,k);
//xxtea((DWORD*)datas,2, k);

//printf("%x %x\n", ((DWORD*)ans)[0], ((DWORD*)ans)[1]);
for (int i = 0; i < 14; i += 2)
{
xteaD((DWORD*)&(((DWORD*)ans)[i]), k);
}
unsigned char* str = (unsigned char*)ans;
for (int i = 0; i < 56; i++)
{
printf("%02x ", str[i]);
}
printf("\n\n\n");
for (int i = 0; i < 56; i++)
{
printf("%c", (unsigned char)((0x7f - str[i])- 0x40));
}
//printf("%x", ans[2]);
//DWORD sum = 0;
//for (int i = 0; i < 0x66; i++)
//{
// sum += DELTA;
//}
//printf("%x", sum);

return 0;
}

最后拿到flag:flag{u_ar3_re@11y_g00d_@t_011vm_de0bf_and_anti_debugger}

草草了,ollvm,还是第一次见混淆之后的程序,太抽象了,有一种vmp的美,给我搞了一天,难绷;

根据flag可以知道有一个deobf的工具应该可以有效去除ollvm的混淆,之后可以研究来看看;