硬件断点分析-解逆向题的一种思路

根据sayhi师傅的思路总结。需要依赖于动态调试。实验用的源码贴在最后面。

实验源程序:http://file.eonew.cn/ctf/re/crackme

题目类型

可以用于非算法类的逆向题目,对于逐个字符比较的题目由为简单,可以忽略ollvm的混淆作用,只需要看核心代码即可。

思路

对于我们的输入下硬件断点。然后查看程序的运行情况:

  1. 查看我们的输入是否有变化
  2. 注意我们的输入是否被转移
  3. 对于断点前后的代码要仔细研究

实例

实验的环境是 IDA7.0 。

这个是我自己用obfuscator-llvm编译的一个简单的程序,但是经过混淆之后的代码根本就不堪入目。下面是main函数的程序流图。

IDA反汇编出的main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // er13
  bool v4; // r14
  signed int v5; // eax
  int v6; // ebp
  const char *v7; // rdi
  bool v8; // zf
  signed int v9; // ecx
  char v11; // [rsp+6h] [rbp-182h]
  char v12; // [rsp+7h] [rbp-181h]
  int v13; // [rsp+8h] [rbp-180h]
  int v14; // [rsp+Ch] [rbp-17Ch]
  __int64 v15; // [rsp+18h] [rbp-170h]
  char *v16; // [rsp+28h] [rbp-160h]
  __int128 v17; // [rsp+30h] [rbp-158h]
  __int64 v18; // [rsp+40h] [rbp-148h]
  char s[255]; // [rsp+50h] [rbp-138h]
  char v20; // [rsp+14Fh] [rbp-39h]

  v17 = xmmword_400920;
  v18 = 0LL;
  fgets(s, 255, stdin);
  v20 = 0;
  v5 = -509329727;
  v6 = 0;
  while ( 1 )
  {
LABEL_25:
    while ( v5 > -509329728 )
    {
      switch ( v5 )
      {
        case -509329727:
          v13 = v6;
          v5 = -1064694185;
          if ( v6 < 255 )
            v5 = 425795528;
          v4 = 0;
          if ( v5 > 287732016 )
          {
LABEL_5:
            while ( v5 > 1288322757 )
            {
              if ( v5 > 1505978791 )
              {
                if ( v5 != 1505978792 )
                {
                  if ( v5 != 1985928755 )
                    goto LABEL_4;
                  v3 = v14 + 1;
                  v5 = -507379925;
                  goto LABEL_25;
                }
                v5 = -1367224388;
                if ( !*v16 )
                  v5 = 287732017;
                if ( v5 <= 287732016 )
                  goto LABEL_25;
              }
              else
              {
                if ( v5 != 1288322758 )
                {
                  if ( v5 != 1321704548 )
                    goto LABEL_4;
                  v6 = v13 + 1;
                  v5 = -509329727;
                  goto LABEL_25;
                }
                v5 = -1367224388;
                if ( !*((_BYTE *)&v17 + v15) )
                  v5 = 1505978792;
                if ( v5 <= 287732016 )
                  goto LABEL_25;
              }
            }
            switch ( v5 )
            {
              case 287732017:
                v7 = "Successed";
                goto LABEL_2;
              case 425795528:
                v4 = s[v13] != 10;
                v5 = -1064694185;
                break;
              case 453611148:
                s[v13] = 0;
                v5 = -507379925;
                v3 = 0;
                break;
              default:
                goto LABEL_4;
            }
          }
          break;
        case -507379925:
          v14 = v3;
          v15 = v3;
          v16 = &s[v3];
          v11 = s[v3];
          v8 = v11 == 0;
          v5 = 1288322758;
          v9 = -916046165;
          goto LABEL_44;
        case 45310531:
          v8 = v14 + v12 + 1 == v11;
          v5 = 1985928755;
          v9 = 1288322758;
          goto LABEL_44;
        default:
LABEL_4:
          if ( v5 > 287732016 )
            goto LABEL_5;
          break;
      }
    }
    if ( v5 > -954026375 )
      break;
    if ( v5 == -1367224388 )
    {
      v7 = "Failed";
LABEL_2:
      puts(v7);
      v5 = -954026374;
    }
    else
    {
      if ( v5 != -1064694185 )
        goto LABEL_4;
      v8 = v4 == 0;
      v5 = 453611148;
      v9 = 1321704548;
LABEL_44:
      if ( !v8 )
        v5 = v9;
      if ( v5 > 287732016 )
        goto LABEL_5;
    }
  }
  if ( v5 == -916046165 )
  {
    v12 = *((_BYTE *)&v17 + v15);
    v8 = v12 == 0;
    v5 = 1288322758;
    v9 = 45310531;
    goto LABEL_44;
  }
  if ( v5 != -954026374 )
    goto LABEL_4;
  return 0;
}

从上面可以看出,要恢复出原来的流程图是非常困难的,这时我们不妨使用一下上面说的那种思路。

第一步:下断点

执行的时候我们先输入字符串12进行实验。

.text:00000000004005D6 mov     rdx, cs:stdin@@GLIBC_2_2_5 ; stream
.text:00000000004005DD mov     esi, 0FFh       ; n
.text:00000000004005E2 mov     rdi, rbx        ; s
.text:00000000004005E5 call    _fgets
.text:00000000004005EA mov     [rsp+188h+var_39], 0

首先在0x4005EA下断点,也就是调用fgets之后的第一条指令,来保证我们的输入现在还没有经过任何操作,然后找到我们的输入的内存,下硬件断点,并加入的watch list中,直接按F9执行到硬件断点处。

第二步:下额外断点

在硬件断点旁继续下断点,比如在执行完上面的步骤后,程序会在0x4006F9停下来:

.text:00000000004006EF loc_4006EF:                             ; CODE XREF: main+ECj
.text:00000000004006EF movsxd  rax, [rsp+188h+var_180]
.text:00000000004006F4 cmp     [rsp+rax+188h+s], 0Ah
.text:00000000004006F9 setnz   r14b
.text:00000000004006FD mov     eax, 0C08A0E57h
.text:0000000000400702 cmp     eax, 11267130h
.text:0000000000400707 jg      loc_40063B
.text:000000000040070D jmp     short loc_400730
.text:000000000040070F ; ---------------------------------------------------------------------------

所以我们在0x4006EF下断点来观察这段代码的具体功能。

继续按F9执行

.text:0000000000400802 loc_400802:                             ; CODE XREF: main+1EC↑j
.text:0000000000400802 mov     [rsp+188h+var_17C], r13d
.text:0000000000400807 movsxd  rax, [rsp+188h+var_17C]
.text:000000000040080C mov     [rsp+188h+var_170], rax
.text:0000000000400811 mov     rax, [rsp+188h+var_170]
.text:0000000000400816 add     rax, rbx
.text:0000000000400819 mov     [rsp+188h+var_160], rax
.text:000000000040081E mov     rax, [rsp+188h+var_160]
.text:0000000000400823 movzx   eax, byte ptr [rax]
.text:0000000000400826 mov     [rsp+188h+var_182], al
.text:000000000040082A cmp     [rsp+188h+var_182], 0
.text:000000000040082F mov     eax, 4CCA3EC6h
.text:0000000000400834 mov     ecx, 0C9663EABh
.text:0000000000400839 jmp     short loc_400858
.text:000000000040083B ; ---------------------------------------------------------------------------

程序在0x400826处停下,所以在0x400802下断点来观察这段代码的具体功能。同时还观察到下面的代码:

.text:0000000000400823 movzx   eax, byte ptr [rax]
.text:0000000000400826 mov     [rsp+188h+var_182], al

这里程序将我们的输入的字符串的1转移到了另一个内存地址,可以怀疑是转移判断,所以在转移到的地方再下一个硬件断点,这里假设断点的名字是break2,并加入的watch list中。

继续按F9执行,发现在.text:000000000040082A cmp [rsp+188h+var_182], 0停下,进行了0判断,应该是判断是否为字符串尾部。

继续按F9执行

.text:000000000040078D movsx   eax, [rsp+188h+var_181]
.text:0000000000400792 mov     ecx, [rsp+188h+var_17C]
.text:0000000000400796 lea     eax, [rcx+rax+1]
.text:000000000040079A movsx   ecx, [rsp+188h+var_182]
.text:000000000040079F cmp     eax, ecx
.text:00000000004007A1 mov     eax, 765EDE33h
.text:00000000004007A6 mov     ecx, 4CCA3EC6h
.text:00000000004007AB jmp     loc_400858

又在0x40079A触发硬件断点,所以继续在0x40078D下断点,这里假设断点的名字是break3,可以看到程序将break2的值赋值给了ecx。并在下面有一个判断:

.text:000000000040079F cmp     eax, ecx

这是eax的低字节是字符y,而ecx的低字节就是我们输入的第一个字符1。所以这应该是一个转移判断。

结束

继续运行,发现程序进入了系统库中,标志着程序的结束。

分析

对上面的几个断点进行分析。不难看出其重点是break3断点。这里我们只需要下断点,在执行下面语句的时候

.text:000000000040079F cmp     eax, ecx

每次都把ecx的值改成和eax一样,并记录eax的值,那么就能得到我们的flag。

注意:在得到flag的步骤那一步,我们输入的字符串要足够长,否则会提前中止判断。

源码

// compiled: clang -mllvm -fla crackme.c -O3 -o crackme
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    int i;
    // you_get_the_flag
    char str[24] = "xmr[b_mWk^ZSY^RW";
    char buf[0x100];
    fgets(buf, 0x100 - 1, stdin);
    buf[0x100 - 1] = '\0';

    i = 0;
    while (i < 0x100 - 1 && buf[i] != '\n')
    {
        i++;
    }
    buf[i] = '\0';

    for (i = 0; buf[i]; i++)
    {
        if (str[i])
        {
            if (str[i] + i + 1 != buf[i])
            {
                break;
            }
        }
        else
        {
            break;
        }
    }

    if (str[i] == 0 && buf[i] == 0)
    {
        puts("Successed");
    }
    else
    {
        puts("Failed");
    }

    return 0;
}

总结

对于CTF题目来说,掌握的思路越多,那么解出题目的可能性更大。