看雪CTF2017第11题WP

这是看雪CTF2017比赛的第11题。题目地址:传送门

应某童鞋的要求,写个新手能跟着照做的题解,我想就拿这个题试试看吧。毕竟我也常有这样的苦恼,大神们觉得很普通的操作过程,一句话说完了,我百思不得其道,有点同病相怜吧。

初探

直接ida载入,发现入口被改,此时入口是直接调用有代码修改功能的函数sub_44701F。此函数先读取程序文件.text区段开始的内容,再进行异或及算术运算,把运算结果复制到当前运行程序的相应地址空间。然后还进行了部分api的加载,最后跳转到像是VC程序OEP特征的入口。部分代码如下:

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
memcpy(result, (char *)lpBaseAddress + 0x1000, 0x45000u);
v1 = 0;
v2 = Dst;
while ( v1 < 0x11400 )
{
*v2 ^= v1 + 0x19930721;
*v2 += v1;
*v2 -= 0x13579246;
++v2;
++v1;
}
v3 = (char *)Dst;
result = (void *)sub_447000(Dst); // 验证pe标识
if ( result
&& (ntHeader = (int)&v3[*((_DWORD *)v3 // ntheader
+ 0xF)],
v24 = *(_WORD *)(ntHeader + 6), // section number
v23 = *(_WORD *)(ntHeader + 0x14), // SizeOfOptionalHeader
ImageBase = *(char **)(ntHeader + 0x34),// ImageBase
Entry = &ImageBase[*(_DWORD *)(ntHeader + 0x28)],// base+AddressOfEntryPoint
SizeOfImage = *(_DWORD *)(ntHeader + 0x50),// SizeOfImage
Size = *(_DWORD *)(ntHeader + 0x54), // SizeOfHeaders
Buf1 = &ImageBase[*(_DWORD *)(ntHeader + 0x80)],// IMAGE_DATA_DIRECTORY Import
(result = (void *)VirtualProtect(ImageBase, 0x45000u, 0x40u, &flOldProtect)) != 0) )
{
memcpy(ImageBase, Dst, Size);
s_hdr = (IMAGE_SECTION_HEADER *)(v23 + ntHeader + 0x18);// Section header
v18 = s_hdr;
v6 = 0;
while ( v6 < v24 )
{
v7 = v6;
memcpy(&ImageBase[s_hdr->VirtualAddress], (char *)Dst + s_hdr->PointerToRawData, s_hdr->SizeOfRawData);
v6 = v7 + 1;
++s_hdr;
}
v8 = Buf1;
while ( memcmp(v8, &Buf2, 0x14u) )
{
if ( *(_DWORD *)&ImageBase[*((_DWORD *)v8 + 4)] )
{
v9 = LoadLibraryA(&ImageBase[*((_DWORD *)v8 + 3)]);
if ( v9 )
{
hModule = v9;
v10 = *(_DWORD *)v8;
if ( !*(_DWORD *)v8 )
v10 = *((_DWORD *)v8 + 4);
v11 = &ImageBase[v10];
v12 = &ImageBase[*((_DWORD *)v8 + 4)];
while ( *(_DWORD *)v11 )
{
if ( *(_DWORD *)v11 & 0x80000000 )
v13 = (const CHAR *)(*(_DWORD *)v11 & 0xFFFF);
else
v13 = &ImageBase[*(_DWORD *)v11 + 2];
*(_DWORD *)v12 = GetProcAddress(hModule, v13);
v11 += 4;
v12 += 4;
}
v8 = (char *)v8 + 0x14;
}
}
else
{
v8 = (char *)v8 + 20;
}
}
if ( Dst )
{
VirtualFree(Dst, 0, 0x8000u);
lpBaseAddress = 0;
}
if ( lpBaseAddress )
{
UnmapViewOfFile(lpBaseAddress);
lpBaseAddress = 0;
}
if ( hFileMappingObject )
{
CloseHandle(hFileMappingObject);
hFileMappingObject = 0;
}
if ( hFile != (HANDLE)-1 )
{
CloseHandle(hFile);
hFile = (HANDLE)-1;
}
result = (void *)((int (__thiscall *)(int))Entry)(savedregs);//跳转到入口
}

看到这,我就想如果我直接把计算后的结果dump出来,是不是就可以了。至于加载的api什么的看看情况再说吧。

我就是这么做的,dump出来,程序直接能运行,图标也似乎正常了。没看出哪有区别。但是为了保险起见,静态都是用的这个dump出来的看的,动态都是用的原程序,那也只是多了个加断点的过程,其它并不影响。在最后我特意验证了下,程序功能似乎更改了。

正确流程寻找

OD载入程序,一路粗跟,最后来到401690函数,检查两个全局变量是否为0,第一个变量439B50为0就直接返回了;第二个变量439B51不为零,后面似乎还有几个函数调用,但是没有启动窗体。

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
00401690 /$ 55 push ebp
00401691 |. 8BEC mov ebp,esp
00401693 |. 83EC 70 sub esp,0x70
00401696 |. 803D 509B4300>cmp byte ptr ds:[0x439B50],0x0
0040169D |. 0F84 88000000 je 11-ToBeB.0040172B
004016A3 |. E8 08FFFFFF call 11-ToBeB.004015B0
004016A8 |. 803D 519B4300>cmp byte ptr ds:[0x439B51],0x0
004016AF |. 74 1C je short 11-ToBeB.004016CD
004016B1 |. 6A 00 push 0x0 ; /lParam = NULL
004016B3 |. 68 40174000 push 11-ToBeB.00401740 ; |DlgProc = 11-ToBeB.00401740
004016B8 |. 6A 00 push 0x0 ; |hOwner = NULL
004016BA |. 6A 67 push 0x67 ; |pTemplate = 0x67
004016BC |. FF75 08 push [arg.1] ; |hInst = NULL
004016BF |. FF15 94A14200 call dword ptr ds:[0x42A194] ; \DialogBoxParamW
004016C5 |. 33C0 xor eax,eax
004016C7 |. 8BE5 mov esp,ebp
004016C9 |. 5D pop ebp ; 0019FEC4
004016CA |. C2 1000 retn 0x10
004016CD |> 8D4D 90 lea ecx,[local.28]
004016D0 |. E8 0BF10000 call 11-ToBeB.004107E0
004016D5 |. E8 C6FEFFFF call 11-ToBeB.004015A0
004016DA |. 8D4D EC lea ecx,[local.5]
004016DD |. 8B40 30 mov eax,dword ptr ds:[eax+0x30]
004016E0 |. 50 push eax
004016E1 |. E8 AAEB0000 call 11-ToBeB.00410290
004016E6 |. 8D4D 90 lea ecx,[local.28]
004016E9 |. E8 C2FE0000 call 11-ToBeB.004115B0
004016EE |. 8D45 EC lea eax,[local.5]
004016F1 |. 50 push eax
004016F2 |. 8D4D 94 lea ecx,[local.27]
004016F5 |. E8 66180000 call 11-ToBeB.00402F60
004016FA |. 8D4D EC lea ecx,[local.5]
004016FD |. E8 2EEC0000 call 11-ToBeB.00410330
00401702 |. FF35 A0404300 push dword ptr ds:[0x4340A0] ; 11-ToBeB.00401C30
00401708 |. 8D4D 90 lea ecx,[local.28]
0040170B |. E8 30FF0000 call 11-ToBeB.00411640 ; !!!!!
00401710 |. 6A 20 push 0x20
00401712 |. 8D4D 90 lea ecx,[local.28]
00401715 |. E8 26F40000 call 11-ToBeB.00410B40
0040171A |. 50 push eax
0040171B |. 8D4D 90 lea ecx,[local.28]
0040171E |. E8 EDFC0000 call 11-ToBeB.00411410 ; 新线程
00401723 |. 8D4D 90 lea ecx,[local.28]
00401726 |. E8 F5F10000 call 11-ToBeB.00410920
0040172B |> 33C0 xor eax,eax
0040172D |. 8BE5 mov esp,ebp
0040172F |. 5D pop ebp ; 0019FEC4
00401730 \. C2 1000 retn 0x10

继续走,窗体创建,设etDlgItemTextW api断点(至于GetDlgItemTextW肯定不是,看看就知道),输入确定。来到401740,还是看伪代码吧。方便看些:

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
if ( a2 == 272 )
return 1;
if ( a2 != 273 )
return 0;
if ( (_WORD)a3 != 1000 )
{
if ( (_WORD)a3 == 2 )
EndDialog(hDlg, 2);
return 0;
}
String = 0;
memset(&v7, 0, 0x1FCu);
GetDlgItemTextW(hDlg, 1001, &String, 510);
v4 = sub_401A40(&String, 0x451E8B10u / dword_437A08);
*(_DWORD *)Caption = 0x793A63D0;
if ( v4 )
{
memset(&v9, 0, 0x1FAu);
*(_DWORD *)Text = 0x518C6CE8;
*(_DWORD *)&Text[2] = 0x529F6210;
memset(&v11, 0, 0x1F6u);
MessageBoxW(hDlg, Text, Caption, 0);
EndDialog(hDlg, 2);
result = 0;
}
else
{
memset(&v9, 0, 0x1FAu);
*(_DWORD *)Text = 0x518C6CE8;
*(_DWORD *)&Text[2] = 0x8D255931;
memset(&v11, 0, 0x1F6u);
MessageBoxW(hDlg, Text, Caption, 0);
result = 0;
}
return result;

很明白,取输入,然后进401A40进行计算,这个计算函数比较简单,主要是算术及异或计算,最后与一定值比较。我不知道别人有没有算,反正我是没有计算的,这个简单得太假,而且当时我猜测可能算不出什么来。在研究这个计算代码的时候,发现401A40下面有函数有明显的字符范围的检查(这个就是常看汇编的好处啊,光盯伪代码肯定发现不了)。这就更肯定了,此处算法为假算法。

再联想到窗体如果不创建,那后面调用的那些函数是干什么的呢,窗体创建了,那串函数就没用了。

先看看控制窗体启动的flag byte_439B51是在哪设置的。只有一个写的地方40145D,在401240函数中。设置过程大概是:先获取本进程快照,然后获取快照中的进程句柄,然后判断此进程的父进程是不是explorer.execmd.exe,如果是并且检查一个堆数据成功则flag为0(检查堆数据这个我一直不理解),否则为1。第一个flag的值也是在此设置的,似乎不影响程序的正常运行,就没有关注。再继续溯源,回到40100041B35A41B28A4181DF,最后来到的函数4181DF是VC入口的___tmainCRTStartup

上面的发现可以推断出两点:一,这应该可以用作反调试,二,此程序有两个进程。

直接运行了下程序,确实有两个进程。我尝试对两个进程进行了附加,一个有窗体,一个没有窗体。没有窗体的可以进行附加,入口为程序壳的入口地址(实际过程中此处还没有发现dump出来的程序不一样,为了保险用的原始程序,包括后面的一些都是用的原始程序);而有窗体的程序不能附加。不能附加说明什么问题:进程已经处于调试状态,调试者当然是父进程。

我又用OD改名和不改名分别跟了下这个过程,因为检查堆数据的问题,flag一直为1。

到这里再思考下,现在不管flag什么条件下为0,有一点可以肯定,正确流程应该是flag先为0,开启另一个进程启动窗体,非窗体进程就是窗体进程的父进程,父进程对子进程进行调试,修改流程。但是由于子进程不能附加,所以是看不了代码的。下一步要做的就是:掌握父进程对子进程的修改或想办法搞到子进程的代码。

考虑到跟踪父进程太累,先使用dump的办法。用petools对刚运行的程序进行dump,没进行任何修复改动,直接进ida,发现本来401740中是这样的

1
2
3
4
String = 0;
memset(&v7, 0, 0x1FCu);
GetDlgItemTextW(hDlg, 1001, &String, 510);
v4 = sub_401A40(&String, 0x451E8B10u / dword_437A08);

后来变成了这样:

1
2
3
sub_418360(&v8, 0, 508);
v745FB940(a2, 1001, &v7, 510, a1, *(_DWORD *)&v7, v9, v10);
v5 = sub_401A40(&v7, 0x451E8B10 / 0u);

dword_437A08已经为0。
还是要看父进程,除0异常应该被调试者接收吧。

还有一个办法,如果父进程对子进程进行了更改,然后并未还原呢?抱着试试看的想法,我随便输入并确定,提示错误,然后再dump。果然不一样了。这里提醒一下,第二次dump出来的文件的PE头的dosheader已经被修改了,需要还原下。下面对比下原始与dump出来的变化情况。

为了方便看,我直接上汇编了,开始是这样的:

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
.text:004017B2 mov eax, 451E8B10h
.text:004017B7 mov ecx, dword_437A08
.text:004017BD div ecx
.text:004017BF mov [ebp+hDlg], eax
.text:004017C2 push [ebp+hDlg]
.text:004017C5 lea eax, [ebp+String]
.text:004017CB push eax
.text:004017CC call sub_401A40
.text:00401A40 push ebp
.text:00401A41 mov ebp, esp
.text:00401A43 sub esp, 0Ch
.text:00401A46 push ebx
.text:00401A47 push esi
.text:00401A48 push edi
.text:00401A49 mov edi, [ebp+arg_0]
.text:00401A4C xor edx, edx
.text:00401A4E xor esi, esi
.text:00401A50 mov [ebp+var_8], edx
.text:00401A53 mov [ebp+var_4], esi
.text:00401A56 xor ecx, ecx
.text:00401A58 lea ebx, [edi+2]
.text:00401A5B jmp short loc_401A60

第一次dump后是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:004017B2 mov eax, 451E8B10h
.text:004017B7 mov ecx, ds:dword_437A08
.text:004017BD div ecx
.text:004017BF mov [ebp+arg_0], eax
.text:004017C2 push [ebp+arg_0]
.text:004017C5 lea eax, [ebp+var_600]
.text:004017CB push eax
.text:004017CC call sub_401A40
.text:00401A40 push ebp
.text:00401A41 mov ebp, esp
.text:00401A43 sub esp, 0Ch
.text:00401A46 push ebx
.text:00401A47 push esi
.text:00401A48 push edi
.text:00401A49 mov edi, [ebp+arg_0]
.text:00401A4C xor edx, edx
.text:00401A4E xor esi, esi
.text:00401A50 mov [ebp+var_8], edx
.text:00401A53 mov [ebp+var_4], esi
.text:00401A56 xor ecx, ecx
.text:00401A58 lea ebx, [edi+2]
.text:00401A5B jmp short loc_401A60

唯一的区别就是dword_437A08变成了0。第二次dump是这样的:

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
.text:004017B2 mov eax, 451E8B10h
.text:004017B7 mov ecx, ds:dword_437A08
.text:004017BD nop
.text:004017BE nop
.text:004017BF mov [ebp+arg_0], eax
.text:004017C2 push [ebp+arg_0]
.text:004017C5 lea eax, [ebp+var_600]
.text:004017CB push eax
.text:004017CC call sub_401A40
.text:00401A40 push ebp
.text:00401A41 mov ebp, esp
.text:00401A43 jmp loc_402303
.text:00402300 push ebp
.text:00402301 mov ebp, esp
.text:00402303
.text:00402303 loc_402303: ; CODE XREF: sub_401A40+3j
.text:00402303 sub esp, 424h
.text:00402309 mov ecx, ds:dword_43AAFC
.text:0040230F mov [ebp+var_80], 0
.text:00402316 mov [ebp+var_34], 0C9DDE6h
.text:0040231D mov [ebp+var_3C], 19170839h
.text:00402324 mov [ebp+var_38], 0E1C01506h
.text:0040232B mov eax, [ecx+3Ch]
.text:0040232E add eax, 78h
.text:00402331 mov [ebp+var_84], 0
.text:0040233B add eax, ecx

这一次变化比较大,除操作已经被nop掉了,401A40调用实际上已经被改成了402300调用。其它地方有没有更改不太清楚,但是更改的可能性比较小,改动开始没有显式调用的函数反倒可能会显露目标,而且这种手法明显只是为了隐藏真实的检验流程,目标已经达到(另外,我当时也简单对比下402300,没看出什么改动)。暂定先尝试手动完成父进程完成的工作,看程序运行情况,及检验流程情况。
直接改成这样:

1
2
3
4
5
6
7
004017BD 90 nop
004017BE 90 nop
004017BF |. 8945 08 mov [arg.1],eax
004017C2 |. FF75 08 push [arg.1]
004017C5 |. 8D85 00FAFFFF lea eax,[local.384]
004017CB |. 50 push eax
004017CC E8 2F0B0000 call 11-ToBeB.00402300

算法

动态前先看下静态的情况,函数过程比较复杂,代码量比较大。我们从输入输出入手,函数参数只有一个输入的字串,返回是True则成功,函数对参数及返回读写的区域在4029D5402B34之间。所以这部分是重点跟的。

动态跟了下这个函数开头,此处利用开始壳读取并修改的文件内容(从这里可以看出,开始从堆中dump出来的程序是不能完成验证的),加载库及其它操作。直接跳过,在下断4029D5

这里开始的算法比较简单。就是用输入前8位作为一个hex数a1,9-10位为一个hex数a2。对一堆区0x5000 byte的数据进行运算,最后与byte_4340B0处的数据比较前0x60个字节。运算部分的代码翻译过来如下:

1
2
3
4
5
6
int j = 0
for (int i = a1; i < a1 +0x5000/4;i++)
{
output[j] = (a2*0x1010101+i)^output[j];
j++;
}

反解,得到前10字节,0x75A29C09,注意,在unhex的函数中均将输入以大写hex进行转换,反解代码如下:

1
2
3
4
5
6
7
8
9
m1 = 0x83F08EA7
m2 = 0x3F0FBA29
c1 = 0x1070EC81
c2 = 0x55530000
d = 0x1010101
for i in range(0x100):
if ((i*d+m1)^c1)&0xffffffff == (((i*d+m2)^c2)&0xffffffff)-1:
print hex(i)
print hex(((i*d+m1)^c1)&0xffffffff)

后面memset两个栈区后,将第11位开始的输入unhex(也要求输入是大写hex)。接着读入!HelloHaniella!常量,并将硬编码的19970907写入栈区,并调用前面刚运算完的堆区,原来上面是为后面解码代码的。

进去看了看,算法比较明显,DES。原因有2:一是19970907应该是作为密钥的,进去后立即被变换成56bit;二是再看下变换表,就是DES的表。

后面还有IP变换表,S-BOX变换表,移位表,P变换表等,我一一比对,没有改动。这里有个细节有注意了下,轮密钥是从后面开始用的,说明这是解密过程。下面的代码就是轮密钥与扩展密文的异或过程,轮密钥从后往前用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
0068101A 8BCA mov ecx,edx
0068101C 8DB424 300C0000 lea esi,dword ptr ss:[esp+0xC30]
00681023 8DBC24 900C0000 lea edi,dword ptr ss:[esp+0xC90]
0068102A F3:A5 rep movs dword ptr es:[edi],dword ptr ds>
0068102C 33F6 xor esi,esi
0068102E 8B7C24 14 mov edi,dword ptr ss:[esp+0x14]
00681032 8BCE mov ecx,esi
00681034 2BCF sub ecx,edi
00681036 8A8C0C 50100000 mov cl,byte ptr ss:[esp+ecx+0x1050]
0068103D 308C34 900C0000 xor byte ptr ss:[esp+esi+0xC90],cl
00681044 46 inc esi
00681045 83FE 30 cmp esi,0x30
00681048 ^ 7C E4 jl short 0068102E

DES加密完成后,与!HelloHaniella!按byte比较,直到遇到0,结束比较,0也进行了比较。

1
2
3
4
5
6
7
8
9
10
11
12
00402AF2 |> /8A10 /mov dl,byte ptr ds:[eax] ;加密串
00402AF4 |. |3A11 |cmp dl,byte ptr ds:[ecx] ;常量
00402AF6 |. |75 30 |jnz short 11-ToBeB.00402B28
00402AF8 |. |84D2 |test dl,dl
00402AFA |. |74 12 |je short 11-ToBeB.00402B0E
00402AFC |. |8A50 01 |mov dl,byte ptr ds:[eax+0x1]
00402AFF |. |3A51 01 |cmp dl,byte ptr ds:[ecx+0x1]
00402B02 |. |75 24 |jnz short 11-ToBeB.00402B28
00402B04 |. |83C0 02 |add eax,0x2
00402B07 |. |83C1 02 |add ecx,0x2
00402B0A |. |84D2 |test dl,dl
00402B0C |.^\75 E4 \jnz short 11-ToBeB.00402AF2

到这里,我直接将检验值进行des加密,结果竟然不对。此处我花了太多时间,重新对比变换表,及算法过程。确实没有发现问题。没办法,我直接将改待解密数据为检验值,将轮密钥前后倒置,最后看了结果与我算的还是不一样。因为此,我花了半天时间。

最后直接一步一步跟,并写了des,打出各个过程中值,进行比对。解密完成也没有发现不对。后面再跟,发现最后结果提取作了手脚,结果数组下标为偶数的进行了取反。我的天,这是玩心理学啊。其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0068248C 8BD6 mov edx,esi
0068248E 81E2 01000080 and edx,0x80000001
00682494 79 05 jns short 0068249B
00682496 4A dec edx
00682497 83CA FE or edx,-0x2
0068249A 42 inc edx
0068249B 75 11 jnz short 006824AE
0068249D 389C34 10020000 cmp byte ptr ss:[esp+esi+0x210],bl
006824A4 0f95c1 setne cl
006824A7 888C34 10020000 mov byte ptr ss:[esp+esi+0x210],cl
006824AE 46 inc esi
006824AF 83FE 40 cmp esi,0x40
006824B2 ^ 7C D8 jl short 0068248C

DES的过程如下,数字为偏移:

1
2
3
4
5
6
7
8
9
10
11
06cd 轮密钥完成
0f6c 扩展表加载完成
0f99 ip置换完成
101a R扩展
103d 扩展输入与轮密钥异或 轮密钥从16组开始用
22e4 s盒加载完成
23aa s盒转换完成
23cc p盒置换完成
2400 R与L异或完成
2413 R L交换
2426 一次round结束

将检验值对应位取反后进行DES加密,得到80217C048420956C15DA309FF2B69170,最终key为75A29C09E180217C048420956C15DA309FF2B69170

最后

得到了正确的key,试了下最开始从壳入口附近dump的出的程序,果然验证不能通过。
另外,由于最后程序没有验证没有固定长度,开始也没有检验长度,所以后面再加des的分组,就会造成多解,加一组,那多解数量也是十分巨大的。

说明:本文首发于看雪论坛。

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 初探
  2. 2. 正确流程寻找
  3. 3. 算法
  4. 4. 最后
,