最近收到THU的同学回复:第6关似乎应该是链表。我之前也很奇怪怎么最后一关会这么简单。于是找来最高难度的phase_6版本挑战一下。
phase_6的反汇编也确实够长了(差不多两页A4)。刚开始,真有点不知从何下手。先大致浏览一遍,唯一的印象是这段代码中跳转语句达到了12个,仅仅是这一条就会晕头转向了。
根据以往的经验,首先就是选出那些不该执行的语句(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