zoukankan      html  css  js  c++  java
  • 逆向工程——二进制炸弹(第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

  • 相关阅读:
    VS2013 调试窗口一闪而过的解决方法
    什么是文件?
    局部变量和全局变量的区别
    一个简单java程序的要素
    运行一个简单的Java程序
    Javascript 构造函数原型继承机制
    函数式编程之一等公民的函数
    弹性布局flex-兼容问题
    TypeScript中的枚举类型
    依赖注入
  • 原文地址:https://www.cnblogs.com/chkkch/p/2091511.html
Copyright © 2011-2022 走看看