BUUCTF RE crackMe WP 详细解析

本文最后更新于:2022年4月13日 下午

前前后后参考了几篇博文,感觉自己有些值得分享的东西,就有了这篇文章

题目地址:
https://buuoj.cn/challenges#crackMe

在这里插入图片描述

首先,国际惯例,查壳

在这里插入图片描述

没壳,拖进IDA里通过String定位到关键代码:

首先是主函数代码:

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
int wmain()
{
FILE *v0; // eax
FILE *v1; // eax
char v3; // [esp+3h] [ebp-405h]
char v4; // [esp+4h] [ebp-404h]
char v5; // [esp+5h] [ebp-403h]
char v6; // [esp+104h] [ebp-304h]
char v7; // [esp+105h] [ebp-303h]
char v8; // [esp+204h] [ebp-204h]
char v9; // [esp+205h] [ebp-203h]
char v10; // [esp+304h] [ebp-104h]
char v11; // [esp+305h] [ebp-103h]

printf("Come one! Crack Me~~~\n");
v10 = 0;
memset(&v11, 0, 0xFFu);
v8 = 0;
memset(&v9, 0, 0xFFu);
while ( 1 )
{
do
{
do
{
printf("user(6-16 letters or numbers):");
scanf("%s", &v10);
v0 = (FILE *)sub_4024BE();
fflush(v0);
}
while ( !(unsigned __int8)sub_401000(&v10) );
printf("password(6-16 letters or numbers):");
scanf("%s", &v8);
v1 = (FILE *)sub_4024BE();
fflush(v1);
}
while ( !(unsigned __int8)sub_401000(&v8) );
sub_401090(&v10);
v6 = 0;
memset(&v7, 0, 0xFFu);
v4 = 0;
memset(&v5, 0, 0xFFu);
v3 = ((int (__cdecl *)(char *, char *))loc_4011A0)(&v6, &v4);
if ( sub_401830((int)&v10, &v8) )
{
if ( v3 )
break;
}
printf(&v4);
}
printf(&v6);
return 0;
}

不难看出,程序中存在一个死循环,而需要让代码中的死循环跳出,那么sub_401830和loc_4011A0需要成立的,但是loc_4011A0这个函数的参数都是前面已经确定好的且不可控为0,所以这个函数是不需要去分析的。故sub_401830是我们重点分析的目标。

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
bool __cdecl sub_401830(int a1, const char *a2)
{
int v3; // [esp+18h] [ebp-22Ch]
unsigned int v4; // [esp+1Ch] [ebp-228h]
unsigned int v5; // [esp+28h] [ebp-21Ch]
unsigned int v6; // [esp+30h] [ebp-214h]
char v7; // [esp+36h] [ebp-20Eh]
char v8; // [esp+37h] [ebp-20Dh]
char v9; // [esp+38h] [ebp-20Ch]
unsigned __int8 v10; // [esp+39h] [ebp-20Bh]
unsigned __int8 v11; // [esp+3Ah] [ebp-20Ah]
char v12; // [esp+3Bh] [ebp-209h]
int v13; // [esp+3Ch] [ebp-208h]
char v14; // [esp+40h] [ebp-204h]
char v15; // [esp+41h] [ebp-203h]
char v16; // [esp+140h] [ebp-104h]
char v17; // [esp+141h] [ebp-103h]

v4 = 0;
v5 = 0;
v11 = 0;
v10 = 0;
v16 = 0;
memset(&v17, 0, 0xFFu);
v14 = 0;
memset(&v15, 0, 0xFFu);
v9 = 0;
v6 = 0;
v3 = 0;
while ( v6 < strlen(a2) )
{
if ( isdigit(a2[v6]) )
{
v8 = a2[v6] - 48;
}
else if ( isxdigit(a2[v6]) )
{
if ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30u) + 24) + 12) != 2 )
a2[v6] = 34;
v8 = (a2[v6] | 0x20) - 87;
}
else
{
v8 = ((a2[v6] | 0x20) - 97) % 6 + 10;
}
v9 = v8 + 16 * v9;
if ( !((signed int)(v6 + 1) % 2) )
{
*(&v14 + v3++) = v9;
v9 = 0;
}
++v6;
}
while ( (signed int)v5 < 8 )
{
v10 += byte_416050[++v11];
v12 = byte_416050[v11];
v7 = byte_416050[v10];
byte_416050[v10] = v12;
byte_416050[v11] = v7;
if ( *(_DWORD *)(__readfsdword(0x30u) + 104) & 0x70 )
v12 = v10 + v11;
*(&v16 + v5) = byte_416050[(unsigned __int8)(v7 + v12)] ^ *(&v14 + v4);
if ( *(_DWORD *)(__readfsdword(0x30u) + 2) & 0xFF )
{
v10 = -83;
v11 = 43;
}
sub_401710(&v16, a1, v5++);
v4 = v5;
if ( v5 >= &v14 + strlen(&v14) + 1 - &v15 )
v4 = 0;
}
v13 = 0;
sub_401470(&v16, &v13);
return v13 == 43924;
}

首先,从最后一行我们可以看出,v13需要等于43924,所以打开sub_401470函数

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
_DWORD *__usercall sub_401470@<eax>(int a1@<ebx>, _BYTE *a2, _DWORD *a3)
{
int v3; // ST28_4
int v4; // ecx
int v6; // edx
int v8; // ST20_4
int v9; // eax
int v10; // edi
int v11; // ST1C_4
int v12; // edx
char v13; // di
int v14; // ST18_4
int v15; // eax
int v16; // ST14_4
int v17; // edx
char v18; // al
int v19; // ST10_4
int v20; // ecx
int v23; // ST0C_4
int v24; // eax
_DWORD *result; // eax
int v26; // edx

if ( *a2 == 100 )
{
*a3 |= 4u;
v4 = *a3;
}
else
{
*a3 ^= 3u;
}
v3 = *a3;
if ( a2[1] == 98 )
{
_EAX = a3;
*a3 |= 0x14u;
v6 = *a3;
}
else
{
*a3 &= 0x61u;
_EAX = (_DWORD *)*a3;
}
__asm { aam }
if ( a2[2] == 97 )
{
*a3 |= 0x84u;
v9 = *a3;
}
else
{
*a3 &= 0xAu;
}
v8 = *a3;
v10 = ~(a1 >> -91);
if ( a2[3] == 112 )
{
*a3 |= 0x114u;
v12 = *a3;
}
else
{
*a3 >>= 7;
}
v11 = *a3;
v13 = v10 - 1;
if ( a2[4] == 112 )
{
*a3 |= 0x380u;
v15 = *a3;
}
else
{
*a3 *= 2;
}
v14 = *a3;
if ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30u) + 24) + 12) != 2 )
{
if ( a2[5] == 102 )
{
*a3 |= 0x2DCu;
v17 = *a3;
}
else
{
*a3 |= 0x21u;
}
v16 = *a3;
}
if ( a2[5] == 115 )
{
*a3 |= 0xA04u;
v18 = (char)a3;
v20 = *a3;
}
else
{
v18 = (char)a3;
*a3 ^= 0x1ADu;
}
v19 = *a3;
_AL = v18 - v13;
__asm { daa }
if ( a2[6] == 101 )
{
*a3 |= 0x2310u;
v24 = *a3;
}
else
{
*a3 |= 0x4Au;
}
v23 = *a3;
if ( a2[7] == 99 )
{
result = a3;
*a3 |= 0x8A10u;
v26 = *a3;
}
else
{
*a3 &= 0x3A3u;
result = (_DWORD *)*a3;
}
return result;
}

可以知道我们需要a2满足所有的if,v13此时就可以等于43924

也就是v16需要是这样一个BYTE数组:
[0x64,0x62,0x61,0x70,0x70,0x73,0x65,0x63],即ddappsec
v16的值知道了,我们还需要知道这个值是怎么来的

我们可以从第二部分代码第64行得知,byte_416050是通过与变换后的密码异或得到v16。且我们现在已经知道账号为welcomebeijing,所求的是账号的密码。因此我们需要通过动态调试,获取byte_416050的值

在这里插入图片描述

在x32dbg中将汇编断点打在xor上,观察寄存器中的内容,循环看8次就得到我们要的内容了
记录如下:
[0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd]

接下来只需要编写脚本解密即可

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

key = [0x2a, 0xd7, 0x92, 0xe9, 0x53, 0xe2, 0xc4, 0xcd]
text = "dbappsec"

flag = []

for i in range(len(text)):
flag.append(hex(ord(text[i])^key[i]).replace("0x",""))

final_flag=''.join(each for each in flag)
md = hashlib.md5(final_flag.encode('utf-8')).hexdigest()
print(md)

以上就是和师傅们的WP大同小异的部分。我想重点说明的是反调试部分,也就是那些莫名其妙的__readfsdword。我所看的WP中都没能解答我对这些东西的疑惑,只能自己去找了。

《逆向工程核心原理》第51章或许能解答我们的一些疑惑(侵删)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

我们需要重点关注的就是最后一页的提示部分,不难看出,我们题目中出现的几个反调试手段

1
2
3
if ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30u) + 24) + 12) != 2 )
if ( *(_DWORD *)(__readfsdword(0x30u) + 104) & 0x70 )
if ( *(_DWORD *)(__readfsdword(0x30u) + 2) & 0xFF )

其实就是寻找PEB结构体中的特定字段来判断是否处于被调试的状态,其中:

1
if ( *(_DWORD *)(__readfsdword(0x30u) + 2) & 0xFF )

从书中我们可以看出就是我们的字段BeingDebugged,我们的IsDebuggerPresent最后寻找的东西其实就是这个字段

1
if ( *(_DWORD *)(__readfsdword(0x30u) + 104) & 0x70 )

这个其实从PEB的结构中我们也不难看出也是反调试中经常出现的字段NtGlobalFlag

最后这个:

1
if ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30u) + 24) + 12) != 2 )

一步步跟进,首先__readfsdword(0x30u) + 24),这玩意也是经常用在反调试中的字段ProcessHeap,它是一个结构体,那偏移量为12是什么呢?我们再补一张图:

在这里插入图片描述

可以看出是字段Flags,如同书中所说,进程处于被调试状态时,它被设置成了特定的值(从我们的题目中我们可以得出这个值在正常情况下应该是2)

至此,我们大体上弄明白了题目中出现的反调试手段。

识别出来了我们需要进行反反调试,绕过反调试代码执行函数的正常逻辑。

这里给出几个比较常见的手段:

其一是动调时手动修改代码,比如汇编下把jz改成jmp/jnz,我改成了jnz,机器码是74改成75

另外一个就是使用插件ScyllaHide了,这里我们只需要使用Basic模块即可(准确来说Hide from PEB就行)

在这里插入图片描述

总结一下,题目本身不难。有坑的地方即是那几个反调试的部分会修改那8个值扰乱分析。当识别出并过掉后题目基本就没问题了。

另外一点就是当fs和0x30同时出现在我们的代码中时需要额外注意。这表明PEB要被访问了,出题人要开始整活了。

PEB结构体是反调试手段中最基础也是经常会用到的技术。我们不能局限于只是能识别一些API如IsDebuggerPresent,而是同时要对背后的底层有自己的认识,这样在IDA没能识别出来时我们也能有自己正确的判断。


BUUCTF RE crackMe WP 详细解析
https://m0ck1ng-b1rd.github.io/2020/08/02/writeup/BUUCTF RE crackMe WP 详细解析/
作者
何语灵
发布于
2020年8月2日
许可协议