pk8900 发表于 2018-6-21 17:48

详细分析看雪2016CTF第一题Crack_ME用IDA+X64DBG追码

本帖最后由 pk8900 于 2018-6-25 23:17 编辑

##前言
      很长时间没有发帖子了,这段时间一直在饿补,随着逆向学习深入,发现自己要学的东西太多了,许多东西过去都是只听说过,跟本没学习过,于是看网上的各种教程,并寻找用来练手的东西,发现看雪CTF里有不少CRACKME,于是拿来分析研究。
      这个CRACKME的作者叫[凉飕飕],CTF要求是追码,整个追码过程断续的进行2天时间,然后写的这个帖子,尽量还原原始分析步骤,如果有遗漏和不对的地方,欢迎大家请跟贴讨论。
       此贴并非技术贴,大牛可以一带而过,和我一样的新手也许可以从中学习到一些方法和经验。
###下载地址
      CRACKME原帖及下载地址:https://ctf.pediy.com/game-fight-2.htm
### crack简介
      PEid显示,Microsoft Visual C++ 8 *,图标是个MFC图标,估计是VC++的程序错不了。界面如下:
      
      输入任意注册码,点击OK,有提示信息框:something you lost!,并且每尝试一次,左边还有一个文本框递增次数。
### 使用工具
       IDA7.0,X64DBG,辅助工具:PEid,慧星小助手(查看控件ID,本例中:密码框为1002,次数框为1000)
##摸清流程
       既然程序有提示字符串,那就从字符串下手。X64DBG载入,运行后程序自动退出,有反调试。
       打开IDA进行静态分析,查找字符串,找到结果如下:
      .text:00402423      sub_402120      push    offset Text   ; "something you lost!"
      转到sub_402120调用处,此处IDA代码如下:
            BeginPaint(hWnd, &Paint);      EndPaint(hWnd, &Paint);
      return 0;
    }
    return DefWindowProcW(hWnd, Msg, wParam, lParam);
}
if ( (unsigned __int16)wParam > 0x40Au )
{
    switch ( (unsigned __int16)wParam )
    {
      case 0x40Bu:                              // 注册成功条件:wParam==0x40B
      *(_OWORD *)v20 = xmmword_41DB98;
      v23 = 0;
      v21 = xmmword_41DBA8;
      v22 = xmmword_41DBB8;
      memset(&v24, 0, 0x96u);
      MessageBoxW(hWnd, v20, L"Successed", 0);// 成功位置
      return 0;
      case 0x40Cu:
      sub_402970(lParam, &v19);
      v9 = operator new[](2 * (v19 + 1));
      sub_402870(v9, lParam);
      sub_4029B0(v9);
      sub_402A00(v9);
      memset(v20, 0, 0xC8u);
      sub_402870(v20, v9);
      j_j_j___free_base(v9);
      v10 = 0;
      if...
      if ( sub_402810(lParam, v20) )
          SendMessageW(hWnd, 0x111u, 0x40Au, 0);
      return 0;
      case 0x40Fu:
      MessageBoxW(0, L"something you lost!", L"Failed", 0);// 失败处
      return 0;
    }
    return DefWindowProcW(hWnd, 0x111u, wParam, lParam);
}
if ( (unsigned __int16)wParam == 0x40A )
{
    *(_OWORD *)Text = xmmword_41DB60;
    v26 = xmmword_41DB70;
    v27 = xmmword_41DB80;
    v28 = qword_41DB90;
    memset(&v29, 0, 0x2Cu);
    MessageBoxW(hWnd, Text, L"Failed", 0);      // 失败处
在失败对话框上方,有明显"Successed"成功字样,分析得出成功的条件就是:wParam==0x40B,再往上可以看到:
switch ( Msg )
    {
      case 0x113u:
v13 = operator new(0x14u);
      v40 = 0;
      *(_OWORD *)v39 = 0i64;
      v14 = GetDlgItem(hWnd, 1000);
      GetDlgItemTextW(v14, 1000, v39, 10);
      v19 = -858993460;
      sub_4027C0(v39, (const char *)L"%d", &v19);
      *v13 = v19;
      v13 = hWnd;
      v13 = GetDlgItem(hWnd, 1002);
      v13 = GetDlgItem(hWnd, 1000);
      v13 = v13;
“.............................”
while ( *((_WORD *)&v32 + v15) );
      if ( OpenEventW(0x1F0003u, 0, L"Cracker") )
          CreateThread(0, 0, StartAddress, v13, 0, 0);
      return 0;
“.............................”
return DefWindowProcW(hWnd, Msg, wParam, lParam);
case 0x113u,百度查询,0x113为时间事件,也就是程序里有一个时钟事件,用做检测,事件中包含"Cracker",“Failed”等可疑字串,未发现成功字串,只有一个可疑的CreateThread(0, 0, StartAddress, v13, 0, 0);函数,创建新线程。
(此期间尝试跟踪此时间事件,结果一点进展都没有,关于程序反调试,我采用的附加的方法轻松绕过,反调试部分放在最后分析。)
绕了半天的弯路后,又回到这里,决定从成功注册的条件:wParam==0x40B入手,IDA查的常量:0x40B(IDA操作为查找立即数),果然找到了关键位置,sub_401870,右键对函数重命名为:check_right_401870(命名方便在函数列表中查找,保留地址是方便X64DBG中进行调试定位),在这个子程序中调用了 return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Bu, 0);,这样wParam就被赋值0x40B,就可以出现成功的提示了。(此期间尝试在X64DBG中对基址+1870地址下断,根本断不下来,说明验证过程中没有调用这里)
继续IDA中静态分析,查找check_right_401870函数调用关系,(IDA操作:鼠标定位于check_right_401870函数中,右键“邻近浏览器”),在邻近浏览器图表视图中通过双击父结点,及隐藏非重要节点,得出如下流程图:

成功只有一个路线,IDA的这个功能直的很有用,如果想爆破实现的话,那就可以按图索骥了。
3.##逐段分析
这个过程是漫长的,也可以说是经验积累的过程,大牛通常能一眼看穿的代码,而新手就要推敲半天,我想这就是大牛和新手之间最大的差别吧~~~。
3.1### msgcheck_2120
此函数代码很长,下面仅贴上关键部分(IDA F5 伪代码):
if ( (unsigned __int16)wParam == 0x3EB )    // 点击了OK按钮
    {
      sub_402790(String, 10, (const char *)L"%d", ++dword_420318);
      v4 = GetDlgItem(hWnd, 1000);
      SetWindowTextW(v4, String);
      v5 = (struct_count_array *)operator new(0x14u);
      v40 = 0;
      v6 = v5;
      *(_OWORD *)v39 = '\0';
      v7 = GetDlgItem(hWnd, 1000);
      GetWindowTextW(v7, v39, 10);
      vsscanf_27C0((int)v39, (const char *)L"%d", &v19);
      *(_DWORD *)&v6->Now_count = v19;
      v6->win_handle = (int)hWnd;
      v6->passwd_handle = (int)GetDlgItem(hWnd, 1002);
      v6->count_handle = (int)GetDlgItem(hWnd, 1000);
      v6->field_10 = (int)v6;
      CreateThread(0, 0, (LPTHREAD_START_ROUTINE)check_01_4020E0, v6, 0, 0);// 调用检查函数
      return 0;
    }
msgcheck_402120是MSG处理函数,这里处理所有返回的事件,上面代码为OK按钮事件,用CreateThread函数创建一个新线程,处理按钮事件,参数V6,即上面的V5,通过X64DBG中查看,估计是一个结构体,于是在IDA中自定义了一个结构体:如下
struct_count_array struc ; (sizeof=0x14, mappedto_101)
00000000 Now_count       dw ?
00000002               db ? ; undefined
00000003               db ? ; undefined
00000004 win_handle      dd ?
00000008 count_handle    dd ?
0000000C passwd_handle   dd ?
00000010 field_10      dd ?
00000014 struct_count_array ends
在IDA中将变量V5指定为结构体指针,这样代码会更加清晰,结构体中:Now_count=当前尝试次数 win_handle=主窗体句柄count_handle=显示次数文本框句柄passwd_handle=密码框句柄 field_10=当前结构体指针,通过这结构体可以轻松访问密码框和次数框的文本内容。
结构体大小是20,从v5 = (struct_count_array *)operator new(0x14u);这句中就可以确定。
3.2 ###check_01_20E0
代码比较少,调用了check_02_401CB0,参数共2个,第一个是我们自定义的那个结构体数据,第二个参数是0
int __stdcall check_01_4020E0(struct_count_array *lpMem)
{
check_02_401CB0((int)lpMem, 0);
sub_402CE4(lpMem);
return 0;
}
3.3 ###check_02_401CB0
伪代码如下:
void __fastcall check_02_401CB0(int a1, int a2)
{
struct_count_array *canshu_1; // edi
int v3; // ecx
unsigned int len; // esi
WCHAR *v5; // eax
_WORD *next_passwd; // ebx
clock_t v7; //
WCHAR String; //

canshu_1 = (struct_count_array *)a1;
if ( a1 )
{
    if ( a2 )
    {
      v7 = clock();
      memset(&String, 0, 200u);
      GetWindowTextW((HWND)canshu_1->passwd_handle, &String, 200);
      len = 0;
      v5 = &String;
      if ( String )
      {
      do
      {
          ++v5;
          ++len;
      }
      while ( *v5 );
      }
      next_passwd = operator new[](2 * (len + 1));
      if ( clock() - v7 > 2 )
      exit(0);
      sub_402870((int)next_passwd, &String);    // 修改了PASSWORD
      if ( len >= 7 )
      {
      if ( len <= 7 )
      {
          check_03_401A60((int)canshu_1, next_passwd);// len>=7 并且<=7 ,所以密码长度只能为7位
          return;
      }
      SendMessageW((HWND)canshu_1->win_handle, 0x40Du, 0, 0);
      }
      else
      {
      SendMessageW((HWND)canshu_1->win_handle, 0x40Eu, 0, 0);
      }
      j_j_j___free_base(next_passwd);
      return;
    }
    if ( checkhave_b_401C00((HWND *)a1)         // 检查是否含有‘b’
      && (memset(&String, 0, 0xC8u),
          GetWindowTextW((HWND)canshu_1->passwd_handle, &String, 100),
          checkhave_402A50(v3, (__int16 *)&String, 'p')) )// 检查是否含有 p
    {
      check_02_401CB0((int)canshu_1, 1);
    }
    else
    {
      SendMessageW((HWND)canshu_1->win_handle, 0x111u, 0x40Fu, 0);
    }
}
}
这个函数很有意思,参数a2=0时,检测密码中是否含有字母‘p’和‘b’,如果有则调用自身,传a2参数为1,当a2=1时,检查密码长度是否为7位,通过这两个条件即可进入下一验证环节。
3.4 ###check_03_1A60
这个函数是一个加密函数,check_03_401A60(struct_count_array *a1, _WORD *a2),a2取我们输入的密码,在这个过程中被加密,加密过程也比较简单,只是做了一下异或加密。前半部分分析了半天貌似没有实际做用,估计是作者加的干扰代码。
函数最后调用:return check_right_401870(next_canshu_a1, next_passwd);,将加密后的密码传入下一验证步骤。
3.5 ###check_right_1870
伪代码如下:
BOOL __fastcall check_right_401870(struct_count_array *a1, _WORD *a2)
{
struct_count_array *struct_count_list; // ebx
_WORD *password; // edi
char *char_N; // ecx
signed int v5; // eax
signed int v6; // eax
__int16 *v7; // ecx
unsigned int count; // edx
__int16 *i; // eax
unsigned int v10; // ecx
unsigned int v11; // eax
unsigned int len_pw; // ecx
_WORD *v13; // eax
unsigned int v14; // eax
unsigned int len_pw1; // edx
_WORD *j; // eax
unsigned int v17; // ecx
unsigned int v18; // eax
int n; // esi
__int16 enc_s; // cx
__int64 *v21; // ebx
__int16 *_pswd; // eax
__int16 v23; // dx
__int16 *v24; // ecx
int v25; // eax
int v26; // ecx
__int64 *v27; // eax
_WORD *v28; // esi
unsigned int v29; // ecx
struct_count_array *tmp_arg_1; //
__int16 enc_list; //
char v33; //
__int64 v34; //
__int16 v35; //

struct_count_list = a1;
password = a2;
tmp_arg_1 = a1;
memset(enc_list, 0, 54u);
char_N = &v33;
v5 = '0';
do
{
    *(_WORD *)char_N = v5;
    char_N += 2;
    ++v5;
}
while ( v5 <= '9' );                        // 填充数字 0-9
v6 = 'a';
v7 = enc_list;
do
{
    *v7 = v6;
    ++v7;
    ++v6;
}
while ( v6 <= 'z' );                        // 填充小写字母 a-z
count = 0;
for ( i = enc_list; *i; ++count )             // 计算个数
    ++i;
v10 = 0;
if ( count )
{
    do
    {
      v11 = (unsigned __int16)enc_list;
      if ( v11 >= 'a' && v11 <= 'z' )
      enc_list = v11 - 32;
      ++v10;
    }
    while ( v10 < count );                      // 小字字母转为大写字母
}
len_pw = 0;
v13 = password;
if ( password )
{
    if ( *password )
    {
      do
      {
      ++v13;
      ++len_pw;
      }
      while ( *v13 );                           // count_pw = Password长度
    }
    v14 = 0;
    if ( len_pw )
    {
      do
      {
      if ( v14 >= 2 )
      {
          if ( v14 >= 4 )
            password ^= 'B';
          else
            password ^= 'P';
      }
      else
      {
          password ^= 0xFu;
      }
      ++v14;
      }
      while ( v14 < len_pw );
    }                                           // 还原已经加密过的PASSWORD
    len_pw1 = 0;
    for ( j = password; *j; ++len_pw1 )
      ++j;
    v17 = 0;
    if ( len_pw1 )
    {
      do
      {
      v18 = (unsigned __int16)password;
      if ( v18 >= 'a' && v18 <= 'z' )
          password = v18 - 32;
      ++v17;
      }
      while ( v17 < len_pw1 );                  // password小写转大写
    }
}
n = 0;
v34 = '\0';
v35 = 0;
if ( *password )
{
    enc_s = enc_list;
    v21 = &v34;
    _pswd = password;
    do
    {
      if ( enc_s )
      {
      v23 = *_pswd;
      v24 = enc_list;
      v25 = 0;
      while ( v23 != *v24 )
      {
          v24 = &enc_list[++v25];
          if ( !enc_list )
            goto LABEL_37;
      }
      *(_WORD *)v21 = enc_list;
      v21 = (__int64 *)((char *)v21 + 2);
LABEL_37:
      enc_s = enc_list;
      }
      _pswd = &password[++n];
    }
    while ( password );
    struct_count_list = tmp_arg_1;
}                                             // 取出第一个字母的位置 V34
v26 = 0;
v27 = &v34;
if ( (_WORD)v34 )
{
    do
    {
      v27 = (__int64 *)((char *)v27 + 2);
      ++v26;
    }
    while ( *(_WORD *)v27 );
    if ( v26 == 2 )
    {
      LODWORD(v34) = '5\01';
      HIDWORD(v34) = &unk_420050;               // “15PB”
      v28 = password + 2;
      v35 = 0;
      v29 = 0;
      while ( *((_WORD *)&v34 + v29) == *v28 )
      {
      ++v29;
      ++v28;
      if ( v29 >= 4 )
      {
          if ( !sub_401740(struct_count_list, password) )
            return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Au, 0);// --40B--成功--
          return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Bu, 0);
      }
      }
    }
}
return PostMessageW((HWND)struct_count_list->win_handle, 0x111u, 0x40Au, 0);
}
这一函数很长,实现的功能也很复杂,函数首先构建一个密码表“123456789AB......Z”,实际用到的只有前9个数字字符,然后还原上一步聚中加密的密码,并转为大写,然后对比了我们输入密码的3至6位是否为“15PB”,这样我们就可以确定我们的密码第3-6位正确的应该是:“15pb”
函数另外调用了sub_401740(struct_count_list, password)来进行验证,IDA伪代码如下:
signed int __fastcall sub_401740(_DWORD *a1, _WORD *a2)
{
_WORD *the_password; // ebx
struct_count_array *v3; // edx
__int128 *v4; // ecx
signed int char_1; // eax
int len; // esi
_WORD *v7; // eax
char *new_string; // edi
unsigned __int16 *last_passwd; // eax
int v10; // esi
__int128 *v11; // ecx
__int16 v12; // di
__int16 v13; // ax
__int128 *v14; // ecx
signed int v15; // edx
unsigned __int16 *v17; //
struct_count_array *v18; //
char *v19; //
__int128 charlist_1to9; //
__int64 v21; //
int v22; //
__int16 v23; //

the_password = a2;
v22 = 0;
v3 = (struct_count_array *)a1;
v23 = 0;
v18 = (struct_count_array *)a1;
charlist_1to9 = '\0';
v4 = &charlist_1to9;
char_1 = '1';
v21 = '\0';
do
{
    *(_WORD *)v4 = char_1;
    v4 = (__int128 *)((char *)v4 + 2);
    ++char_1;
}                                             // charlist_1to9="123456789"
while ( char_1 <= '9' );
len = 0;
v7 = the_password;
if ( the_password && *the_password )
{
    do
    {
      ++v7;
      ++len;
    }
    while ( *v7 );                              // 计算密码长度
}
new_string = cat_4028D0((char *)&charlist_1to9, &the_password[*(_DWORD *)&v3->Now_count]);
last_passwd = &the_password;
v19 = new_string;
v10 = 0;
v17 = last_passwd;
if ( (_WORD)charlist_1to9 )
{
    v11 = &charlist_1to9;
    v12 = *last_passwd & 1;
    while ( 1 )
    {
      v13 = v12 + (*(_WORD *)v11 >> 2);
      if ( v13 == '2' )
      break;
      if ( v13 != 'd' )
      {
      v11 = (__int128 *)((char *)&charlist_1to9 + 2 * ++v10);
      if ( *(_WORD *)v11 )
          continue;
      }
      new_string = v19;
      goto LABEL_12;
    }
}
else
{
LABEL_12:
    v14 = &charlist_1to9;
    v15 = '1';
    while ( *(_WORD *)v14 == *(_WORD *)((char *)v14 + (char *)the_password - (char *)&charlist_1to9) )// 第一位 是 1   the_password 是原始密码
    {
      v15 += 6;
      v14 = (__int128 *)((char *)v14 + 2);
      if ( v15 > '9' )
      {
      if ( (unsigned __int16)*the_password + *((unsigned __int16 *)new_string + 9) == 'c'// 第二位即:*((unsigned __int16 *)new_string + 9,所以第

二位是2
          && *v17 == *(_DWORD *)&v18->Now_count + *((unsigned __int16 *)new_string + 6) )// 最后一位:次数+字串里的7,即8
      {
          return 1;
      }
      return 0;
      }
    }
}
return 0;
}
在sub_401740中,验证第一位是否为“1”,函数将密码连接成“123456789”+从调用次数开始截取到最后的密码部分,比如第一次点OK时密码是“ABCDEFG”,连接后即为“123456789BCDEFG”,验证第一位+第二位是否等于‘c’,所以第二位为‘2’,通过取“123456789BCDEFG”中第(7+验证次数)位与我们输入密码的最后一位比较,来验证最后一位,所以第一次验证时最后一位应该是“8”,但第二次验证则无法验证成功第二位的2,因为2被截去了,所以只能在第一次按OK时输入“1215pb8”才能通过验证。
至此整个分析过程完成,并成功找到密码。
截个图:

4. ##反调试分析
关于反调试:程序启动有反调试:2处:
1、比较父进程名称,是否为exeplorer.exe
0109152C | 74 04                        | je crack_.1091532                              |
0109152E | 84 D2                        | test dl, dl                                    |
01091530 | 74 1A                        | je crack_.109154C                              | 跳去结束程序
01091532 | 57                           | push edi                                       |
01091533 | FF 15 60 80 0A 01            | call dword ptr ds:[<&CloseHandle>]             |
01091539 | 8B 4D FC                     | mov ecx, dword ptr ss:                |
0109153C | 33 C0                        | xor eax, eax                                 |
0109153E | 5F                           | pop edi                                        |
0109153F | 5E                           | pop esi                                        |
01091540 | 33 CD                        | xor ecx, ebp                                 |
01091542 | 5B                           | pop ebx                                        |
01091543 | E8 7D 17 00 00               | call crack_.1092CC5                            |
01091548 | 8B E5                        | mov esp, ebp                                 |
0109154A | 5D                           | pop ebp                                        |
0109154B | C3                           | ret                                          |
0109154C | 6A 00                        | push 0x0                                       |
0109154E | E8 5E 36 00 00               | call <crack_.sub_1094BB1>                      |
2、通过以下代码,比较运行中延时时间,大于2毫秒,则认为被调试,程序退出,这块没明白具体原理,下断结果3毫秒,比判断多了1毫秒,不知道是怎么产生的,猜测是程序被调试响应速度慢。
01091575 | 57                           | push edi                                       | 获取时间
01091576 | E8 63 33 00 00               | call <crack_.sub_10948DE>                      |
0109157B | 8B 35 84 81 0A 01            | mov esi, dword ptr ds:[<&LoadStringW>]         |
01091581 | 8B F8                        | mov edi, eax                                 |
01091583 | 6A 64                        | push 0x64                                    |
01091585 | 68 20 04 0B 01               | push crack_.10B0420                            | 10B0420:L"Crack_Me_4"
0109158A | 6A 67                        | push 0x67                                    |
0109158C | 53                           | push ebx                                       |
0109158D | FF D6                        | call esi                                       |
0109158F | 6A 64                        | push 0x64                                    |
01091591 | 68 58 03 0B 01               | push crack_.10B0358                            | 10B0358:L"CRACK_ME_4"
01091596 | 6A 6D                        | push 0x6D                                    |
01091598 | 53                           | push ebx                                       |
01091599 | FF D6                        | call esi                                       |
0109159B | 8B CB                        | mov ecx, ebx                                 |
0109159D | E8 FE 00 00 00               | call <crack_.sub_10916A0>                      |
010915A2 | E8 37 33 00 00               | call <crack_.sub_10948DE>                      |
010915A7 | 2B C7                        | sub eax, edi                                 |
010915A9 | 6A 00                        | push 0x0                                       |
010915AB | 83 F8 02                     | cmp eax, 0x2                                 | 比较时间
010915AE | 0F 8F DA 00 00 00            | jg crack_.109168E                              |
中间也有几处反调试,皆为第2种方法,通过时间差来判断。

附上Crackme程序:
IDA数据文件:

cyhcuichao 发表于 2018-6-22 22:31

学习了楼主

pk8900 发表于 2018-6-22 10:31

hjm666 发表于 2018-6-22 09:25
刚开始学的时候有注意到看雪的这个CM,后来死在了反调试上,现在一直就没有遇到过反调试CM,想问附加程序后 ...

想找反调试,就不能进行附加,载入后查找API调用,进程检测的几个API函数是必须要跟的,比如:<kernel32.CreateToolhelp32Snapshot>快照函数,<kernel32.Process32FirstW>和<kernel32.Process32NextW>遍历函数,<kernel32.ExitProcess>和<kernel32.TerminateProcess>结束进程函数,在这个CM中引用了时间较验,可下断<kernel32.GetSystemTimeAsFileTime>,关于反调试方面太多,大多通过下断<kernel32.ExitProcess>后,进行回溯。本例中最快方法是IDA中查找MFC的Exit调用。

yahanwangluo 发表于 2018-6-21 19:50

其实从技术角度出发来看,这个东西应该这样分析,我不会!{:301_1001:}

测试中…… 发表于 2018-6-21 21:14

学习下,比较忙都没时间搞这些东西......

chenjingyes 发表于 2018-6-22 00:52

谢谢楼主分享   :lol

iteamo 发表于 2018-6-22 08:40

越学习发现不会的越多怎么办

gutfreund 发表于 2018-6-22 09:13

学习下,学海无涯

hjm666 发表于 2018-6-22 09:25

刚开始学的时候有注意到看雪的这个CM,后来死在了反调试上,现在一直就没有遇到过反调试CM,想问附加程序后,是怎么找到检测OD在运行的关键点,并过检测的啊??

连长233 发表于 2018-6-22 10:06

谢谢楼主分享
页: [1] 2 3 4 5 6
查看完整版本: 详细分析看雪2016CTF第一题Crack_ME用IDA+X64DBG追码