首页
社区
课程
招聘
[原创]Buuctf-reverse-crackMe
2022-4-6 11:32 11704

[原创]Buuctf-reverse-crackMe

2022-4-6 11:32
11704

crackMe

ps:学习了一下大家写文章的方式,以后就都这么写了,之前的第一篇就懒得改了,参考了网上师傅的wp

 

题目链接:https://buuoj.cn/challenges#crackMe

题目分析

img

 

通过题目内容可以了解到,用户名是welcomebeijing,密码MD5的小写哈希就是我们的flag。点开程序,可以看到关键字符串,大致的思路就是通过这个字符串可以定位到关键函数,再进行进一步分析。

函数分析

先用查壳软件看一下程序情况

 

img

 

是一个无壳的32位PE文件,用IDA32打开,定位到主函数

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] BYREF
    char v5[255]; // [esp+5h] [ebp-403h] BYREF
    char Format; // [esp+104h] [ebp-304h] BYREF
    char v7[255]; // [esp+105h] [ebp-303h] BYREF
    char v8; // [esp+204h] [ebp-204h] BYREF
    char v9[255]; // [esp+205h] [ebp-203h] BYREF
    char v10; // [esp+304h] [ebp-104h] BYREF
    char v11[255]; // [esp+305h] [ebp-103h] BYREF
 
    printf("Come one! Crack Me~~~\n");
    v10 = 0;
    memset(v11, 0, sizeof(v11));
    v8 = 0;
    memset(v9, 0, sizeof(v9));
    while ( 1 )
    {
        do
        {
            do
            {
                printf("user(6-16 letters or numbers):");
                scanf("%s", &v10);           #接收user
                    v0 = (FILE *)sub_4024BE();
                fflush(v0);
            }
            while ( !(unsigned __int8)sub_401000(&v10) );
            printf("password(6-16 letters or numbers):");
            scanf("%s", &v8);         #接收password
                v1 = (FILE *)sub_4024BE();
            fflush(v1);       #这几个并没有对接收的字符串做什么操作,就不管了
            }
        while ( !(unsigned __int8)sub_401000(&v8) ); #检查是不是字母或者数字
            sub_401090(&v10);
        Format = 0;
        memset(v7, 0, sizeof(v7));
        v4 = 0;
        memset(v5, 0, sizeof(v5));
        v3 = ((int (__cdecl *)(char *, char *))loc_4011A0)(&Format, &v4); #这个应该是判断后输出再试一次的字符串
            if ( (unsigned __int8)sub_401830(&v10, &v8) ) #关键的验证函数
            {
                if ( v3 )
                    break;
            }
        printf(&v4);
    }
    printf(&Format);
    return 0;
}

对于一些不是很重要的函数的作用我都直接在上面的函数注明了,比较重要的,我会在下文着重分析。

sub_401090函数

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
_BYTE *__cdecl sub_401090(_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_416050[i] = i;
  v2 = 0;
  v9 = 0;
  v3 = 0;
  result = a1;
  v4 = a1;
  do
    LOBYTE(result) = *v4;
  while ( *v4++ );
  while ( v2 < 256 )
  {
    v8 = byte_416050[v2];
    v9 += v8 + a1[v3];
    v7 = byte_416050[v9];
    ++v3;
    byte_416050[v9] = v8;
    byte_416050[v2] = v7;
    result = (_BYTE *)v3;
    if ( v3 >= v4 - (a1 + 1) )
      v3 = 0;
    ++v2;
  }
  return result;
}

这个函数就是对user进行了一系列操作,最后把值赋给了byte_416050这个数组,我看着很像RC4加密,后面可以直接动调出来

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
78
79
bool __cdecl sub_401830(int a1, const char *a2)
{
    int v3; // [esp+18h] [ebp-22Ch]
    int v4; // [esp+1Ch] [ebp-228h]
    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; // [esp+140h] [ebp-104h] BYREF
    char v17[255]; // [esp+141h] [ebp-103h] BYREF
 
    v4 = 0;
    v5 = 0;
    v11 = 0;
    v10 = 0;
    v16 = 0;
    memset(v17, 0, sizeof(v17));
    v14 = 0;
    memset(v15, 0, sizeof(v15));
    v9 = 0;
    v6 = 0;
    v3 = 0;
    while ( v6 < strlen(a2) )
    {
        if ( isdigit(a2[v6]) )
        {
            v8 = a2[v6] - 48#这里将输入的字符0-9转化成十进制的0-9,就是转换一下类型
            }
        else if ( isxdigit(a2[v6]) )
        {
            if ( *((_DWORD *)NtCurrentPeb()->SubSystemData + 3) != 2 )#反调试的
                a2[v6] = 34;
            v8 = (a2[v6] | 0x20) - 87#意思是减87加32,也就是将字母,例如a转化成16进制数a(就是10)
            }
        else
        {
            v8 = ((a2[v6] | 0x20) - 97) % 6 + 10; #将其他的字符转化到十六进制数a-f之间(10-16)
            }
        __rdtsc();
        __rdtsc();
        v9 = v8 + 16 * v9;
        if ( !((int)(v6 + 1) % 2) ) #将输入的密码两位一组,存放在v14数组里面
        {
            *(&v14 + v3++) = v9;
            v9 = 0;
        }
        ++v6;
    }
    while ( v5 < 8 )
    {
        v10 += byte_416050[++v11];
        v12 = byte_416050[v11];
        v7 = byte_416050[v10];
        byte_416050[v10] = v12;
        byte_416050[v11] = v7;
        if ( ((int)NtCurrentPeb()->UnicodeCaseTableData & 0x70) != 0 ) #反调试
            v12 = v10 + v11;
        *(&v16 + v5) = byte_416050[(unsigned __int8)(v7 + v12)] ^ *(&v14 + v4); #关键的异或操作
            if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )#反调试
            {
                v10 = -83;
                v11 = 43;
            }
        sub_401710(&v16, a1, v5++);
        v4 = v5;
        if ( v5 >= (unsigned int)(&v14 + strlen(&v14) + 1 - v15) )
            v4 = 0;
    }
    v13 = 0;
    sub_401470(&v16, &v13);
    return v13 == 0xAB94;
}

一下子就能看见有好几个反调试的语句,第一个while循环里的意思就是将输入的密码,例如123456,按0x12,0x34,0x56这样两位切割,然后放在v14数组里面。接下来重点就是第二个循环,我们先来看这个sub_401710函数

sub_401710函数

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
int __cdecl sub_401710(int a1, const char *a2, signed int a3)
{
  int 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 )
      result = (int)&a2[a3];
    else
      result = (int)&a2[v4];
  }
  else if ( a3 <= v4 ) #a3是肯定小于v4的,所以只会执行这条语句
  {
    result = a2[a3] ^ *(unsigned __int8 *)(a3 + a1);
    *(_BYTE *)(a3 + a1) = result;
  }
  else
  {
    result = a3 + a1;
    *(_BYTE *)(a3 + a1) += byte_416050[v4 + a3] & a2[v4];
  }
  return result;
}

在上面这个函数中 v4是user用户名的长度,a3是两位一组后的密码分组个数计数,v4是永远大于a3的。else if里面的操作就是把V6和用户名异或再把值存放在v16里,为了区别,这里将新的v16记作v16'。然后就是sub_401470,我们通过return语句可以推出,v13 == 0xAB94,通过这个我们就可以推出v16'的值。

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
_DWORD *__usercall sub_401470@<eax>(int a1@<ebx>, _BYTE *a2, _DWORD *a3)
{
  _DWORD *_EAX; // eax
  char v5; // al
  char _AL; // al
  _DWORD *result; // eax
 
  if ( *a2 != 'd' )
    *a3 ^= 3u;
  else
    *a3 |= 4u;
  if ( a2[1] != 'b' )
  {
    *a3 &= 0x61u;
    _EAX = (_DWORD *)*a3;
  }
  else
  {
    _EAX = a3;
    *a3 |= 0x14u;
  }
  __asm { aam }
  if ( a2[2] != 'a' )
    *a3 &= 0xAu;
  else
    *a3 |= 0x84u;
  if ( a2[3] != 'p' )
    *a3 >>= 7;
  else
    *a3 |= 0x114u;
  if ( a2[4] != 'p' )
    *a3 *= 2;
  else
    *a3 |= 0x380u;
  if ( *((_DWORD *)NtCurrentPeb()->SubSystemData + 3) != 2 )
  {
    if ( a2[5] != 'f' )
      *a3 |= 0x21u;
    else
      *a3 |= 0x2DCu;
  }
  if ( a2[5] != 's' )
  {
    v5 = (char)a3;
    *a3 ^= 0x1ADu;
  }
  else
  {
    *a3 |= 0xA04u;
    v5 = (char)a3;
  }
  _AL = v5 - (~(a1 >> 5) - 1);
  __asm { daa }
  if ( a2[6] != 'e' )
    *a3 |= 0x4Au;
  else
    *a3 |= 0x2310u;
  if ( a2[7] != 'c' )
  {
    *a3 &= 0x3A3u;
    result = (_DWORD *)*a3;
  }
  else
  {
    result = a3;
    *a3 |= 0x8A10u;
  }
  return result;
}

这里面的a2就是v16',只要v16'的每一位值能等于这里面if判断语句的每一个值就会输出v13 == 0xAB94,所以v16'=dbappfsec。然后我们就可以通过v16'和用户名异或得出v16,再把v16和byte_416050数组异或就可以得出我们要的两位一组分割后的密码,最后拼起来就行。现在我们唯一缺的就是byte_416050数组,这里可以通过动态调试得出

byte_416050数组

动态调试之前需要先将前面的反调试语句处理一下,将下图处的jz改为jmp就行了

 

img

 

然后我们再去看一下关键位置的汇编代码

 

img

 

img

 

可以看见我们只需要观察ecx的值就可以得出byte_416050数组的值,动调得出值为0x2A, 0xD7, 0x92, 0xE9, 0x53, 0xE2, 0xC4, 0xCD。ps:这里动调的值,不知道是程序原因还是什么原因,动调出来的值跟网上动调出来的值不一样,最后搬的网上师傅动调出来的值,有师傅知道的可以在评论区指点一手。

exp

1
2
3
4
5
6
7
8
9
10
11
12
v16_ = "dbappsec"
user = "welcomebeijing"
v16 = []
for i in range(8):
    a = ord(v16_[i]) ^ ord(user[i])
    v16.append(a)
password = ""
byte_416050 = [0x2a, 0xd7, 0x92, 0xe9, 0x53, 0xe2, 0xc4, 0xcd]
for i in range(8):
    password += hex(byte_416050[i]^v16[i])[2:]
print(passw0rd)
#39d09ffa4cfcc4cc

奇怪的是这个结果程序是可以通过的,但是提交到Buuctf没通过,网上的一些wp的结果提交到Buuctf是可以的,但是通不过程序的检验,不知道是不是Buuctf的flag错了。网上其他wp的结果是4eb5f3992391a1ae,大家可以自己试一下。

 

img


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

上传的附件:
收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回