buuoj reverse crackMe

crackMe

附件

  • crackme.exe

分析

IDA定位到wmain函数

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
int wmain()
{
FILE *v0; // eax
FILE *v1; // eax
char v3; // [esp+3h] [ebp-405h]
char v4[256]; // [esp+4h] [ebp-404h] BYREF
char Format[256]; // [esp+104h] [ebp-304h] BYREF
char v6[256]; // [esp+204h] [ebp-204h] BYREF
char v7[256]; // [esp+304h] [ebp-104h] BYREF

printf("Come one! Crack Me~~~\n");
memset(v7, 0, sizeof(v7));
memset(v6, 0, sizeof(v6));
while ( 1 )
{
do
{
do
{
printf("user(6-16 letters or numbers):");
scanf("%s", v7);
v0 = (FILE *)sub_CE24BE();
fflush(v0);
}
while ( !checkInput_CE1000(v7) );
printf("password(6-16 letters or numbers):");
scanf("%s", v6);
v1 = (FILE *)sub_CE24BE();
fflush(v1);
}
while ( !checkInput_CE1000(v6) );
sub_CE1090(v7);
memset(Format, 0, sizeof(Format));
memset(v4, 0, sizeof(v4));
v3 = ((int (__cdecl *)(char *, char *))sub_CE11A0)(Format, v4);
if ( sub_CE1830((int)v7, v6) )
{
if ( v3 )
break;
}
printf(v4);
}
printf(Format);
return 0;
}

开始的 while 循环条件中的 checkInput_CE1000 用来判断输入的用户名和密码是否只有字母和数字.

sub_CE1090(v7); 中, v7就是输入的用户名, sub_CE1090用来根据用户名生成一个Sbox.

Sbox: 一个有 256 个 uint8_t 类型元素的数组,常用于加密算法.Sbox的识别方法: 遇到形如for ( i = 0; i < 256; ++i ) sbox[i] = i; 以及 while (i < 256) { /* ...*/ } 就要引起注意. 目前常见加密算法RC4、AES等都有用到Sbox.

根据 sub_CE1090 的代码:

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
_BYTE *__cdecl sub_CE1090(_BYTE *a1)
{
_BYTE *result; // eax
int v2; // [esp+Ch] [ebp-18h]
int v3; // [esp+10h] [ebp-14h]
_BYTE *v4; // [esp+14h] [ebp-10h]
int i; // [esp+18h] [ebp-Ch]
char v7; // [esp+20h] [ebp-4h]
char v8; // [esp+22h] [ebp-2h]
unsigned __int8 v9; // [esp+23h] [ebp-1h]

// 第一个循环////////////////////////////////////////////////////
for ( i = 0; i < 256; ++i )
byte_CF6050[i] = i;
////////////////////////////////////////////////////////////////////
v2 = 0;
v9 = 0;
v3 = 0;
result = a1;
v4 = a1;
do
LOBYTE(result) = *v4;
while ( *v4++ ); // 这个循环没啥用, 忽略

// 第三个循环////////////////////////////////////////////////////
while ( v2 < 256 )
{
v8 = byte_CF6050[v2];
v9 += v8 + a1[v3];
v7 = byte_CF6050[v9];
++v3;
byte_CF6050[v9] = v8;
byte_CF6050[v2] = v7;
result = (_BYTE *)v3;
if ( v3 >= v4 - (a1 + 1) )
v3 = 0;
++v2;
}
////////////////////////////////////////////////////////////////////

return result;
}

可以看到该函数第一个循环就是将Sbox先初始化为 [0, 1, 2, 3, … …, 255] 的数组, 然后接下来的第三个循环可以用代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
uint8_t j = 0;
int k = 0;
for (int i = 0; i < 256; i++)
{
// username -> 输入的用户名
j += sbox[i] + username[k];
uint8_t tmp = sbox[j];
sbox[j] = sbox[i];
sbox[i] = tmp;
k = (k + 1) % username_length;
}
}

接下来的if ( sub_CE1830((int)v7, v6) ) 就是将用户名(v7)和密码(v6)传入参数并检查:

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
bool __cdecl sub_CE1830(const char *username_a1, const char *pwd_a2)
{
int v3; // [esp+18h] [ebp-22Ch]
signed int v4; // [esp+1Ch] [ebp-228h]
signed 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] BYREF
char v14; // [esp+40h] [ebp-204h] BYREF
char v15[255]; // [esp+41h] [ebp-203h] BYREF
char v16[256]; // [esp+140h] [ebp-104h] BYREF

v4 = 0;
v5 = 0;
v11 = 0;
v10 = 0;
memset(v16, 0, sizeof(v16));
v14 = 0;
memset(v15, 0, sizeof(v15));
v9 = 0;
v6 = 0;
v3 = 0;

// 第一个循环
while ( v6 < strlen(pwd_a2) )
{
if ( isdigit(pwd_a2[v6]) )
{
v8 = pwd_a2[v6] - 48;
}
else if ( isxdigit(pwd_a2[v6]) )
{
if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
pwd_a2[v6] = '"';
v8 = (pwd_a2[v6] | 0x20) - 'W';
}
else
{
v8 = ((pwd_a2[v6] | 0x20) - 97) % 6 + 10;
}
__rdtsc();
__rdtsc();
v9 = v8 + 16 * v9;
if ( !((int)(v6 + 1) % 2) )
{
v15[v3++ - 1] = v9;
v9 = 0;
}
++v6;
}
////////////////////////////////////////////////////////////////////////////

// 第二个循环
while ( v5 < 8 )
{
v10 += byte_CF6050[++v11];
v12 = byte_CF6050[v11];
v7 = byte_CF6050[v10];
byte_CF6050[v10] = v12;
byte_CF6050[v11] = v7;
if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 )
v12 = v10 + v11;

// 这里的 v15[v4 - 1] 应该是IDA反编译出错 根据直觉应该是v15[v4]
v16[v5] = byte_CF6050[(unsigned __int8)(v7 + v12)] ^ v15[v4 - 1];

if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
{
v10 = -83;
v11 = 43;
}
inner_encrypt_CE1710((int)v16, username_a1, v5++);
v4 = v5;
if ( v5 >= (unsigned int)(&v15[strlen(&v14)] - v15) )
v4 = 0;
}
////////////////////////////////////////////////////////////////////////////

v13 = 0;
sub_CE1470(v16, &v13);
return v13 == 0xAB94;
}

上面第一个循环将密码(pwd_a2)转换为数字, isdigit 函数检测字符是否为'0' ~ '9', isxdigit函数检测字符是否为'0' ~ '9''a' ~ 'f''A' ~ 'F', 根据字符类型的不同使用不同的计算方式, 比如字符'a' 最后就会被转换为数字0x0A.

使用 Python命令行模拟计算一下:

1
2
3
4
5
6
7
8
Python 3.10.2 (tags/v3.10.2:a58ebcc, Jan 17 2022, 14:12:15) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> hex((ord('a') | 0x20) - 87)
'0xa'
>>> hex((ord('F') | 0x20) - 87)
'0xf'
>>> hex((ord('f') | 0x20) - 87)
'0xf'

if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 ) 以及 __rdtsc() 就是反调试以及花指令, 对于反调试可以用插件(如ScyllaHide) 来绕过并且再静态分析时忽略if块里面的内容.

来到第二个循环, 固定循环8次, 第一行的v10每次取sbox的一个值加到自身, 从第二行开始的 4 行是用来交换byte_CF6050[v10]byte_CF6050[v11] 两个sbox元素.

if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 ) 是根据有PEB来进行反调试, 里面的代码在静态分析时候直接忽略不看.

接下来的 v16[v5] = byte_CF6050[(unsigned __int8)(v7 + v12)] ^ v15[v4 - 1]; 是将sbox的一个元素与我们在第一个循环由输入的密码计算得到的数字进行异或操作并存到v16这个数组里.

if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged ) 处理方法和上面的PEB反调试一样处理.

最后是调用了一个inner_encrypt_CE1710函数, 将v16和username进行加密并将结果存储到v16当前元素中.

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
const char *__cdecl inner_encrypt_CE1710(int a1, const char *a2, signed int a3)
{
const char *result; // eax
signed int v4; // [esp+4h] [ebp-58h]
struct _STARTUPINFOW StartupInfo; // [esp+14h] [ebp-48h] BYREF

memset(&StartupInfo, 0, sizeof(StartupInfo));
StartupInfo.cb = 68;
GetStartupInfoW(&StartupInfo);
v4 = strlen(a2);
if ( StartupInfo.dwX
|| StartupInfo.dwY
|| StartupInfo.dwXCountChars
|| StartupInfo.dwYCountChars
|| StartupInfo.dwFillAttribute
|| StartupInfo.dwXSize
|| StartupInfo.dwYSize )
{
if ( a3 <= v4 )
return &a2[a3];
else
return &a2[v4];
}
else if ( a3 <= v4 )
{
result = (const char *)(a2[a3] ^ *(unsigned __int8 *)(a3 + a1));
*(_BYTE *)(a3 + a1) = (_BYTE)result;
}
else
{
result = (const char *)(a3 + a1);
*(_BYTE *)(a3 + a1) += byte_CF6050[v4 + a3] & a2[v4];
}
return result;
}

第一个if根据STARTUPINFO进行反调试, 里面内容直接不看. else if ( a3 <= v4 ) 部分将用户名索引为a3的ASCII值与v16索引为a3的值进行进行异或, 并存回v16[a3]. 通过动态调试可以发现对于用户名welcomebeijing是不会进入else块的, 直接忽略else块.

第二个循环结束之后, 调用了一个sub_CE1470,将计算结果通过指针返回到 v13 中, 最后return返回 v13 时候等于 0xAB94 .

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
unsigned int *__usercall sub_CE1470@<eax>(int a1@<ebx>, _BYTE *a2, unsigned int *a3)
{
char v5; // al
unsigned int *result; // eax

if ( *a2 != 0x64 )
*a3 ^= 3u;
else
*a3 |= 4u;
if ( a2[1] != 0x62 )
{
*a3 &= 0x61u;
_EAX = (int *)*a3;
}
else
{
_EAX = (int *)a3;
*a3 |= 0x14u;
}
__asm { aam }
if ( a2[2] != 0x61 )
*a3 &= 0xAu;
else
*a3 |= 0x84u;
if ( a2[3] != 0x70 )
*a3 >>= 7;
else
*a3 |= 0x114u;
if ( a2[4] != 0x70 )
*a3 *= 2;
else
*a3 |= 0x380u;
if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
{
if ( a2[5] != 0x66 )
*a3 |= 0x21u;
else
*a3 |= 0x2DCu;
}
if ( a2[5] != 0x73 )
{
v5 = (char)a3;
*a3 ^= 0x1ADu;
}
else
{
*a3 |= 0xA04u;
v5 = (char)a3;
}
_AL = v5 - (~(a1 >> 5) - 1);
__asm { daa }
if ( a2[6] != 0x65 )
*a3 |= 0x4Au;
else
*a3 |= 0x2310u;
if ( a2[7] != 0x63 )
{
*a3 &= 0x3A3u;
return (unsigned int *)*a3;
}
else
{
result = a3;
*a3 |= 0x8A10u;
}
return result;
}

还是反调试if块和花指令一律不看, 根据计算可以观察到可以若最初v13为0, 且v16的元素分别为[0x64, 0x62, 0x61, 0x70, 0x70, 0x73, 0x65, 0x63]就可以让v13最终等于0xAB94.

还是拿Python命令行模拟计算一下:

1
2
>>> hex(((((((0 | 4) | 0x14) | 0x84) | 0x380) | 0xA04 ) | 0x2310 ) | 0x8A10)
'0xab94'

sub_CE1830总结一下就是把password转换成数字, 然后使用username和password对v16进行加密, 目前用户名和v16都已经知道, 那么就可以把v15求出来, 然后把v15转换成字符串即可.

解密脚本

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
#include <iostream>
#include <cstdint>

// username 用户名, username_length 长度
// passwd 密码, 最终计算得到
void encrypt(uint8_t *username, int username_length, uint8_t *passwd, int passwd_size)
{
uint8_t secret[16] =
{0x64, 0x62, 0x61, 0x70, 0x70, 0x73, 0x65, 0x63};
uint8_t ciphertext[256] = {0};
uint8_t sbox[256];

// 对应 sub_CE1090 函数, 对Sbox进行初始化
for (int i = 0; i < 256; i++)
sbox[i] = i;

{
uint8_t j = 0;
int k = 0;
for (int i = 0; i < 256; i++)
{
j += sbox[i] + username[k];
uint8_t tmp = sbox[j];
sbox[j] = sbox[i];
sbox[i] = tmp;
k = (k + 1) % username_length;
}
}
// 对应 sub_CE1090 函数 - 结束

// 对应 sub_CE1830 函数

{ // 分割一下作用域, 防止和 sub_CE1090 的 uint8_t j 发生冲突
uint8_t j = 0, k = 0, p = 0;
for (int i = 0; i < 8; i++)
{
j += sbox[++k];

// 交换 sbox[i] , sbox[j]
uint8_t tmp = sbox[k];
sbox[k] = sbox[j];
sbox[j] = tmp;

passwd[i] = sbox[(uint8_t)(sbox[j] + sbox[k])] ^ secret[i];
passwd[i] ^= username[i];
}
}
// 对应 sub_CE1830 函数 - 结束

// 输出
for (int i = 0; i < 8; i++)
{
printf("%x", passwd[i]);
}
}

int main(int argc, const char *argv[])
{
uint8_t secret[16];

encrypt((uint8_t *)"welcomebeijing", 14, secret, 16);

printf("\n");
return 0;
}

最终结果:

1
2
3
4
5
$ .\crackMe.exe
Come one! Crack Me~~~
user(6-16 letters or numbers):welcomebeijing
password(6-16 letters or numbers):39d09ffa4cfcc4cc
Congratulations:)