下载实验用的文件们戳这里
bomblab的背景很有趣。Dr. Evil把“二进制炸弹”装在了教室的机子里。想要拆掉炸弹,你必须反编译“炸弹”,通过其中的汇编指令推测出可以拆掉炸弹的phrase。
好啦,我们看一看下载的文件夹里都有什么。bomb就是要我们去拆的“炸弹”;bomb.c是炸弹的源代码,但是最为关键的部分被删掉了,只保留了骨架。gdbnotes-x86-64是个重要的文件,里面有这个实验里用到的各种工具的使用方法。剩下的就是实验的各种说明了~
Phase 1
我们来看一看phase_1的反汇编是什么样子。一种方法是直接在终端里输入"objdump -d bomb > bomb.txt", 直接把objdump的输出重定向到bomb.txt里,之后就可以直接在记事本里面看反汇编了;或者,先在终端里输入"gdb bomb", 进到gdb里面;再输入"disas phase_1", 就能看到phase_1的反汇编了。我们这里用第一种方法,最终在记事本里看到的结果是下面这个样子:
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
那我们就先来捋顺这段汇编的逻辑吧。这段代码先把一个值,更准确地说,是字符串的地址(0x402400)放到%esi里,之后调用一个叫"strings_not_equal"的函数;最后判断这个函数的返回值:等于零,通过;不等于零,调用"explode_bomb"把炸弹炸掉(或许你会问%edi在哪里?其实%edi就是我们输入的字符串的地址)。显然,这短短的一段汇编里,最重要的就是对strings_not_equal函数的调用。至于这个函数是干什么的,猜也能猜得出来:判断两个字符串是不是不相等:相等,返回零;否则返回非零(仔细看strings_not_equal的实现,实际上不相等时返回1)(当然你也可以自己翻到0x401338,看一看这个推测是否正确。)。
现在让我们考虑一下strings_not_equal这个函数的两个参数。%rdi中的值,就是我们输入的字符串的地址;%rsi中的值是后面传进去的0x402400.那么0x402400指向的是什么字符串呢?我们打开gdb看一看。在终端中输入:
gdb bomb
x /s 0x402400
输出是什么呢?
0x402400: "Border relations with Canada have never been better."
只要我们的输入和上面这个字符串相同就行了。打开终端试一试~
(P.S. 这个字符串是2016年初更新的。它的出处是,2001年时任美国总统的乔治·布什,为欢迎加拿大总理访美所作的讲话。原句为"Border relations between Canada and Mexico have never been better. " 5年后,布什签署法案,授权在美墨边境修建隔离墙。2015年9月,特朗普宣布竞选美国总统,而其竞选承诺中有一条就是“在美墨边境修墙”。从这个细节,我们似乎可以一瞥作者对美墨边境相关政策的态度。)
Phase 2
前面那个Phase就当热身啦,接下来的几个Phase才是重头戏。还是刚才的办法,我们把phase_2的汇编也拿出来:
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
... ...
这段汇编先“读入“六个数(0x400f05:read_six_numbers)。从哪里“读入”呢?当然不是stdin。我们注意到,和上面那个Phase一样,%rdi并没有在调用这个函数之前出现。也就是说,phase_2函数的第一个参数,被原封不动地传到了read_six_numbers的第一个参数。那么%rsi,也就是第二个参数,代表的是什么呢?注意看这两行汇编:
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi
知道了吗?%rsi里放的是地址!结合read_six_numbers第一个参数的含义(恰好是我们输入的字符串)大胆猜想,我们可以知道—— 1. read_six_numbers从我们自己输进去的字符串里"read"; 2. %rsi在源代码里应该是个指针,这个指针指向一个数组的开头,而这个数组就是放read_six_numbers从字符串里抽出的六个数用的。不过我们还是来看一看read_six_numbers具体是怎么实现的:
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8
401480: be c3 25 40 00 mov $0x4025c3,%esi
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 retq
%rsi是什么?我们猜是数组首个元素的地址。0x4(%rsi)是什么?我们猜是数组第二个元素的地址。那么0x14(%rsi)、0x10(%rsi)等等呢?同理。以上这些都被read_six_numbers做成了sscanf的末几个参数(不是"scanf"!多了一个"s"!!!)(注意到了吗?有几个地址,因为寄存器放不下,被送到栈里面了)。sscanf的第一个参数,从汇编来看,是read_six_numbers的第一个参数,也就是我们输入的字符串;而第二个参数是存储在0x4025c3的字符串。和上面一样,我们来看一看0x4025c3里有什么。
0x4025c3: "%d %d %d %d %d %d"
觉得熟悉吗?事实上,sscanf的作用与scanf类似,只不过scanf从sdtin中读取数据,sscanf从它的第一个参数中读取数据。read_six_numbers就是借助sscanf,把我们输入的字符串中的数抽出来,放到一个数组里。我们的猜测是对的。
现在,我们知道了phase_2的要求是输入6个特定的数,数与数之间用空格隔开。那么这六个数又是什么呢?我们接着往下看。
... ...
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
... ...
我们已经知道了,%rsp里放的就是数组第一个元素的地址。所以说,很显然,这段汇编在判断我们输入的六个数,第一个数是不是等于一。判断了之后就跳到0x400f30:
... ...
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b>
... ...
嗯,这三行汇编把第二个数(int是四个字节)的地址放进%rbx里,把最后一个数下一个字节( ( 18 ) 16 = ( 24 ) 10 (18)_{16} = (24)_{10} (18)16=(24)10)的地址放进%rbp里(是判断循环终止条件用的),之后跳到0x400f17.
... ...
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
... ...
从0x400f17这里,程序就开始循环处理我们输进去的六个数了。首先,程序把%rbx指向的数的前一个放到%eax里,之后让它翻倍:
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
翻倍之后再检查%eax里的值是不是和%rbx当前指向的数相同:
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
如果相同呢,就跳到0x400f25;如果不相同,就调用explode_bomb引爆炸弹。看来,这好像是个等比数列!1为首项,2为公比!
我们最后再来看一看0x400f25那里的汇编是什么。
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
和我们想得一样,这里程序把%rbx里的指针后移四字节,再判断指针是否到达了数组末尾。
所以,要过这个phase,只要输入以一为首项,二为公比的等比数列前六项就行了。
Phase 3
这个phase考的是switch语句的汇编表示。
好啦,先看题吧。phase_3开头和上面那个read_six_numbers很像,也调用了sscanf,从我们输入的字符串里提取数字。只不过这次只有两个数,第一个放在0x8(%rsp)那里,第二个放在0xc(%rsp)那里。
仔细看汇编。我们发现,0x8(%rsp)和0xc(%rsp)这两个值,只在两个地方出现过。对于0x8(%rsp), 这个地方是:
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
而对于0xc(%rsp), 这个地方是:
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
在0x400f75这一行,代码究竟要跳转到哪里呢?应该是0x402470这个地址中储存的值(星号解引用,和指针一样),和8乘%rax里的值加在一起组成的地址。%rax里的值我们知道,就是我们输入的第一个数;那么0x402470这个地址中的值又是什么呢?还是像前面那样,我们在gdb中输入x /wx 0x402470,看一看输出:
(gdb) x /wx 0x402470
0x402470: 0x00400f7c
0x400f7c