复盘一下强网决赛的Reverse题。

S1mpleVM

附件下载

题目名字已经很明显的告诉你了,就是 vm 逆向。

基本分析

入口其实没啥,就是输入 32 长度的 passcode 然后校验,启动方式是 ./secret_box.exe quest 命令行传参。

可以找到最关键的函数 sub_140001D30 就是 VM 入口。

这个函数里面很明显的 vm_handler

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
__int64 __fastcall vmrun(char *input, char *vmcode)
{
//some defs for local variable
v2 = 0LL;
v3 = *vmcode - 16;
v5 = v48;
v6 = vmcode + 1;
while ( 2 )
{
switch ( v3 )
{
case 0u:
if ( v2 )
{
v9 = v2;
v2 = (signed int *)*((_QWORD *)v2 + 1);
v7 = *v9;
free(v9);
if ( v2 )
{
v10 = v2;
v2 = (signed int *)*((_QWORD *)v2 + 1);
v8 = *v10;
free(v10);
}
else
{
v8 = 0x80000000;
}
}
else
{
v7 = 0x80000000;
v8 = 0x80000000;
}
v11 = (signed int *)j__malloc_base(0x10uLL);
*v11 = v7 % v8;
goto LABEL_53;
case 1u:
//...
LABEL_53:
*((_QWORD *)v11 + 1) = v2;
v2 = v11;
}
}
}

不难发现,v3 是所谓的 opcode,v6 是 PC 指针,并且 vmcode 是实际的字节码 - 0x10,下面来一个个分析这些 vm 的指令。

首先是 0 号指令,做了一个较为复杂的指针操作。这里初看可能啥也看不明白,但是可以发现最下面它分配了 0x10 的空间同时又 v7 和 v8 做模运算了赋值给 v11 指向的地址。

操作完成之后又执行了 *((_QWORD *)v11 + 1) = v2;v2 = v11;,如此种种的迹象显然不难让人联想到一种结构:链表。如果将划分的 0x10 字节内存进行划分,也可以看出,前八个字节存储数据,后八个字节存储指针。

shift+F1 打开 IDA 的 local type 窗口,按 insert 键插入结构体的定义

1
2
3
4
struct LinkEntry{
signed val;
LinkEntry * next;
}

将 v11 和 v2 定义修改之后,IDA 将展示如下的伪代码:

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
case 0u:
if ( v2 )
{
v9 = &v2->val;
v2 = v2->next;
v7 = *v9;
free(v9);
if ( v2 )
{
v10 = &v2->val;
v2 = v2->next;
v8 = *v10;
free(v10);
}
else
{
v8 = 0x80000000;
}
}
else
{
v7 = 0x80000000;
v8 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v11->val = v7 % v8;
goto LABEL_53;

可以说,基本上是一目了然了,并且据此可以联想到一个栈的结果,将两个数 push 进栈中,再弹出来做计算,计算的结果重新入栈。

这里可以将所有 malloc 返回值接收的变量都改成 LinkEntry * 类型。

此时再来观察整个反编译的代码

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
__int64 __fastcall vmrun(char *input, char *vmcode)
{
LinkEntry *v2; // rdi
unsigned int opcode; // eax
unsigned int v5; // r14d
char *PC; // rbp
signed int v7; // esi
signed int v8; // ebx
signed int *v9; // rcx
signed int *v10; // rcx
LinkEntry *v11; // rcx
int v12; // ebx
LinkEntry *v13; // rax
unsigned int *v14; // rcx
unsigned int v15; // esi
unsigned int v16; // ebx
unsigned int *v17; // rcx
unsigned int *v18; // rcx
LinkEntry *v19; // rax
unsigned int v20; // esi
unsigned int v21; // ebx
unsigned int *v22; // rcx
unsigned int *v23; // rcx
int v24; // eax
int v25; // ebx
LinkEntry *v26; // rax
int v27; // esi
int v28; // ebx
signed int *v29; // rcx
int *v30; // rcx
LinkEntry *v31; // rax
unsigned int v32; // esi
unsigned int v33; // ebx
unsigned int *v34; // rcx
unsigned int *v35; // rcx
LinkEntry *v36; // rax
unsigned int v37; // ebx
unsigned int v38; // esi
unsigned int *v39; // rcx
unsigned int *v40; // rcx
LinkEntry *v41; // rax
signed int v42; // esi
signed int v43; // ebx
signed int *v44; // rcx
signed int *v45; // rcx
int v46; // eax
unsigned int v48; // [rsp+58h] [rbp+10h]

v2 = 0LL;
opcode = *vmcode - 16;
v5 = v48;
PC = vmcode + 1;
while ( 2 )
{
switch ( opcode )
{
case 0u:
if ( v2 )
{
v9 = &v2->val;
v2 = v2->next;
v7 = *v9;
free(v9);
if ( v2 )
{
v10 = &v2->val;
v2 = v2->next;
v8 = *v10;
free(v10);
}
else
{
v8 = 0x80000000;
}
}
else
{
v7 = 0x80000000;
v8 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v11->val = v7 % v8;
goto LABEL_53;
case 1u:
v12 = *PC;
v13 = (LinkEntry *)j__malloc_base(0x10uLL);
++PC;
v13->next = v2;
v2 = v13;
v13->val = v12;
goto LABEL_54;
case 2u:
if ( v2 )
{
v14 = (unsigned int *)v2;
v2 = v2->next;
v5 = *v14;
free(v14);
}
else
{
v5 = 0x80000000;
}
goto LABEL_54;
case 3u:
if ( v2 )
{
v17 = (unsigned int *)v2;
v2 = v2->next;
v15 = *v17;
free(v17);
if ( v2 )
{
v18 = (unsigned int *)v2;
v2 = v2->next;
v16 = *v18;
free(v18);
}
else
{
v16 = 0x80000000;
}
}
else
{
v15 = 0x80000000;
v16 = 0x80000000;
}
v19 = (LinkEntry *)j__malloc_base(0x10uLL);
v19->next = v2;
v2 = v19;
v19->val = v15 * v16;
goto LABEL_54;
case 4u:
if ( v2 )
{
v22 = (unsigned int *)v2;
v2 = v2->next;
v20 = *v22;
free(v22);
if ( v2 )
{
v23 = (unsigned int *)v2;
v2 = v2->next;
v21 = *v23;
free(v23);
}
else
{
v21 = 0x80000000;
}
}
else
{
v20 = 0x80000000;
v21 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v24 = v21 + v20;
goto LABEL_52;
case 5u:
sub_1400011B0("%c", v5);
goto LABEL_54;
case 6u:
v25 = *input;
v26 = (LinkEntry *)j__malloc_base(0x10uLL);
v26->next = v2;
v2 = v26;
v26->val = v25;
goto LABEL_54;
case 7u:
if ( v2 )
{
v29 = &v2->val;
v2 = v2->next;
v27 = *v29;
free(v29);
if ( v2 )
{
v30 = &v2->val;
v2 = v2->next;
v28 = *v30;
free(v30);
}
else
{
v28 = 0x80000000;
}
}
else
{
LOBYTE(v27) = 0;
v28 = 0x80000000;
}
v31 = (LinkEntry *)j__malloc_base(0x10uLL);
v31->next = v2;
v2 = v31;
v31->val = (v28 >> v27) & 1;
goto LABEL_54;
case 8u:
if ( v2 )
{
v34 = (unsigned int *)v2;
v2 = v2->next;
v32 = *v34;
free(v34);
if ( v2 )
{
v35 = (unsigned int *)v2;
v2 = v2->next;
v33 = *v35;
free(v35);
}
else
{
v33 = 0x80000000;
}
}
else
{
v32 = 0x80000000;
v33 = 0x80000000;
}
v36 = (LinkEntry *)j__malloc_base(0x10uLL);
v36->next = v2;
v2 = v36;
v36->val = v32 ^ v33;
goto LABEL_54;
case 9u:
++input;
goto LABEL_54;
case 0xAu:
return v5;
case 0xBu:
if ( v2 )
{
v39 = (unsigned int *)v2;
v2 = v2->next;
v37 = *v39;
free(v39);
if ( v2 )
{
v40 = (unsigned int *)v2;
v2 = v2->next;
v38 = *v40;
free(v40);
}
else
{
v38 = 0x80000000;
}
}
else
{
v37 = 0x80000000;
v38 = 0x80000000;
}
v41 = (LinkEntry *)j__malloc_base(0x10uLL);
v41->next = v2;
v2 = v41;
v41->val = v37 - v38;
goto LABEL_54;
case 0xCu:
if ( v2 )
{
v44 = &v2->val;
v2 = v2->next;
v42 = *v44;
free(v44);
if ( v2 )
{
v45 = &v2->val;
v2 = v2->next;
v43 = *v45;
free(v45);
}
else
{
v43 = 0x80000000;
}
}
else
{
v42 = 0x80000000;
v43 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v24 = v42 / v43;
LABEL_52:
v11->val = v24;
LABEL_53:
v11->next = v2;
v2 = v11;
LABEL_54:
v46 = *PC++;
opcode = v46 - 16;
if ( opcode <= 0xC )
continue;
goto LABEL_57;
default:
LABEL_57:
sub_1400011B0("WTF are u doinggg...");
exit(1);
}
}
}

恢复结构体之后这个 vm 还是很一目了然的,下面解释一下各个 opcode 的作用。

  • 0:取模操作(先弹出的值在运算符左侧)
  • 1:push 操作
  • 2:pop 操作
  • 3:乘法操作
  • 4:加法操作
  • 5:输出
  • 6:取输入指针
  • 7:右移位后取最低位(先弹出的值在运算符右侧)
  • 8:异或操作
  • 9:输入指针+1
  • 10:返回
  • 11:减法操作(先弹出的值在运算符左侧)
  • 12:除法操作(先弹出的值在运算符左侧)

动态分析

同时可以根据操作自己实现虚拟机,这里已经很清楚是栈的数据结构了就直接用 std::stack 实现即可。

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
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/fcntl.h>
#include<stack>
std::stack<int>s;
char buffer[0x10000];
int getstackval(){
int val=0x80000000;
if(s.size()){
val=s.top();
s.pop();
}
return val;
}
void pushstackval(int val){
printf("push val %d\n",val);
s.push(val);
}
int main(){

int fd=open("./quest",0);
size_t ret=read(fd,buffer,0x10000);
char *PC=buffer;
int reg1,reg2,reg3;
char input[]="flag{aaaaaaaaaaaaaaaaaaaaaaaaaa}";
char *p=input;
while(1){
char opcode=*PC-0x10;
char operand;
PC++;

switch (opcode) {
case 0:
reg2=getstackval();
reg3=getstackval();
pushstackval(reg2%reg3);
printf("calc %d %% %d=%d",reg2,reg3,reg2%reg3);
break;
case 1:
operand=*PC++;
pushstackval(operand);
break;
case 2:
reg1=getstackval();
printf("pop %d to reg1\n",reg1);
break;
case 3:
reg2=getstackval();
reg3=getstackval();
printf("calc %d*%d=%d\n",reg2,reg3,reg2*reg3);
pushstackval(reg2*reg3);
break;
case 4:
reg2=getstackval();
reg3=getstackval();
printf("calc %d+%d=%d\n",reg2,reg3,reg2+reg3);
pushstackval(reg2+reg3);
break;
case 5:
printf("output a char %c\n",reg1);
break;
case 6:
printf("get %d input\n",p-input+1,*p);
pushstackval(*p);
break;
case 7:
reg3=getstackval();
reg2=getstackval();
printf("calc (%d>>%d)&1=%d\n",reg2,reg3,(reg2>>reg3)&1);
pushstackval((reg2>>reg3)&1);

break;
case 8:
reg2=getstackval();
reg3=getstackval();
printf("calc %d^%d=%d\n",reg2,reg3,reg2^reg3);
pushstackval(reg2^reg3);
break;
case 9:
p++;
break;
case 10:
printf("retval=%d\n",reg1);
return reg1;
case 11:
reg2=getstackval();
reg3=getstackval();
printf("calc %d-%d=%d\n",reg2,reg3,reg2-reg3);
pushstackval(reg2-reg3);
break;
case 12:
reg2=getstackval();
reg3=getstackval();
printf("calc %d/%d=%f\n",reg2,reg3,reg2/reg3);
pushstackval(reg2/reg3);
break;
default:
printf("invalid op");
exit(0);
break;
}
}
}

下面节选一段log

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
get 1 input
push val 102
push val 0
calc (102>>0)&1=0
push val 0
push val 2
calc 2*0=0
push val 0
get 1 input
push val 102
push val 1
calc (102>>1)&1=1
push val 1
push val 3
calc 3*1=3
push val 3
get 1 input
push val 102
push val 2
calc (102>>2)&1=1
push val 1
push val 67
calc 67*1=67
push val 67
get 1 input
push val 102
push val 3
calc (102>>3)&1=0
push val 0
push val 37
calc 37*0=0
push val 0
get 1 input
push val 102
push val 4
calc (102>>4)&1=0
push val 0
push val 41
calc 41*0=0
push val 0
get 1 input
push val 102
push val 5
calc (102>>5)&1=1
push val 1
push val 11
calc 11*1=11
push val 11
get 1 input
push val 102
push val 6
calc (102>>6)&1=1
push val 1
push val 13
calc 13*1=13
push val 13
get 1 input
push val 102
push val 7
calc (102>>7)&1=0
push val 0
push val 89
calc 89*0=0
push val 0
calc 0+13=13
push val 13
calc 13+11=24
push val 24
calc 24+0=24
push val 24
calc 24+0=24
push val 24
calc 24+67=91
push val 91
calc 91+3=94
push val 94
calc 94+0=94
push val 94
push val 70
calc 70^94=24
push val 24
get 2 input
push val 108
push val 0
calc (108>>0)&1=0

最前面事实上就是输出一句话 Thank for providing passcode, my ultimate secret box is checking... 用的,跳过之后就能看到。其中最明显的应该能看到它频繁的取输入字符并做 (x>>y)&1 的运算,y 从 0~7,不难想到,这是在一个一个取出输入字节的每一位,每一位都对应了一个权值。第一个字节可以看出来,从低位到高位权值分别为

1
2 3 67 37 41 11 13 89

而最后,它将所有权值相加,得到的结果和 70 做异或运算得到 24,将该值存入栈底。

而把log拉到最后发现

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
calc 137+71=208
push val 208
calc 208+39=247
push val 247
calc 247+120=367
push val 367
calc 367+22=389
push val 389
calc 389+119=508
push val 508
calc 508+89=597
push val 597
calc 597+22=619
push val 619
calc 619+218=837
push val 837
calc 837+203=1040
push val 1040
calc 1040+125=1165
push val 1165
calc 1165+125=1290
push val 1290
calc 1290+5=1295
push val 1295
calc 1295+118=1413
push val 1413
calc 1413+30=1443
push val 1443
calc 1443+59=1502
push val 1502
calc 1502+89=1591
push val 1591
calc 1591+213=1804
push val 1804
calc 1804+114=1918
push val 1918
calc 1918+35=1953
push val 1953
calc 1953+18=1971
push val 1971
calc 1971+18=1989
push val 1989
calc 1989+121=2110
push val 2110
calc 2110+65=2175
push val 2175
calc 2175+32=2207
push val 2207
calc 2207+221=2428
push val 2428
calc 2428+253=2681
push val 2681
calc 2681+348=3029
push val 3029
calc 3029+130=3159
push val 3159
calc 3159+92=3251
push val 3251
calc 3251+140=3391
push val 3391
calc 3391+24=3415
push val 3415
pop 3415 to reg1
retval=3415

我们所计算的第一个异或值,在最后一刻被加起来返回了。

而外面判断我们的输入是否正确,依赖于返回值是否为 0,因此我们要尽可能让每次异或值都相等,这里用个小技巧,将要输出的值打印到 stderr 中,再用重定向 2>out.txt 就可以快速拿到一些值。

首先我们拿异或的目标值,在异或的 opcode 中加入 fprintf(stderr,"%d,",reg2);,得到值。

然后拿每一个字节的每一位的权值,在 * 运算中加入 fprintf(stderr,"%d,",reg2);,得到值。

最终根据逻辑,写出还原 passcode 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
char v[]={
2,3,67,37,41,11,13,89,2,3,67,5,7,47,61,29,2,67,37,7,43,11,13,31,97,3,41,73,11,13,53,29,97,67,3,11,43,13,47,83,67,5,37,71,7,11,89,29,2,3,5,11,13,83,53,61,2,3,7,71,43,83,29,31,7,73,11,13,53,89,29,31,2,3,5,37,7,43,13,61,2,5,7,43,11,13,53,89,5,7,73,43,11,13,59,31,3,5,73,41,43,13,83,89,2,7,71,11,43,13,29,61,2,5,7,11,13,79,47,83,3,67,37,5,73,11,13,61,2,67,5,7,71,11,13,61,67,3,5,37,43,11,13,61,2,3,37,7,71,41,11,29,3,5,41,11,43,47,53,29,2,3,7,71,43,13,47,79,2,3,5,37,11,43,13,79,97,67,5,37,7,41,11,61,3,71,7,43,11,79,53,61,2,3,71,73,11,13,61,31,97,2,3,67,5,11,13,83,2,3,5,37,7,41,11,53,2,3,73,43,11,13,53,61,2,67,3,37,7,11,47,59,2,37,5,73,13,47,53,59,2,67,71,73,41,11,13,89,2,3,67,37,73,11,43,59,};
char target[]={70,56,70,77,74,90,87,82,60,67,86,95,64,94,85,66,33,69,64,98,67,71,94,93,90,32,65,82,68,65,93,96,};
int checkval(int i,int pos){
int sum=0;
for(int j=0;j<8;j++){
sum+=((i>>j)&1)*v[j+pos*8];
}
return sum;
}
int main(){
for(size_t i=0;i<sizeof(target);i++){
for(int j=0x20;j<127;j++){
if(target[i]==checkval(j,i)){
putchar(j);
break;
}
}
}
}
//s1mpl3_VM_us3s_link3d_l1st_st4ck

输入后得到 flag

最后注意一下,这个反调试有点隐蔽,只要中了反调试就会修改 quest 这个文件,如果你不注意文件修改日期的话那么永远也算不出正确答案了。

反调试手段分析

如何发现反调试?通常情况下,我们不会刻意地去注意反调试,只有当程序提示,报错,运行结果附加调试器与不附加调试器运行结果有较大差异时,才会去注意反调试。

这里程序初始化的时候会拉起反调试,不过需要非常仔细,能够敏锐地观察到 vm 的代码文件被修改了。

首先确定反调试的位置,在main函数下断点,检查文件是否被修改。

发现main函数之前反调试就运行完了,那么就要讲到 main 函数之前调用的代码了,在 Linux 中,会保存一个 init_array,它会保存一系列的函数指针,这些函数先于 main 被调用;同样的,在 windows 中也有类似的。

通过 initterm 函数交叉引用找到 First 指针,获取函数指针的起始地址。

在指向的函数中,sub_140001190 是反调试的关键函数

其中对 FileName 变量似乎在做一个异或解密的操作,异或的key是0x69,dump下来解密之后发现果然是在对目标文件进行操作。

下面一系列就是写这个文件了,不过在这之前有一个关键判断 qword_14002ED10,这个函数指针保存了哪个函数,大概率是 IsDebuggerPresent 了,但是还是去验证一遍。

交叉找到赋值的位置,是在这之前执行的函数内容。

下断点,看看能否断在这里,发现果真如我们所料

那么这个反调试的过程就是在main函数之前先执行了两个函数,一个函数获取 IsDebuggerPresent 函数的地址保存在全局变量中,第二个函数调用这个 API 判断,如果的确被调试那么修改 quest 的文件内容。

对于这个绕过直接上 xdbg 的反调试插件 or 修改文件名,队爹就是直接上 xdbg 甚至感受不到反调试的存在,而我狂踩坑…

UnsafeFile

题目描述

附件下载,解压密码 2024qwbfinal

作者在此申明:本题目为类勒索病毒分析的题目,若要分析请一定一定不要在个人电脑或者公共电脑上运行此程序,请在虚拟机中调试分析,若因此造成任何的损失与作者无关。

以下是原题目描述

小Y玩游戏很菜,于是他找了个神秘人要了一个修改器文件,在开启功能后,发现他的一个重要文件居然被加密了,你能想办法帮他恢复吗?

请不要在物理机上运行题目中的任何文件,主办方对由此造成的任何损失不承担任何责任,如有需要请在虚拟机内进行运行和调试,解压密码:2024qwbfinal

基本分析

压缩包给了两个文件,一个是 CT 脚本,一个是 .pdf.yr,看起来 .yr 是一个勒索了 pdf 类型文件的后缀。

先看看 CT 脚本,运行之后会拉起 DBK 驱动,运行计算器,并且看标题似乎是一个植物大战僵尸的修改器。

其中比较主要的就是运行了一个 decodeFunction 去解密一段函数运行,这里可以直接用网上的脚本还原这段。

运行后得到一个 luac 文件,luac 文件需要用另一个工具去还原为 lua 脚本,这里我是用的是 unluac_2023_12_24.jar,同样附上下载地址:https://sourceforge.net/projects/unluac/files/Unstable/

前面都是一些赋值函数,拉到最后发现几个有意思的字符串,其中 C:\\system.dll 引起了注意,于是去对应目录下,能找到一个 dll 文件,那么毫无疑问,剩下的就是对 system.dll 进行分析了,lua 脚本应该就是做注入用的。

dllmain 一个很标准的起线程的动作

这个 StartAddress 就比较有意思,一直执着于判断自身某个内存的标志位,循环,而循环体内就是一直在 Sleep。

中间用 FindCrypt 发现 AES 的模式。

那么毫无疑问,勒索的文件应该是使用 AES 加密的,交叉找到对应的函数,其中一个是 10001840,另一个是 10001790

静态分析比较难了,下面开始动态分析。

动态调试

因为是个 dll,还不像 exe 那样好调试,这里我写了一个简单的 demo

1
2
3
4
5
6
7
8
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main(){
while(1){
Sleep(1000);
}
}

然后直接远线程注入这个模块,为了能断下,选择 patch dll 的 dllmain 函数开头为 int 3

然后,虚拟机里

x32 起被调试程序,用注入器注入这个 dll。

程序成功被断下来

这里可以去恢复 int 3 指令然后重新执行一遍。

如果不改那个标志位,发现 while ( !byte_1000C6DC ) 将会是一个死循环,这里估计是要等某个合适的时机,因此找到这个标志位将它赋值为1。

跳出循环之后,执行了一个函数

看字符串,获取了用户名,又有 Documents 字符串,猜测是对我们用户目录下的文档文件感兴趣,这里可以随意写几个pdf文件让它加密看看它的规律。

往后跟进几步,发现关键字符串

往后跟进之后发现,抛异常了,不过很幸运的是,栈中有数据提示我们

文件大小需要时 16 的倍数,那就换一个 16 个 a 的 pdf 文件再来一次。

随后调试到调用 AES 函数之前,这里需要分析一下这个参数的作用,调用约定属于 thiscall,this 指针存 ECX,其余参数从右往左压入栈中。

这里记录一下第三个参数指向的一片内存,这个内存每次运行都不一样,先记录一下。

C3 67 B7 93 5E 0D AB 9A 48 3D BA EB 65 5F B5 92

而第二个参数就是指向了一片 0xBAADF00D 的内存。运行放过去,同时火绒剑也查获了一下这个 dll 的行为。

加密文件,删除文件,典型的勒索病毒,同时也注意到多次运行的加密结果是不一样的,而且原本16字节的结果变成了48字节,AES就算是 padding 模式也不会多 32 字节,于是断定它必然是随机加密,而密钥肯定也保存到了文件里,这里 AES 的参数很有可能就是密钥。

其实原本这里有点山穷水尽了,后面的结论要得出来对我就比较看运气(高手肉眼就看出来)了。

偶然的情况下,上面的第二个参数生成了 00 字节,而对应的第二行位置上生成了 0x5A,又联想到之前 lua 脚本写过一段异或 0x5a 的脚本。

相对应地,去源码中找到这一段 system.dll+25C6,在此下断点调试。

发现它在异或 0068D14D 的内存,异或了 0x10 个字节,难道说这就是所看到的密钥,验证一遍

发现果然就是它会将真实密钥每个字节异或 0x5A 之后保存到倒数第二行,那么最后一行必然不可能是 padding,猜测应该是初始向量,这是一个 CBC 模式的 AES。

通过研究 lua 脚本还发现,它似乎还对 DLL 进行了 hook,并且使用了 WriteByte 将 system.dll+C6DC 写为了 1,好样的,就是前面死循环的条件 while(!byte_1000C6DC),在这一刻完成了闭环。这个 dll 不仅用 lua 进行注入,还进行了一定的 hook,直接运行分析样本可能真分析不太出来,其实这里猜也能猜个大概了,但是作者这里觉得还是力求分析完整这个样本。

首先看看 initterm 函数的函数指针,发现了一些有意思的函数

使用 std::_Random_device 获取随机数,随后使用梅森旋转算法计算后续的随机数,这里 0x6C078965 是该算法的一个常数,搜也是能搜出来的。

而随后将计算出的这么多随机数,使用一定的算法将某 0x10 个字节赋值到了 this 指针,一共调用了两次这个函数,一个在 sub_10001000,另一个在 sub_10001020。赋值的全局变量分别在 1000C6E01000C6EC,这个是一开始就生成好的。

自己再去调试一遍也可以验证得到 1000C6E0 指针所指向的值,就是被异或加密前的密钥,或者说就是 pdf 倒数第二行异或 0x5A 的结果,而最后一行的结果与 1000C6EC 指针所指向的内容是一致的。

因此可以验证一遍:

确定没问题之后就可以开始恢复 pdf 文件了,但是因为我的 system.dll 没有做 hook,而 lua 脚本运行的时候做了 hook,因此在题目加密的 pdf 文件中,要先交换高低半个字节,再异或,才是原始密钥。

先写一下解密 key 的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>

int main(){
unsigned char key[]="\xcd\x8b\x95\xe3\x1f\x16\xd9\x21\x6b\x3c\x3c\x24\xb2\x6e\x98\xe7";
for(int i=0;i<16;i++){
unsigned char t=key[i]&0xf;
key[i]>>=4;
key[i]|=t<<4;
key[i]^=0x5A;
printf("%02x ",key[i]);
}
}

拿得到的结果去解密

可以发现已经是一个 pdf 文件头了,下载,打开查看,flag 到手。


还有一道 bvp47 也是一道恶意样本分析的题,但是目前精力有限,可能要咕很久才能做出来了233。