最近因为参加比赛有两周没有做lab了,上周做完了bomb lab,现在终于想起来补这篇解题报告,这次实验是一次相当有趣的拆炸弹游戏(嗯,没错,你可以理解为游戏),要利用gdb进行反汇编调试,一步步找到通过六个关卡和一个隐藏关的密码。总体涉及C语言的分支跳转循环链表二叉树等结构的汇编语言表示,总的说来并不算特别难。

Bomb Lab 简介

Github地址:Bomb Lab

这个lab练习反汇编调试程序,需要你对汇编语言有一些掌握,最起码需要明白每个指令是要干什么。整个实验基础部分有六关,每一关有个密码,你需要在调试的过程中通过逆向工程解析出通关的密码,以此来突破六关,此外还有一个隐藏关,需要你去找到它的入口并解析出它的密码。当然如果你输入了错误的密码,炸弹就会爆炸。

首先让我们来看一看实验中给出的boom.c文件。

/* Hmm...  Six phases must be more secure than one phase! */
    input = read_line();             /* Get input                   */
    phase_1(input);                  /* Run the phase               */
    phase_defused();                 /* Drat!  They figured it out!
				      * Let me know how they did it. */
    printf("Phase 1 defused. How about the next one?\n");

这一部分显然就是每一关的代码,input是我们输入的密码字符串,puase_x就是每一关的判断函数,在通过这一关之后会调用phase_defused函数,然后打印一行提示信息。

整个文件并没有什么太多的提示,唯一要我们注意的地方就是在第六关结束之后,有这样一段话:

/* Wow, they got it!  But isn't something... missing?  Perhaps
     * something they overlooked?  Mua ha ha ha ha! */

有什么忽略的地方?对,那就是本实验还有一个隐藏关卡,至于怎么进入就需要我们自己去探讨了。

GDB调试器

关于汇编语言的细节我就不再赘述了,很多地方都有指令含义的介绍。我们这里重点提一下我们解决本lab的工具——gdb调试器。

首先,我们可以先对整个文件进行反汇编处理一下:

objdump -d bomb > bomb.asm //得到含有整个程序所有汇编代码的文件
objdump -t bomb > func.asm //得到含有整个程序所有函数入口地址的文件

接着,使用gdb bomb进入gdb调试界面。先介绍下常用的命令:

  • r:运行程序
  • b *0x400e65:在地址为0x400e65处设置断点
  • d 1:删除第一个断点,以此类推
  • c:继续运行
  • stepi:执行一条指令
  • stepi 2:执行两条指令,以此类推
  • disas:反汇编当前函数
  • disas 0x400e65:反汇编0x400e65为入口地址的函数
  • disas main:反汇编以main为函数名的函数
  • info frame:查看有关栈帧的信息
  • info registers:查看有关寄存器的信息
  • p $rax:查看寄存器%rax内储存的值
  • x $rax:查看以寄存器%rax内储存的值为地址处的值
  • p或x /s $rax:以字符串形式输出
  • p或x /t $rax:以二进制形式输出
  • p或x /x $rax:以十六进制形式输出
  • x /32b $rsp:查看栈帧的前32字节
  • x /8g $rax:查看以%rax中储存的值位地址开始的8个字

好了,常用的命令就这么些,接下来就让我们开始紧张刺激的拆炸弹游戏了!

第一关:简单的字符串

首先我们用disas main对主函数反汇编,找到phase_1的代码:

   0x0000000000400e32 <+146>:	callq  0x40149e <read_line>
   0x0000000000400e37 <+151>:	mov    %rax,%rdi
   0x0000000000400e3a <+154>:	callq  0x400ee0 <phase_1>
   0x0000000000400e3f <+159>:	callq  0x4015c4 <phase_defused>
   0x0000000000400e44 <+164>:	mov    $0x4023a8,%edi

再用disas phase_1得到第一关的代码。

Dump of assembler code for function phase_1:
   0x0000000000400ee0 <+0>:     sub    $0x8,%rsp
   0x0000000000400ee4 <+4>: 	mov    $0x402400,%esi
   0x0000000000400ee9 <+9>: 	callq  0x401338 <strings_not_equal>
   0x0000000000400eee <+14>:	test   %eax,%eax
   0x0000000000400ef0 <+16>:	je     0x400ef7 <phase_1+23>
   0x0000000000400ef2 <+18>:	callq  0x40143a <explode_bomb>
   0x0000000000400ef7 <+23>:	add    $0x8,%rsp
   0x0000000000400efb <+27>:	retq   
End of assembler dump.

在这里我们看到调用了一个函数strings_not_equal,根据名称我们猜测它应该是用来判断字符串是否相等的,然后有发现前面把0x402400赋值给了%esi,于是我们看一下这个地址到底存放了什么。

执行x /s 0x402400,就看到:

0x402400:	"Border relations with Canada have never been better."

这就是第一关的答案,基本送分。

第二关:等比数列

同样地,我们看一看第二关的代码:

Dump of assembler code for function phase_2:
   0x0000000000400efc <+0>:     push   %rbp
   0x0000000000400efd <+1>:     push   %rbx
   0x0000000000400efe <+2>:     sub    $0x28,%rsp
   0x0000000000400f02 <+6>:     mov    %rsp,%rsi
   0x0000000000400f05 <+9>:     callq  0x40145c <read_six_numbers>
   ......
End of assembler dump.

首先我们看到有个函数read_six_numbers,预示这关要输入六个数字。至于输入格式呢,我们要再看一看这个函数的代码。

   0x0000000000401480 <+36>:	mov    $0x4025c3,%esi
   0x0000000000401485 <+41>:	mov    $0x0,%eax
   0x000000000040148a <+46>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x000000000040148f <+51>:	cmp    $0x5,%eax
   0x0000000000401492 <+54>:	jg     0x401499 <read_six_numbers+61>

注意到mov $0x4025c3,%esi这一行,我们看一看到底给%esi赋值了什么。执行x /s 0x4025c3,得到。

0x4025c3:	"%d %d %d %d %d %d"

显然这是scanf函数中的表示输入的字符串。

回到第二关的函数,我们在调用输入读入六个数字的函数之后设置一个断点,输入1 2 3 4 5 6来测试。

程序暂停后,我们用x /32x $rsp看一看现在栈的结构:

(gdb) x /32x $rsp
0x7fffffffdfb0:	0x01	0x00	0x00	0x00	0x02	0x00	0x00	0x00
0x7fffffffdfb8:	0x03	0x00	0x00	0x00	0x04	0x00	0x00	0x00
0x7fffffffdfc0:	0x05	0x00	0x00	0x00	0x06	0x00	0x00	0x00
0x7fffffffdfc8:	0x31	0x14	0x40	0x00	0x00	0x00	0x00	0x00

一下就发现,我们输入的六个数分别被存储在了栈中相应的位置。

接着往后,发现程序拿($rsp),也就是我们输入的第一个字符去和1比较,若不相等,接着就引爆炸弹,说明我们要输入的第一个数字是1。随后程序跳转到52行:

   0x0000000000400f30 <+52>:	lea    0x4(%rsp),%rbx
   0x0000000000400f35 <+57>:	lea    0x18(%rsp),%rbp
   0x0000000000400f3a <+62>:	jmp    0x400f17 <phase_2+27>

这里将(%rsp)+4也就是第二个数字赋值给了%rbx。

   0x0000000000400f17 <+27>:	mov    -0x4(%rbx),%eax
   0x0000000000400f1a <+30>:	add    %eax,%eax
   0x0000000000400f1c <+32>:	cmp    %eax,(%rbx)
   0x0000000000400f1e <+34>:	je     0x400f25 <phase_2+41>
   0x0000000000400f20 <+36>:	callq  0x40143a <explode_bomb

跳转回27行之后,看到eax被赋值给了(%rbx)-4,也就是前一个数,再将它加倍和(%rbx)比较。换句话说,就是拿前一个数的两倍和后面一个数比较,若不相等就爆炸。

   0x0000000000400f25 <+41>:	add    $0x4,%rbx
   0x0000000000400f29 <+45>:	cmp    %rbp,%rbx
   0x0000000000400f2c <+48>:	jne    0x400f17 <phase_2+27>
   0x0000000000400f2e <+50>:	jmp    0x400f3c <phase_2+64>

此后发现rbx增加了4,也就是变成了后一个数,之后经过一个比较,若不相等就会跳。这个时候我们就明白了,此前的lea 0x18(%rsp),%rbp是在设置循环终止条件。这个循环要循环到最后一个数,每一个数都是前一个数的两倍。

因此,本关的答案就是:

1 2 4 8 16 32

第三关:switch

我们继续前进到第三关,首先看代码:

Dump of assembler code for function phase_3:
   0x0000000000400f43 <+0>:     sub    $0x18,%rsp
   0x0000000000400f47 <+4>:     lea    0xc(%rsp),%rcx
   0x0000000000400f4c <+9>:     lea    0x8(%rsp),%rdx
   0x0000000000400f51 <+14>:	mov    $0x4025cf,%esi
   0x0000000000400f56 <+19>:	mov    $0x0,%eax
   0x0000000000400f5b <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000400f60 <+29>:	cmp    $0x1,%eax
   0x0000000000400f63 <+32>:	jg     0x400f6a <phase_3+39>
   0x0000000000400f65 <+34>:	callq  0x40143a <explode_bomb>
   0x0000000000400f6a <+39>:	cmpl   $0x7,0x8(%rsp)
   0x0000000000400f6f <+44>:	ja     0x400fad <phase_3+106>
   0x0000000000400f71 <+46>:	mov    0x8(%rsp),%eax
   0x0000000000400f75 <+50>:	jmpq   *0x402470(,%rax,8)
   0x0000000000400f7c <+57>:	mov    $0xcf,%eax
   0x0000000000400f81 <+62>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400f83 <+64>:	mov    $0x2c3,%eax
   0x0000000000400f88 <+69>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400f8a <+71>:	mov    $0x100,%eax
   0x0000000000400f8f <+76>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400f91 <+78>:	mov    $0x185,%eax
   0x0000000000400f96 <+83>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400f98 <+85>:	mov    $0xce,%eax
   0x0000000000400f9d <+90>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400f9f <+92>:	mov    $0x2aa,%eax
   0x0000000000400fa4 <+97>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400fa6 <+99>:	mov    $0x147,%eax
   0x0000000000400fab <+104>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400fad <+106>:	callq  0x40143a <explode_bomb>
   0x0000000000400fb2 <+111>:	mov    $0x0,%eax
   0x0000000000400fb7 <+116>:	jmp    0x400fbe <phase_3+123>
   0x0000000000400fb9 <+118>:	mov    $0x137,%eax
   0x0000000000400fbe <+123>:	cmp    0xc(%rsp),%eax
   0x0000000000400fc2 <+127>:	je     0x400fc9 <phase_3+134>
   0x0000000000400fc4 <+129>:	callq  0x40143a <explode_bomb>
   0x0000000000400fc9 <+134>:	add    $0x18,%rsp
   0x0000000000400fcd <+138>:	retq   
End of assembler dump.

整个观察一下代码,发现有很多地方直接跳转到了123行,又在前面有一个jmpq *0x402470(,%rax,8)这样的跳转,所以,这是一个switch结构,本题应该有很多答案。

同样的,看到第14行,执行x /s 0x4025cf后得到:

0x4025cf:	"%d %d"

这应该就是本题需要我们输入的结果了。

接着往后看,这里有一个比较,说明我们要输入的第一个数要小于等于7。

   0x0000000000400f6a <+39>:	cmpl   $0x7,0x8(%rsp)
   0x0000000000400f6f <+44>:	ja     0x400fad <phase_3+106>

得到这个结果后,本关就简单了,我们只需要输入1-7,设置断点进行测试,看结果会跳转到哪一行,再查看比较的数是多少即可。

最终,本关的答案是:(共7组)

(1,311), (2,707), (3,256), (4,389), (5,206), (6,682), (7,327)

这一关其实比上一关还要简单,目的就是为了让你熟悉switch结构。

第四关:递归

首先看第四关的代码:

Dump of assembler code for function phase_4:
   0x000000000040100c <+0>:     sub    $0x18,%rsp
   0x0000000000401010 <+4>:     lea    0xc(%rsp),%rcx
   0x0000000000401015 <+9>:     lea    0x8(%rsp),%rdx
   0x000000000040101a <+14>:	mov    $0x4025cf,%esi
   0x000000000040101f <+19>:	mov    $0x0,%eax
   0x0000000000401024 <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000401029 <+29>:	cmp    $0x2,%eax
   0x000000000040102c <+32>:	jne    0x401035 <phase_4+41>
   0x000000000040102e <+34>:	cmpl   $0xe,0x8(%rsp)
   0x0000000000401033 <+39>:	jbe    0x40103a <phase_4+46>
   0x0000000000401035 <+41>:	callq  0x40143a <explode_bomb>
   0x000000000040103a <+46>:	mov    $0xe,%edx
   0x000000000040103f <+51>:	mov    $0x0,%esi
   0x0000000000401044 <+56>:	mov    0x8(%rsp),%edi
   0x0000000000401048 <+60>:	callq  0x400fce <func4>
   0x000000000040104d <+65>:	test   %eax,%eax
   0x000000000040104f <+67>:	jne    0x401058 <phase_4+76>
   0x0000000000401051 <+69>:	cmpl   $0x0,0xc(%rsp)
   0x0000000000401056 <+74>:	je     0x40105d <phase_4+81>
   0x0000000000401058 <+76>:	callq  0x40143a <explode_bomb>
   0x000000000040105d <+81>:	add    $0x18,%rsp
   0x0000000000401061 <+85>:	retq   
End of assembler dump.

同样的老套路,查看第14行的x /s 0x4025cf可以得到:

0x4025cf:	"%d %d"

本关的输入格式仍然是两个整数。

接着往后看,34行说明我们输入的第一个数必须小于等于0xe,46到56行再把0xe赋给edx、0x0赋给esi,(%rsp)+0x8的值赋给edi。最后调用func4函数。第67行又进行了判定,%eax即func4的返回值如果不是0,就跳转之后爆炸。

好了,本关的核心在于func4函数了,我们看一下它的代码:

Dump of assembler code for function func4:
   0x0000000000400fce <+0>:     sub    $0x8,%rsp
   0x0000000000400fd2 <+4>:     mov    %edx,%eax
   0x0000000000400fd4 <+6>:     sub    %esi,%eax
   0x0000000000400fd6 <+8>:     mov    %eax,%ecx
   0x0000000000400fd8 <+10>:	shr    $0x1f,%ecx
   0x0000000000400fdb <+13>:	add    %ecx,%eax
   0x0000000000400fdd <+15>:	sar    %eax
   0x0000000000400fdf <+17>:	lea    (%rax,%rsi,1),%ecx
   0x0000000000400fe2 <+20>:	cmp    %edi,%ecx
   0x0000000000400fe4 <+22>:	jle    0x400ff2 <func4+36>
   0x0000000000400fe6 <+24>:	lea    -0x1(%rcx),%edx
   0x0000000000400fe9 <+27>:	callq  0x400fce <func4>
   0x0000000000400fee <+32>:	add    %eax,%eax
   0x0000000000400ff0 <+34>:	jmp    0x401007 <func4+57>
   0x0000000000400ff2 <+36>:	mov    $0x0,%eax
   0x0000000000400ff7 <+41>:	cmp    %edi,%ecx
   0x0000000000400ff9 <+43>:	jge    0x401007 <func4+57>
   0x0000000000400ffb <+45>:	lea    0x1(%rcx),%esi
   0x0000000000400ffe <+48>:	callq  0x400fce <func4>
   0x0000000000401003 <+53>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401007 <+57>:	add    $0x8,%rsp
   0x000000000040100b <+61>:	retq   
End of assembler dump.

这段代码到没有什么好怎么分析的,一眼看上去显然是递归函数,我们直接逆向工程吧。(主要是因为我感觉递归函数你就这么直接去分析太麻烦了,还是写出函数跑测试方便)

// x in %edx, y in %esi, z in %edi
int fun(int x, int y, int z){
    int a = x - y;
    int t = a >> 31;
    t = ((a + t) >> 1) + y;
    if(b == x)
        return 0;
    if(b < x)
        return fun(x, t + 1, x) * 2 + 1;
    else
        return fun(t - 1, y, x) * 2;
}

遍历z为1~e之后,我们可以得到可能的值有0 1 3 7,再往后看:

   0x0000000000401051 <+69>:	cmpl   $0x0,0xc(%rsp)
   0x0000000000401056 <+74>:	je     0x40105d <phase_4+81>

查看一下栈就知道,这里是在拿我们输入的第二个数和0比较,若不是0,则炸弹爆炸。综合这两个结果,我们得到本关的答案。

(0, 0), (1, 0), (3, 0), (7, 0)

第五关:映射

仍然首先看代码:

Dump of assembler code for function phase_5:
   0x0000000000401062 <+0>:     push   %rbx
   0x0000000000401063 <+1>:     sub    $0x20,%rsp
   0x0000000000401067 <+5>:     mov    %rdi,%rbx
   0x000000000040106a <+8>:     mov    %fs:0x28,%rax
   0x0000000000401073 <+17>:	mov    %rax,0x18(%rsp)
   0x0000000000401078 <+22>:	xor    %eax,%eax
   0x000000000040107a <+24>:	callq  0x40131b <string_length>
   0x000000000040107f <+29>:	cmp    $0x6,%eax
   0x0000000000401082 <+32>:	je     0x4010d2 <phase_5+112>
   0x0000000000401084 <+34>:	callq  0x40143a <explode_bomb>

这里进行一个比较,如果字符串长度不是6,就引爆炸弹,很明显这是在说本关要求输入一个长度为6的字符串。

接下来看这一段:

   0x000000000040108b <+41>:	movzbl (%rbx,%rax,1),%ecx
   0x000000000040108f <+45>:	mov    %cl,(%rsp)
   0x0000000000401092 <+48>:	mov    (%rsp),%rdx
   0x0000000000401096 <+52>:	and    $0xf,%edx
   0x0000000000401099 <+55>:	movzbl 0x4024b0(%rdx),%edx
   0x00000000004010a0 <+62>:	mov    %dl,0x10(%rsp,%rax,1)
   0x00000000004010a4 <+66>:	add    $0x1,%rax
   0x00000000004010a8 <+70>:	cmp    $0x6,%rax
   0x00000000004010ac <+74>:	jne    0x40108b <phase_5+41>
   0x00000000004010ae <+76>:	movb   $0x0,0x16(%rsp)

首先查看第55行到底做了什么,执行x /s 0x4024b0,得到:

0x4024b0 <array.3449>:	"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

然后经过我们的尝试,发现执行x /s $rbx后得到的就是我们输入的字符,于是%rbx就是存储我们输入的寄存器。再看一看%rax的作用,后面增加了1,当小于6的时候回跳,说明这是一个索引,并且要执行六次循环,对应我们输入的长度为6的字符串。那么第一行就是每次把输入的一个字符赋值给%ecx,随后取其低字节。

于是,从(%rsp)+0x10开始的六个字节存放的分别是我们输入的六个字符的低字节+0x4024b0后得到的结果,换句话说,我们这六个字符的低字节就是起到索引的作用。最后,在(%rsp)+0x16的地方赋值0x0,构成字符串。

   0x00000000004010b3 <+81>:	mov    $0x40245e,%esi
   0x00000000004010b8 <+86>:	lea    0x10(%rsp),%rdi
   0x00000000004010bd <+91>:	callq  0x401338 <strings_not_equal>
   0x00000000004010c2 <+96>:	test   %eax,%eax
   0x00000000004010c4 <+98>:	je     0x4010d9 <phase_5+119>
   0x00000000004010c6 <+100>:	callq  0x40143a <explode_bomb>

随后注意到这里有一个strings_not_equal函数,那么要比较的地方在哪呢?

注意到给%esi赋值的那一行,执行x /s 0x40245e后,我们得到:

0x40245e:	"flyers"

这在上面那段字符串中的索引位置分别为:9 f e 5 6 7,对应的一个答案就是:

ionefg

第六关:链表

第六关开始难度就比较大了,涉及到一些数据结构的成分。

这一关的代码就非常长了,我们只能一点一点来看他在做什么。

Dump of assembler code for function phase_6:
   0x00000000004010f4 <+0>:     push   %r14
   0x00000000004010f6 <+2>:     push   %r13
   0x00000000004010f8 <+4>:     push   %r12
   0x00000000004010fa <+6>:     push   %rbp
   0x00000000004010fb <+7>:     push   %rbx
   0x00000000004010fc <+8>:     sub    $0x50,%rsp
   0x0000000000401100 <+12>:	mov    %rsp,%r13
   0x0000000000401103 <+15>:	mov    %rsp,%rsi
   0x0000000000401106 <+18>:	callq  0x40145c <read_six_numbers>

首先还是输入六个数字。

   0x000000000040110b <+23>:	mov    %rsp,%r14
   0x000000000040110e <+26>:	mov    $0x0,%r12d
   0x0000000000401114 <+32>:	mov    %r13,%rbp
   0x0000000000401117 <+35>:	mov    0x0(%r13),%eax
   0x000000000040111b <+39>:	sub    $0x1,%eax
   0x000000000040111e <+42>:	cmp    $0x5,%eax
   0x0000000000401121 <+45>:	jbe    0x401128 <phase_6+52>
   0x0000000000401123 <+47>:	callq  0x40143a <explode_bomb>
   0x0000000000401128 <+52>:	add    $0x1,%r12d
   0x000000000040112c <+56>:	cmp    $0x6,%r12d
   0x0000000000401130 <+60>:	je     0x401153 <phase_6+95>
   0x0000000000401132 <+62>:	mov    %r12d,%ebx
   0x0000000000401135 <+65>:	movslq %ebx,%rax
   0x0000000000401138 <+68>:	mov    (%rsp,%rax,4),%eax
   0x000000000040113b <+71>:	cmp    %eax,0x0(%rbp)
   0x000000000040113e <+74>:	jne    0x401145 <phase_6+81>
   0x0000000000401140 <+76>:	callq  0x40143a <explode_bomb>
   0x0000000000401145 <+81>:	add    $0x1,%ebx
   0x0000000000401148 <+84>:	cmp    $0x5,%ebx
   0x000000000040114b <+87>:	jle    0x401135 <phase_6+65>
   0x000000000040114d <+89>:	add    $0x4,%r13
   0x0000000000401151 <+93>:	jmp    0x401114 <phase_6+32>

接着我们可以看到在39和42行中,将减1之后的值和5比较,如果大于5就爆炸,这说明我们输入的数必须小于等于6。

从第52和56行可以看到%r12d显然是跳出循环的条件,这个循环执行了六次。

从65行到87行,这里是在不断用栈中的值和rbp比较,如果相同就爆炸,即后面的数都不能和前面的数相等。第89行将%r13增加了4,随后跳转之后赋值给了%rbp,实际就是%rbp变成了下一个数,之后在此进行内循环。

综上,这段代码就是说这六个数必须各不相同,换句话说,就是1-6的一个排列。

接着往后看:

   0x0000000000401153 <+95>:	lea    0x18(%rsp),%rsi
   0x0000000000401158 <+100>:	mov    %r14,%rax
   0x000000000040115b <+103>:	mov    $0x7,%ecx
   0x0000000000401160 <+108>:	mov    %ecx,%edx
   0x0000000000401162 <+110>:	sub    (%rax),%edx
   0x0000000000401164 <+112>:	mov    %edx,(%rax)
   0x0000000000401166 <+114>:	add    $0x4,%rax
   0x000000000040116a <+118>:	cmp    %rsi,%rax
   0x000000000040116d <+121>:	jne    0x401160 <phase_6+108>

这里很明显就是用7减去每个数,不断循环。最后得到的效果就是我们输入的这六个数,变成了7减去他们之后的值,我们接下来称他们为输入数组。

接下来就是本关的重点了:

   0x000000000040116f <+123>:	mov    $0x0,%esi
   0x0000000000401174 <+128>:	jmp    0x401197 <phase_6+163>
   0x0000000000401176 <+130>:	mov    0x8(%rdx),%rdx
   0x000000000040117a <+134>:	add    $0x1,%eax
   0x000000000040117d <+137>:	cmp    %ecx,%eax
   0x000000000040117f <+139>:	jne    0x401176 <phase_6+130>
   0x0000000000401181 <+141>:	jmp    0x401188 <phase_6+148>
   0x0000000000401183 <+143>:	mov    $0x6032d0,%edx
   0x0000000000401188 <+148>:	mov    %rdx,0x20(%rsp,%rsi,2)
   0x000000000040118d <+153>:	add    $0x4,%rsi
   0x0000000000401191 <+157>:	cmp    $0x18,%rsi
   0x0000000000401195 <+161>:	je     0x4011ab <phase_6+183>
   0x0000000000401197 <+163>:	mov    (%rsp,%rsi,1),%ecx
   0x000000000040119a <+166>:	cmp    $0x1,%ecx
   0x000000000040119d <+169>:	jle    0x401183 <phase_6+143>
   0x000000000040119f <+171>:	mov    $0x1,%eax
   0x00000000004011a4 <+176>:	mov    $0x6032d0,%edx
   0x00000000004011a9 <+181>:	jmp    0x401176 <phase_6+130>

这段代码到底要干什么呢?首先还是已有数据的突破口吧,看到143行有一个赋值地址的行为,我们看一下这个地址,执行x /12g 0x6032d0

0x6032d0 <node1>:	0x000000010000014c	0x00000000006032e0
0x6032e0 <node2>:	0x00000002000000a8	0x00000000006032f0
0x6032f0 <node3>:	0x000000030000039c	0x0000000000603300
0x603300 <node4>:	0x00000004000002b3	0x0000000000603310
0x603310 <node5>:	0x00000005000001dd	0x0000000000603320
0x603320 <node6>:	0x00000006000001bb	0x0000000000000000

突然惊喜,这不就是链表嘛,名字的node给了我们暗示,同时每个node的后半部分存储的地址值也相当明显,所以,这一关就是在链表上面做文章了。

好了,我们紧接着127行开始往下走,跳转到163行,这里开始去一个数赋值给%ecx并和1比较,是1的话是一个分支,其他情况是另一个分支,那么我们先去观察循环多的不是1的分支。

176行把链表第一个结点的地址赋值给%edx,紧接着跳转到130行。

   0x0000000000401176 <+130>:	mov    0x8(%rdx),%rdx
   0x000000000040117a <+134>:	add    $0x1,%eax
   0x000000000040117d <+137>:	cmp    %ecx,%eax
   0x000000000040117f <+139>:	jne    0x401176 <phase_6+130>

我们发现,这个语句mov 0x8(%rdx),%rdx就是典型的链表操作,指向下一个结点,p = p->next。而之后这个循环就是不断让指针向后移动到最后一个结点。这里我们可以不断打印各个寄存器的值来发现。

此后调到了148行,把这个结点赋值给了(%rsp)+0x20开始之后的位置。

经过这番循环,从(%rsp)+0x20开始之后存的就是按照输入数组的顺序存放的结点。

好了来看后面一部分代码:

   0x00000000004011ab <+183>:	mov    0x20(%rsp),%rbx
   0x00000000004011b0 <+188>:	lea    0x28(%rsp),%rax
   0x00000000004011b5 <+193>:	lea    0x50(%rsp),%rsi
   0x00000000004011ba <+198>:	mov    %rbx,%rcx
   0x00000000004011bd <+201>:	mov    (%rax),%rdx
   0x00000000004011c0 <+204>:	mov    %rdx,0x8(%rcx)
   0x00000000004011c4 <+208>:	add    $0x8,%rax
   0x00000000004011c8 <+212>:	cmp    %rsi,%rax
   0x00000000004011cb <+215>:	je     0x4011d2 <phase_6+222>
   0x00000000004011cd <+217>:	mov    %rdx,%rcx
   0x00000000004011d0 <+220>:	jmp    0x4011bd <phase_6+201>

首先分析开始三行的三个数据,(%rsp)+0x20是六个结点开始时的位置,(%rsp)+0x28是第二个结点的位置,(%rsp)+0x50是最后一个结点的位置。

接下来是一个循环,198-204行把后一个结点的地址放在了前一个结点的指针域内,然后208行和217行分别让两个指针都指向下一个结点。经过这样的一个循环之后,实际上就是把链表重新按照输入数组的顺序连接起来了。

接着是最后一部分:

   0x00000000004011d2 <+222>:	movq   $0x0,0x8(%rdx)
   0x00000000004011da <+230>:	mov    $0x5,%ebp
   0x00000000004011df <+235>:	mov    0x8(%rbx),%rax
   0x00000000004011e3 <+239>:	mov    (%rax),%eax
   0x00000000004011e5 <+241>:	cmp    %eax,(%rbx)
   0x00000000004011e7 <+243>:	jge    0x4011ee <phase_6+250>
   0x00000000004011e9 <+245>:	callq  0x40143a <explode_bomb>
   0x00000000004011ee <+250>:	mov    0x8(%rbx),%rbx
   0x00000000004011f2 <+254>:	sub    $0x1,%ebp
   0x00000000004011f5 <+257>:	jne    0x4011df <phase_6+235>

第230行和254行说明了%ebp是循环的计数器,一共5次循环。然后每次将后面一个数赋值给%rax,然后和前一个数%rbx比较,如果前面大的话就不会爆炸。不过,由于使用了%eax,注意比较的时候的低4字节。

因此,我们的结果就出来了,就是要在按照经过一系列变换之后的输入数组排序的链表的值是从大到小的。

于是,我们的答案是:

4 3 2 1 6 5

第一次做这个lab时,第六关做完了就直接结束了。然而联想起第六关结束之后的那句话:

/* Wow, they got it!  But isn't something... missing?  Perhaps
     * something they overlooked?  Mua ha ha ha ha! */

这句话里面肯定隐藏着一些东西,一开始没有想到到底是什么,在我查阅资料后发现,原来还有一个隐藏关!于是继续开始了拆炸弹游戏。

寻找隐藏关

隐藏关的开启肯定和之前一样,肯定还是一个函数,那么怎么找到这个函数呢?

利用objdump -t bomb查看一下所有的函数的入口地址,找到了这样一段:

00000000006031b0 g     O .data	0000000000000018              n34
0000000000603150 g     O .data	0000000000000018              n32
0000000000603e10 g       *ABS*	0000000000000000              _end
0000000000400c90 g     F .text	0000000000000000              _start
0000000000401242 g     F .text	0000000000000051              secret_phase
0000000000603768 g     O .bss	0000000000000008              infile
0000000000401660 g     F .text	000000000000002e              sigalrm_handler
0000000000401f91 g     F .text	0000000000000027              init_timeout
0000000000603740 g       *ABS*	0000000000000000              __bss_start
0000000000400da0 g     F .text	0000000000000137              main

这里出现了一个secret_phase,显然就是隐藏关的函数了。然后我们再执行objdump -d bomb > bomb.asmbomb.asm中找一找隐藏关函数所在的地点,结果发现,在函数phase_defused中有这么一个入口

00000000004015c4 <phase_defused>:
  ......
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	callq  401242 <secret_phase>
  401635:	bf 58 25 40 00       	mov    $0x402558,%edi
  ......

没错,这个函数就是在每一关通过之后执行的那个函数!让我们来分析一下:

为了方便说明,我们仍然使用gdb反汇编这个函数:

Dump of assembler code for function phase_defused:
   0x00000000004015c4 <+0>:     sub    $0x78,%rsp
   0x00000000004015c8 <+4>:     mov    %fs:0x28,%rax
   0x00000000004015d1 <+13>:	mov    %rax,0x68(%rsp)
   0x00000000004015d6 <+18>:	xor    %eax,%eax
   0x00000000004015d8 <+20>:	cmpl   $0x6,0x202181(%rip)        # 0x603760 <num_input_strings>
   0x00000000004015df <+27>:	jne    0x40163f <phase_defused+123>
   0x00000000004015e1 <+29>:	lea    0x10(%rsp),%r8
   0x00000000004015e6 <+34>:	lea    0xc(%rsp),%rcx
   0x00000000004015eb <+39>:	lea    0x8(%rsp),%rdx
   0x00000000004015f0 <+44>:	mov    $0x402619,%esi
   0x00000000004015f5 <+49>:	mov    $0x603870,%edi
   0x00000000004015fa <+54>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x00000000004015ff <+59>:	cmp    $0x3,%eax
   0x0000000000401602 <+62>:	jne    0x401635 <phase_defused+113>
   0x0000000000401604 <+64>:	mov    $0x402622,%esi
   0x0000000000401609 <+69>:	lea    0x10(%rsp),%rdi
   0x000000000040160e <+74>:	callq  0x401338 <strings_not_equal>
   0x0000000000401613 <+79>:	test   %eax,%eax
   0x0000000000401615 <+81>:	jne    0x401635 <phase_defused+113>
   0x0000000000401617 <+83>:	mov    $0x4024f8,%edi
   0x000000000040161c <+88>:	callq  0x400b10 <puts@plt>
   0x0000000000401621 <+93>:	mov    $0x402520,%edi
   0x0000000000401626 <+98>:	callq  0x400b10 <puts@plt>
   0x000000000040162b <+103>:	mov    $0x0,%eax
   0x0000000000401630 <+108>:	callq  0x401242 <secret_phase>
   0x0000000000401635 <+113>:	mov    $0x402558,%edi
   0x000000000040163a <+118>:	callq  0x400b10 <puts@plt>
   0x000000000040163f <+123>:	mov    0x68(%rsp),%rax
   0x0000000000401644 <+128>:	xor    %fs:0x28,%rax
   0x000000000040164d <+137>:	je     0x401654 <phase_defused+144>
   0x000000000040164f <+139>:	callq  0x400b30 <__stack_chk_fail@plt>
   0x0000000000401654 <+144>:	add    $0x78,%rsp
   0x0000000000401658 <+148>:	retq   

第20行后面的注释已经给了我们提示,这里只有在输入六次之后才会向后执行,也就是在第六关之后才有可能触发。再看看44和49行中的赋值到底是什么:

(gdb) x /s 0x402619
0x402619:	"%d %d %s"
(gdb) x /s 0x603870
0x603870 <input_strings+240>:	"0 0"

这个时候发现,这个0 0不正是我们第四关输入的字符嘛,这里我们可以更换第四关的答案,发现这里的字符串也变了,再结合前面的输入要求,我们就明白,这里就是要叫我们在第四关的答案后面多输入一个字符串。向后看到74行有一个strings_not_equal,这应该就是要求两个字符串相等,查看一下64行的字符串是什么,得到:

(gdb) x /s 0x402622
0x402622:	"DrEvil"

于是答案出来了,我们需要在第四关的答案后面加一个DrEvil ,就进入了隐藏关。

隐藏关:二叉树

隐藏关的难度就要更大一些了,本关考察了二叉树这种结构在汇编语言中的表示。

首先仍然是查看代码:

Dump of assembler code for function secret_phase:
   0x0000000000401242 <+0>:     push   %rbx
   0x0000000000401243 <+1>:     callq  0x40149e <read_line>
   0x0000000000401248 <+6>:     mov    $0xa,%edx
   0x000000000040124d <+11>:	mov    $0x0,%esi
   0x0000000000401252 <+16>:	mov    %rax,%rdi
   0x0000000000401255 <+19>:	callq  0x400bd0 <strtol@plt>
   0x000000000040125a <+24>:	mov    %rax,%rbx
   0x000000000040125d <+27>:	lea    -0x1(%rax),%eax
   0x0000000000401260 <+30>:	cmp    $0x3e8,%eax
   0x0000000000401265 <+35>:	jbe    0x40126c <secret_phase+42>
   0x0000000000401267 <+37>:	callq  0x40143a <explode_bomb>
   0x000000000040126c <+42>:	mov    %ebx,%esi
   0x000000000040126e <+44>:	mov    $0x6030f0,%edi
   0x0000000000401273 <+49>:	callq  0x401204 <fun7>
   0x0000000000401278 <+54>:	cmp    $0x2,%eax
   0x000000000040127b <+57>:	je     0x401282 <secret_phase+64>
   0x000000000040127d <+59>:	callq  0x40143a <explode_bomb>
   0x0000000000401282 <+64>:	mov    $0x402438,%edi
   0x0000000000401287 <+69>:	callq  0x400b10 <puts@plt>
   0x000000000040128c <+74>:	callq  0x4015c4 <phase_defused>
   0x0000000000401291 <+79>:	pop    %rbx
   0x0000000000401292 <+80>:	retq   
End of assembler dump.

至于输入什么,不知道,我们先随便输入一个数看看,打印寄存器的值,发现我们输入的值在%rax中,这里要求它减1小于等于0x3e8,也就是说我们输入的这个值要小于等于1001

49行调用了一个函数fun7,54行要求它的返回值是2,否则爆炸。那么本关的关键就在fun7函数内了。

Dump of assembler code for function fun7:
   0x0000000000401204 <+0>:     sub    $0x8,%rsp
   0x0000000000401208 <+4>:     test   %rdi,%rdi
   0x000000000040120b <+7>:     je     0x401238 <fun7+52>
   0x000000000040120d <+9>:     mov    (%rdi),%edx
   0x000000000040120f <+11>:	cmp    %esi,%edx
   0x0000000000401211 <+13>:	jle    0x401220 <fun7+28>
   0x0000000000401213 <+15>:	mov    0x8(%rdi),%rdi
   0x0000000000401217 <+19>:	callq  0x401204 <fun7>
   0x000000000040121c <+24>:	add    %eax,%eax
   0x000000000040121e <+26>:	jmp    0x40123d <fun7+57>
   0x0000000000401220 <+28>:	mov    $0x0,%eax
   0x0000000000401225 <+33>:	cmp    %esi,%edx
   0x0000000000401227 <+35>:	je     0x40123d <fun7+57>
   0x0000000000401229 <+37>:	mov    0x10(%rdi),%rdi
   0x000000000040122d <+41>:	callq  0x401204 <fun7>
   0x0000000000401232 <+46>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401236 <+50>:	jmp    0x40123d <fun7+57>
   0x0000000000401238 <+52>:	mov    $0xffffffff,%eax
   0x000000000040123d <+57>:	add    $0x8,%rsp
   0x0000000000401241 <+61>:	retq   
End of assembler dump.

这个函数的代码以外地短,可以看到是一个递归函数。打印发现我们输入的数在%esi中,这里每次拿(%rdi)在和我们的输入做比较,如果小了就(%rdi)+0x8,大了就(%rdi)+0x10紧接着是递归,那么问题就是(%rdi)中放的到底是什么。

secret_phase中的44行,有赋值给%rdi的操作,我们看一看这里面有啥:

(gdb) x /60g 0x6030f0
0x6030f0 <n1>:	0x0000000000000024	0x0000000000603110
0x603100 <n1+16>:	0x0000000000603130	0x0000000000000000
0x603110 <n21>:	0x0000000000000008	0x0000000000603190
0x603120 <n21+16>:	0x0000000000603150	0x0000000000000000
0x603130 <n22>:	0x0000000000000032	0x0000000000603170
0x603140 <n22+16>:	0x00000000006031b0	0x0000000000000000
0x603150 <n32>:	0x0000000000000016	0x0000000000603270
0x603160 <n32+16>:	0x0000000000603230	0x0000000000000000
0x603170 <n33>:	0x000000000000002d	0x00000000006031d0
0x603180 <n33+16>:	0x0000000000603290	0x0000000000000000
0x603190 <n31>:	0x0000000000000006	0x00000000006031f0
0x6031a0 <n31+16>:	0x0000000000603250	0x0000000000000000
0x6031b0 <n34>:	0x000000000000006b	0x0000000000603210
0x6031c0 <n34+16>:	0x00000000006032b0	0x0000000000000000
0x6031d0 <n45>:	0x0000000000000028	0x0000000000000000
0x6031e0 <n45+16>:	0x0000000000000000	0x0000000000000000
0x6031f0 <n41>:	0x0000000000000001	0x0000000000000000
0x603200 <n41+16>:	0x0000000000000000	0x0000000000000000
0x603210 <n47>:	0x0000000000000063	0x0000000000000000
0x603220 <n47+16>:	0x0000000000000000	0x0000000000000000
0x603230 <n44>:	0x0000000000000023	0x0000000000000000
0x603240 <n44+16>:	0x0000000000000000	0x0000000000000000
0x603250 <n42>:	0x0000000000000007	0x0000000000000000
0x603260 <n42+16>:	0x0000000000000000	0x0000000000000000
0x603270 <n43>:	0x0000000000000014	0x0000000000000000
0x603280 <n43+16>:	0x0000000000000000	0x0000000000000000
0x603290 <n46>:	0x000000000000002f	0x0000000000000000
0x6032a0 <n46+16>:	0x0000000000000000	0x0000000000000000
0x6032b0 <n48>:	0x00000000000003e9	0x0000000000000000
0x6032c0 <n48+16>:	0x0000000000000000	0x0000000000000000

首先名字从1,21,22,31,32,…,47,48是一个提示,然后每个结点三个域,后面两个明显存放的是另一个结点的地址,这不就是一个二叉树么。画出来大概是这样:

                         +-----+
                         | 3 6 |
                         +-----+
                            |
              +---------------------------+
              |                           |
          +-------+                   +-------+
          |   8   |                   |   50  |
          +-------+                   +-------+
          |       |                   |       |
    +-----+       +-----+       +-----+       +-----+
    |  6  |       |  22 |       |  45 |       | 107 |
    +-----+       +-----+       +-----+       +-----+
    |     |       |     |       |     |       |     |
 +----+ +----+ +----+ +----+ +----+ +----+ +----+ +----+
 |  1 | |  7 | | 20 | | 35 | | 40 | | 47 | | 63 | |1001|
 +----+ +----+ +----+ +----+ +----+ +----+ +----+ +----+

分析整个递归过程,不断拿我们输入的数和二叉树中一个结点的值比较,开始是根结点:

  • 如果两者相等:返回0
  • 如果我们的值小:以左子树递归执行,返回2 * rax
  • 如果我们的值大:以右子树递归执行,返回2 * rax + 1

经过检验,符合要求的答案有两个:

20, 22

总结

至此整个lab就结束了,整体来说这个lab偏简单,主要目的就是熟悉各种结构的汇编语言表示,做起来也非常有趣,亮点在于第六关和隐藏关的链表和二叉树结构,很好地告诉了我们一些数据结构是如何储存的。

另一方面也加强了我们的逆向工程能力,根据汇编语言去还原C语言代码,解析原程序。

下一个lab还是和汇编打交道,attack lab将会让我们亲自体验CSAPP第三章中讲到的栈溢出攻击,下周接着写博客。