逆向工程——二进制炸弹(第6关补完版)

最近收到THU的同学回复:第6关似乎应该是链表。我之前也很奇怪怎么最后一关会这么简单。于是找来最高难度的phase_6版本挑战一下。

  

phase_6的反汇编也确实够长了(差不多两页A4)。刚开始,真有点不知从何下手。先大致浏览一遍,唯一的印象是这段代码中跳转语句达到了12个,仅仅是这一条就会晕头转向了。

phase6

  

根据以往的经验,首先就是选出那些不该执行的语句(explode_bomb)。如图所示,黄线标出了引爆点。另外,既然跳转这么多,那就先来看看跳转,理出个大体结构。可以看到代码中跳转分成了两大类:1)条件跳转。2)直接跳转。而进一步分析,有些条件跳转是和call explode_bomb相关的,而这条指令不该被执行,所以就又可以确定了某些条件跳转的方向。于是,红色箭头标出了确定的跳转方向,蓝色箭头标出5个条件跳转的不确定方向。

 

标出了跳转方向后,程序的大致结构就比较清晰了。根据这些跳转,可以把phase_6分成两大主要语句块(红色和绿色)。这时发现红色块执行到绿色块唯一的途径是蓝色跳转语句(1)(蓝1)。大致的结构分析完了,就深入程序看看吧。

 

首先由call 8049112<read_six_numbers>可以知道要读入6个数。那问题就是怎样的6个数?

 

红色代码块:

 

由红色的代码块,我得出结论:这6个数<=6,且均不相等。

 

为什么这么说呢?由蓝2和最后一条jmp 8048c77的大跳转,能想到什么,是不是有点像一个大循环嵌套了一个小循环呢?那为什么大跳转是用jmp而不是条件跳转。这时,想到了唯一跳转出红色大块的蓝1,可以猜想到了语句中用到了类似goto的语句(可见goto确实很会搅局,连分析起反汇编后的代码都很晦涩)。看到红色大块的前两条,感觉和输入数据有关,于是查看-0x24(%ebp)后知道是输入的第一个数据data[0],所以之后data[i]就存在了-0x24(%ebp, i, 4) = -0x24(%ebp) + 4i。由此可以推断%ebx应该是索引变量,而每次%eax = data[i]。再由代码块(a)得知data[i] <= 6才行,否则就爆炸了。

 

再往下就是%edi = %ebx + 1,%edi就是下一个索引,当到了索引5时就执行蓝1跳转。由此,可以先写个外层循环的大致框架:

   1:  for (int i = 0; ; i++)
   2:  {
   3:      %eax = data[i]
   4:      %edi = i + 1
   5:      if (%edi == 6)
   6:          goto blueblock;
   7:  }

 

之后的lea -0x24(%ebp, %ebx, 4), %esi则是把当前数据的地址赋给%esi,即%esi = &data[i]。

   1:  mov %edi, %ebx        // %ebx = %ebx + 1
   2:  lea -0x24(%ebp), %eax  // %eax = &data[0]
   3:  %edx = &data[0]
   4:  mov -0x4(%edx, %edi, 4), %eax  // %eax = %edx + 4%edi – 4 = &data[0] + 4(%edi – 1)。
   5:  cmp 0x4(%esi), %eax  // 比较*(&data[i] + 0x4),*(&data[0] + 4(%edi – 1)) 从这条可以隐约看出是在比较两个输入的数据,那么这两个输入的数据关系是什么呢?

 

往下读,jne 8048cb1可知这两个数不能相等,否则就爆炸了。

 

最后看到代码块(b),每次%ebx + 1, %esi + 4,同时以%ebx <= 5作为循环条件。根据蓝2的跳转,知道每次mov -0x4(%edx, %edi, 4), %eax中的%edx和%edi是不变的,所以%eax == &data[0] + 4(%edi – 1),而%edi为外循环初始时的%ebx+1(因为内循环%ebx每次都在累加),所以%eax == &data[0] + 4%ebx == &data[i]。

 

而每次%esi + 4,而初始%esi = -0x24(%ebp, %ebx, 4)=&data[i],所以cmp 0x4(%esi), %eax依次遍历data[i]后面的数据。根据这些线索,我们可以写出内循环的框架:

   1:  %esi = &data[i];
   2:  for(int j = i + 1 ; ; j <= 5 )
   3:  {
   4:      if (data[j] != data[i])
   5:          j++;
   6:  else
   7:      explode_bomb();
   8:  }

 

最后的那条mov %edi, %ebx是把外层的循环变量复原,在此就在内外层用不同的两个变量了,最后写成C代码如下:

   1:      for(int i = 0; ; i++)
   2:      {
   3:          if (data[i] > 6)
   4:              explode_bomb();
   5:   
   6:          if (i + 1 == 6)
   7:              goto blueblock;
   8:   
   9:          for(int j = i + 1; j <= 5; j++)
  10:          {
  11:              if (data[j] == data[i])
  12:                  explode_bomb();
  13:          }
  14:      }

 

于是,根据以上的分析,我们就得出了最初的结论:这6个数<=6,且均不相等。

 

绿色代码:

 

这段代码很长,初看很没头绪。那怎么办呢?依然根据跳转语句来理出一些思路,希望能够分成更小的块,分而治之。于是我们有了四个更小的代码块c, d, e, f。

 

代码c:

可以知道,%ecx保存的是地址值,而%eax是一个循环变量。cmp -0x24(%ebp, %edx, 4), %eax可知要取地址的次数为我们输入的数据。

 

代码d:

由-0x24(%ebp, %edx, 4)可知%edx是索引变量。再由mov %ecx, -0x3c(%ebp, %edx, 4)可知相应的地址(-0x3c(%ebp), -0x38(%ebp), -0x34(%ebp), -0x30(%ebp), -0x2c(%ebp), -0x28(%ebp))将被赋值(与代码e中的取地址对应)

 

根据对代码块d的理解,可以写出一个大致的c代码框架:

   1:  for(int i = 0; i < 6; i++)
   2:  {
   3:      addr = 0x804a5fc;
   4:   
   5:      for(int j = 0; j < data[i]; j++)
   6:          addr = *(addr + 0x8);
   7:   
   8:      -0x3c(%ebp) + 4 * i = addr;
   9:  }

 

代码e:

 

举例来说:

   1:  mov -0x3c(%ebp), %ecx
   2:  mov -0x38(%ebp), %eax
   3:  mov %eax, 0x8(%ecx)

 

可知, -0x38(%ebp)中的地址被放入了*(-0x3c(%ebp)) + 0x8中,而后的各步与此相似。相当于在做一个链表的链接功能。

 

代码f:

   1:  mov 0x8(%ebx), %edx
   2:  mov (%ebx), %eax
   3:  cmp (%edx), %eax

 

这三条语句可知这是当前节点的值(*%ebx)和下一个节点的值(*0x8(%ebx))在进行比较,而且当前节点的值必须大于等于下一个节点的值。由此可以写出一个c的代码框架:

   1:  struct node
   2:  {
   3:      int x, y;
   4:      node *next;
   5:  };
   6:   
   7:  node a = firstNode;
   8:  for(int i = 0; i < 5; i++)
   9:  {
  10:      node b = a->next;
  11:   
  12:      if (a->x >= b->x)
  13:          a = b;
  14:      else
  15:          explode_bomb();
  16:  }

 

由此,我们可以知道检查0x804a5fc及其之后0x8为步长的地址是关键。

 

由gdb查看得知:

                                                   *0x804a5fc = 0x3b7

*(0x804a5fc + 8) = 0x804a5f0,   *0x804a5f0 = 0x3c6

*(0x804a5f0 + 8) = 0x804a5e4,    *0x804a5e4 = 0x112

*(0x804a5e4 + 8) = 0x804a5d8,   *0x804a5d8 = 0x3a4

*(0x804a5d8 + 8) = 0x804a5cc,   *0x804a5cc = 0x5a

*(0x804a5cc + 8) = 0x804a5c0,    *0x804a5c0 = 0xfe

根据这个内存的检测,我们把值进行排序,以期符合代码f中的要求,得到(括号中为所属序号):

0x3c6 (2) > 0x3b7 (1) > 0x3a4 (4) > 0x112 (3) > 0xfe (6) > 0x5a (5),根据其序号得出最后的答案: 2 1 4 3 6 5

posted @ 2011-06-27 17:01  chkkch  阅读(7427)  评论(3编辑  收藏  举报