看雪CTF2016CrackMe攻防大赛——第一题

Posted roachs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了看雪CTF2016CrackMe攻防大赛——第一题相关的知识,希望对你有一定的参考价值。

前言

暑假来了,不知道做些什么好,就拿看雪CTF的题来练习练习,学习下大佬们的操作。这是2016年CrackMe攻防赛的第一题,我就被难到了。本来都已经放弃了,但是幸得大佬分享,故跟随大佬的步伐粗略分析了下。

准备

  • 系统:Windows 7 x64 ultimate

  • 工具:IDA pro v7.0(最好用7.0,用6.8会有些问题)

分析

首先运行Crack_Me。

技术分享图片

有一个密码输入框,一个OK按钮和一个计数框。

随便输入密码,获得失败提示,提示失败。

技术分享图片

载入OD,先运行一下,可是却发现程序直接终止了,看来是有反调试了。

我不会过反调试,所以采用了附加的方式,想着绕过检测,但是它应该有基于延迟时间的检测,所以最终也没动态调试成功。

后来看了大佬的文章,采用了IDA静态分析的办法。

载入IDA中,搜索那个失败提示字符串,快捷键是Alt + T

技术分享图片

发现失败提示的弹窗调用。

.text:0040241C loc_40241C:                             ; CODE XREF: sub_402120+2D9↑j
.text:0040241C                 push    0               ; uType
.text:0040241E                 push    offset Caption  ; "Failed"
.text:00402423                 push    offset Text     ; "something you lost!"
.text:00402428                 push    0               ; hWnd
.text:0040242A                 call    ds:MessageBoxW
.text:00402430                 jmp     loc_402775

向下翻,可以看到Successed字符串,那么这里应该就是调用成功弹窗的代码了。

F5查看此处伪代码。

        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:
        *(_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 ( v20[0] )
        {
          v11 = 0;
          v12 = v20;
          do
          {
            ++v10;
            *v12 ^= *(_WORD *)(v11 + lParam);

可以发现是通过一个标志来判断调用不同的弹窗的,看来是通过传递消息来完成的,而成功的消息是0x40B

上下翻看,并没有发现检测函数调用,说明这个函数就只是一个消息框函数,先记住该函数是sub_2120

既然是传递消息,那么就需要找到消息传递的地方,搜索立即数0x40B,快捷键是Alt + I

技术分享图片

有一个压栈操作,有可能就是传参操作。双击点开发现是利用PostMessageW传递消息的。

  v26 = 0;
  v27 = &v34;
  if ( (_WORD)v34 )
  {
    do
    {
      v27 = (__int64 *)((char *)v27 + 2);
      ++v26;
    }
    while ( *(_WORD *)v27 );
    if ( v26 == 2 )
    {
      LODWORD(v34) = ‘51‘;
      HIDWORD(v34) = &unk_420050;
      v28 = v3 + 2;
      v35 = 0;
      v29 = 0;
      while ( *((_WORD *)&v34 + v29) == *v28 )
      {
        ++v29;
        ++v28;
        if ( v29 >= 4 )
        {
          if ( !sub_401740(v2, v3) )
            return PostMessageW(*(HWND *)(v2 + 4), 0x111u, 0x40Au, 0);
          return PostMessageW(*(HWND *)(v2 + 4), 0x111u, 0x40Bu, 0);
        }
      }
    }
  }

那么该函数就是一个检测函数,如果检测通过就传递成功的消息。

该函数是sub_401870,那么我们就找一下有哪些函数调用了这里。

切到反汇编窗口,在sub_401870函数中右击鼠标,选择Proximity browser,可以看到函数调用图。

我们向上双击点开父节点,最后可以得到该调用图。

技术分享图片

简单分析可以发现只有一条路可以到达最后的消息传递函数,也就是只有一条路可以最后调用成功,那么我们就逐步分析。

sub_402120

查看函数伪代码。

    if ( (unsigned __int16)wParam == 0x3EB )
    {
      sub_402790(String, 10, (const char *)L"%d", ++dword_420318);
      v4 = GetDlgItem(hWnd, 0x3E8);
      SetWindowTextW(v4, String);
      v5 = operator new(0x14u);
      v40 = 0;
      v6 = v5;
      *(_OWORD *)v39 = 0i64;
      v7 = GetDlgItem(hWnd, 0x3E8);
      GetWindowTextW(v7, v39, 10);
      sub_4027C0(v39, (const char *)L"%d", &v19);
      *v6 = v19;
      v6[1] = hWnd;
      v6[3] = GetDlgItem(hWnd, 0x3EA);
      v6[2] = GetDlgItem(hWnd, 0x3E8);
      v6[4] = v6;
      CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_4020E0, v6, 0, 0);
      return 0;
    }

该函数通过创建新线程的方法调用了sub_4020E0函数,而这一块函数正是OK按钮的事件处理函数。因为这里的wParam == 0x3EB,而0x3EB是OK按钮的消息。

CreateWindowExW(0, L"button", L"OK", 0x50000000u, 160, 60, 250, 50, hWnd, (HMENU)0x3EB, hInstance, 0);

CreateThread函数以sub_4020E0函数为线程入口,而v6则作为参数传递。从v6的赋值可以猜测v6是一个结构体,而v6 = v5,那么v5就是一个结构体。这里创建一个结构体赋给v5。

00000000 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 Now_struct      dd ?
00000014 struct_count_array ends

这样的话看起来就很有感觉了。

sub_4020E0

该函数的代码很少,主要是调用了两个函数。

int __stdcall sub_4020E0(struct_count_array *lpMem)
{
  sub_401CB0((int)lpMem, 0);
  sub_402CE4(lpMem);
  return 0;
}

将结构体和一个0传给sub_401CB0。

sub_401CB0

查看伪代码。

void __fastcall sub_401CB0(struct_count_array *a1, int a2)
{
  struct_count_array *v2; // edi
  unsigned int v3; // esi
  WCHAR *v4; // eax
  void *v5; // ebx
  clock_t v6; // [esp+4h] [ebp-D0h]
  WCHAR String; // [esp+8h] [ebp-CCh]

  v2 = a1;
  if ( a1 )
  {
    if ( a2 )
    {
      v6 = clock();
      memset(&String, 0, 0xC8u);
      GetWindowTextW((HWND)v2->passwd_handle, &String, 200);
      v3 = 0;
      v4 = &String;
      if ( String )
      {
        do
        {
          ++v4;
          ++v3;
        }
        while ( *v4 );  //计算输入的密码长度
      }
      v5 = operator new[](2 * (v3 + 1));
      if ( clock() - v6 > 2 )
        exit(0);
      sub_402870(v5, &String);  //将密码改为WORD型保存
      if ( v3 >= 7 )
      {
        if ( v3 <= 7 )  //判断是否为7
        {
          sub_401A60(v2, v5);
          return;
        }
        SendMessageW((HWND)v2->win_handle, 0x40Du, 0, 0);
      }
      else
      {
        SendMessageW((HWND)v2->win_handle, 0x40Eu, 0, 0);
      }
      j_j_j___free_base(v5);
      return;
    }
    if ( sub_401C00((HWND *)a1)     //检测‘b‘
      && (memset(&String, 0, 0xC8u), GetWindowTextW((HWND)v2->passwd_handle, &String, 100), sub_402A50(‘p‘)) ) //检测‘p‘
    {
      sub_401CB0(v2, 1);
    }
    else
    {
      SendMessageW((HWND)v2->win_handle, 0x111u, 0x40Fu, 0);
    }
  }
}

函数有两块部分,根据传入a2值的不同选择不同的部分。当a2 == 0时,先检测输入的字符串中是否有b和p。然后再次调用自己,此时传入的a2值为1。

a2 == 1时,检测输入的密码长度是否为7,若为7,则调用sub_401A60,并传递结构体与修改后的密码。

sub_401A60

查看伪代码。

BOOL __fastcall sub_401A60(struct_count_array *a1, _WORD *a2)
{
  struct_count_array *v2; // edi
  _WORD *v3; // ebx
  unsigned int v4; // ecx
  int *i; // eax
  unsigned __int8 v6; // cf
  unsigned int v7; // ecx
  int *v8; // edi
  int j; // ecx
  unsigned int v10; // ecx
  int *k; // eax
  unsigned int v12; // ecx
  int *v13; // edi
  int l; // ecx
  int v15; // ecx
  _WORD *v16; // eax
  _WORD *v17; // esi
  int v18; // eax
  __int16 v19; // cx
  int v20; // edx
  unsigned int v21; // ecx
  _WORD *v22; // eax
  unsigned int v23; // eax
  struct_count_array *v25; // [esp+10h] [ebp-198h]
  int v26[50]; // [esp+14h] [ebp-194h]
  int lParam[50]; // [esp+DCh] [ebp-CCh]

  v2 = a1;
  v3 = a2;
  v25 = a1;
  memset(lParam, 0, 0xC8u);
  memset(v26, 0, 0xC8u);
  SendMessageW((HWND)v2->passwd_handle, 0xDu, 0xDu, (LPARAM)lParam);
  SendMessageW((HWND)v2->win_handle, 0x111u, 0x40Cu, (LPARAM)v26);
  v4 = 0;
  for ( i = lParam; *(_WORD *)i; ++v4 )
    i = (int *)((char *)i + 2);
  if ( v4 )
  {
    v6 = v4 & 1;
    v7 = v4 >> 1;
    memset(lParam, 0, 4 * v7);
    v8 = &lParam[v7];
    for ( j = v6; j; --j )
    {
      *(_WORD *)v8 = 0;
      v8 = (int *)((char *)v8 + 2);
    }
  }
  v10 = 0;
  for ( k = v26; *(_WORD *)k; ++v10 )
    k = (int *)((char *)k + 2);
  if ( v10 )
  {
    v6 = v10 & 1;
    v12 = v10 >> 1;
    memset(v26, 0, 4 * v12);
    v13 = &v26[v12];
    for ( l = v6; l; --l )
    {
      *(_WORD *)v13 = 0;
      v13 = (int *)((char *)v13 + 2);
    }
  }
  v15 = 0;
  v16 = v3;
  if ( v3 && *v3 )
  {
    do
    {
      ++v16;
      ++v15;
    }
    while ( *v16 );
  }
  v17 = operator new[](2 * (v15 + 1));
  v18 = 0;
  if ( *v3 )
  {
    v19 = *v3;
    v20 = 0;
    do
    {
      ++v18;
      v17[v20] = v19;
      v20 = v18;
      v19 = v3[v18];
    }
    while ( v19 );
  }
  v21 = 0;
  v17[v18] = 0;
  v22 = v17;
  if ( v17 )
  {
    if ( *v17 )
    {
      do
      {
        ++v22;
        ++v21;
      }
      while ( *v22 );
    }
    v23 = 0;
    if ( v21 )
    {
      do
      {
        if ( v23 >= 2 )
        {
          if ( v23 >= 4 )
            v17[v23] ^= ‘B‘;
          else
            v17[v23] ^= ‘P‘;
        }
        else
        {
          v17[v23] ^= 0xFu;
        }
        ++v23;
      }
      while ( v23 < v21 );  //对密码进行加密
    }
  }
  j_j_j___free_base(v3);
  return sub_401870((int)v25, v17);
}

这是一个加密函数,对密码进行了简单的异或运算。然后将加密后的密码与结构体传给sub_401870函数。

根据最后传给sub_401870函数的参数来看,该函数的上半部分没有什么用,应该是垃圾代码,用来混淆视听的。

sub_401870

查看伪代码。

BOOL __fastcall sub_401870(struct_count_array *a1, _WORD *a2)
{
  struct_count_array *str_1; // ebx
  _WORD *en_pass; // edi
  char *v4; // ecx
  signed int v5; // eax
  signed int v6; // eax
  __int16 *v7; // ecx
  unsigned int v8; // edx
  __int16 *i; // eax
  unsigned int v10; // ecx
  unsigned int v11; // eax
  unsigned int v12; // ecx
  _WORD *v13; // eax
  unsigned int v14; // eax
  unsigned int v15; // edx
  _WORD *j; // eax
  unsigned int v17; // ecx
  unsigned int v18; // eax
  int v19; // esi
  __int16 v20; // cx
  __int64 *v21; // ebx
  __int16 *v22; // 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 *str_2; // [esp+Ch] [ebp-54h]
  __int16 charlist_ATOZ[28]; // [esp+10h] [ebp-50h]
  char charlist_1to9; // [esp+48h] [ebp-18h]
  __int64 v34; // [esp+50h] [ebp-10h]
  __int16 v35; // [esp+58h] [ebp-8h]

  str_1 = a1;
  en_pass = a2;
  str_2 = a1;
  memset(charlist_ATOZ, 0, 0x36u);
  v4 = &charlist_1to9;
  v5 = ‘0‘;
  do
  {
    *(_WORD *)v4 = v5;
    v4 += 2;
    ++v5;
  }
  while ( v5 <= ‘9‘ );  //填充数字0~9
  v6 = ‘a‘;
  v7 = charlist_ATOZ;
  do
  {
    *v7 = v6;
    ++v7;
    ++v6;
  }
  while ( v6 <= ‘z‘ );  //填充小写字母a~z
  v8 = 0;
  for ( i = charlist_ATOZ; *i; ++v8 )
    ++i;
  v10 = 0;
  if ( v8 )
  {
    do
    {
      v11 = (unsigned __int16)charlist_ATOZ[v10];
      if ( v11 >= ‘a‘ && v11 <= ‘z‘ )
        charlist_ATOZ[v10] = v11 - 32;
      ++v10;
    }
    while ( v10 < v8 );     //将小写字母转为大写字母
  }
  v12 = 0;
  v13 = en_pass;
  if ( en_pass )
  {
    if ( *en_pass )
    {
      do
      {
        ++v13;
        ++v12;
      }
      while ( *v13 );   //计算密码长度
    }
    v14 = 0;
    if ( v12 )
    {
      do
      {
        if ( v14 >= 2 )
        {
          if ( v14 >= 4 )
            en_pass[v14] ^= ‘B‘;
          else
            en_pass[v14] ^= ‘P‘;
        }
        else
        {
          en_pass[v14] ^= 0xFu;
        }
        ++v14;
      }
      while ( v14 < v12 );  //将加密的密码解密
    }
    v15 = 0;
    for ( j = en_pass; *j; ++v15 )
      ++j;
    v17 = 0;
    if ( v15 )
    {
      do
      {
        v18 = (unsigned __int16)en_pass[v17];
        if ( v18 >= ‘a‘ && v18 <= ‘z‘ )
          en_pass[v17] = v18 - 32;
        ++v17;
      }
      while ( v17 < v15 );  //将密码中的小写字母转为大写字母
    }
  }
  v19 = 0;
  v34 = ‘‘;
  v35 = 0;
  if ( *en_pass )
  {
    v20 = charlist_ATOZ[0];
    v21 = &v34;
    v22 = en_pass;
    do
    {
      if ( v20 )
      {
        v23 = *v22;
        v24 = charlist_ATOZ;
        v25 = 0;
        while ( v23 != *v24 )
        {
          v24 = &charlist_ATOZ[++v25];
          if ( !charlist_ATOZ[v25] )
            goto LABEL_37;
        }
        *(_WORD *)v21 = charlist_ATOZ[v25];
        v21 = (__int64 *)((char *)v21 + 2);
LABEL_37:
        v20 = charlist_ATOZ[0];
      }
      v22 = &en_pass[++v19];
    }
    while ( en_pass[v19] );
    str_1 = str_2;
  }             //记录密码中所有字母的位置
  v26 = 0;
  v27 = &v34;
  if ( (_WORD)v34 )
  {
    do
    {
      v27 = (__int64 *)((char *)v27 + 2);
      ++v26;
    }
    while ( *(_WORD *)v27 );    //统计密码中字母的个数
    if ( v26 == 2 )     //密码中的字母个数必须为2个
    {
      LODWORD(v34) = 0x350031;  //设置v34低字为15
      HIDWORD(v34) = &unk_420050;   //设置v34高字为pb
      v28 = en_pass + 2;
      v35 = 0;
      v29 = 0;
      while ( *((_WORD *)&v34 + v29) == *v28 )
      {
        ++v29;
        ++v28;
        if ( v29 >= 4 )
        {
          if ( !sub_401740(str_1, en_pass) )
            return PostMessageW((HWND)str_1->win_handle, 0x111u, 0x40Au, 0);
          return PostMessageW((HWND)str_1->win_handle, 0x111u, 0x40Bu, 0);
        }
      }
    }
  }
  return PostMessageW((HWND)str_1->win_handle, 0x111u, 0x40Au, 0);
}

该函数有些复杂,我一开始也没看懂,最后是看了大佬的文章又想了想,才明白了一点。

大致过程就是i,先生成了两个表,一个数字0~9的表,一个大写字母表。然后解密密码,将密码中的小写字母转为大写字母。随后统计密码中的字母个数并检测密码从第三位到第六位是否为15pb。通过sub_401740函数检测第一位、第二位和最后一位,通过后传递成功消息。

sub_401740

该函数的参数为结构体和解密后的密码。

查看伪代码。

signed int __fastcall sub_401740(struct_count_array *a1, _WORD *a2)
{
  _WORD *passwd; // ebx
  struct_count_array *str_1; // edx
  __int128 *v4; // ecx
  signed int v5; // eax
  int passwd_len; // esi
  _WORD *v7; // eax
  _WORD *v8; // edi
  unsigned __int16 *v9; // eax
  int v10; // esi
  __int128 *v11; // ecx
  __int16 v12; // di
  __int16 v13; // ax
  __int128 *v14; // ecx
  signed int v15; // edx
  unsigned __int16 *v17; // [esp+4h] [ebp-50h]
  struct_count_array *str_2; // [esp+8h] [ebp-4Ch]
  _WORD *v19; // [esp+Ch] [ebp-48h]
  __int128 charlist_1to9; // [esp+30h] [ebp-24h]
  __int64 v21; // [esp+40h] [ebp-14h]
  int v22; // [esp+48h] [ebp-Ch]
  __int16 v23; // [esp+4Ch] [ebp-8h]

  passwd = a2;
  v22 = 0;
  str_1 = a1;
  v23 = 0;
  str_2 = a1;
  charlist_1to9 = ‘‘;
  v4 = &charlist_1to9;
  v5 = ‘1‘;
  v21 = ‘‘;
  do
  {
    *(_WORD *)v4 = v5;
    v4 = (__int128 *)((char *)v4 + 2);
    ++v5;
  }
  while ( v5 <= ‘9‘ );  //填充数字1~9
  passwd_len = 0;
  v7 = passwd;
  if ( passwd && *passwd )
  {
    do
    {
      ++v7;
      ++passwd_len;
    }
    while ( *v7 );  //计算密码长度
  }
  v8 = sub_4028D0((char *)&charlist_1to9, &passwd[*(_DWORD *)&str_1->Now_count]);   //截取密码连接在数字表后面
  v9 = &passwd[passwd_len - 1];
  v19 = v8;
  v10 = 0;
  v17 = v9;
  if ( (_WORD)charlist_1to9 )
  {
    v11 = &charlist_1to9;
    v12 = *v9 & 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;
      }
      v8 = v19;
      goto LABEL_12;
    }
  }
  else
  {
LABEL_12:
    v14 = &charlist_1to9;
    v15 = ‘1‘;
    while ( *(_WORD *)v14 == *(_WORD *)((char *)v14 + (char *)passwd - (char *)&charlist_1to9) )    //第一位是1
    {
      v15 += 6;
      v14 = (__int128 *)((char *)v14 + 2);
      if ( v15 > ‘9‘ )
      {
        if ( (unsigned __int16)*passwd + (unsigned __int16)v8[9] == ‘c‘ //第二位是2
          && *v17 == *(_DWORD *)&str_2->Now_count + (unsigned __int16)v8[6] )   //最后一位7+次数,即8
        {
          return 1;
        }
        return 0;
      }
    }
  }
  return 0;
}

该函数也很复杂,对我来说也是挺有难度的。大致过程是,先生成1~9的数字表,然后从输入密码次数+1的位置开始截取密码,即假如输入‘ABCDEFG‘,次数为1,则截取后的字符串为‘BCDEFG‘,然后将其连接在数字表的后面,得到‘123456789BCDEFG‘。

根据循环的条件可以知道第一位为1,第一位+第二位=‘c‘,所以第二位为2,因为次数参与了运算,所以从第二次的验证开始就无法验证第二位了,所以只能一次输对。最后一位是次数+7,所以即8。

最后得到正确密码为:1215pb8。

技术分享图片

参考

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

https://www.52pojie.cn/thread-755611-1-1.html

(出处: 吾爱破解论坛)

以上是关于看雪CTF2016CrackMe攻防大赛——第一题的主要内容,如果未能解决你的问题,请参考以下文章

看雪CTF 2016_第二十一题分析

国庆七天嗨之看雪CTF--第一题

看雪2017CTF第二题解法

攻防世界 Reverse高手进阶区 3分题 crackme

攻防世界 Reverse高手进阶区 3分题 crackme

攻防世界 Reverse高手进阶区 3分题 crackme