BUUCTF RE crackMe WP 详细解析

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

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

在这里插入图片描述
首先,国际惯例,查壳

在这里插入图片描述
没壳,拖进IDA里通过String定位到关键代码:

首先是主函数代码:

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是我们重点分析的目标。

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函数
_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]

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

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章或许能解答我们的一些疑惑(侵删)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
我们需要重点关注的就是最后一页的提示部分,不难看出,我们题目中出现的几个反调试手段

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

其实就是寻找PEB结构体中的特定字段来判断是否处于被调试的状态,其中:
if ( *(_DWORD *)(__readfsdword(0x30u) + 2) & 0xFF )

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




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

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



最后这个:

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没能识别出来时我们也能有自己正确的判断。

猜你喜欢

转载自blog.csdn.net/a709046532/article/details/108323936