Bomb Lab
简介
这是CMU15213课程的第二个实验,也是十分经典的一个实验,世界上用CSAPP当教科书的高校一般都会保留这个实验,实验要求是给一个用C语言编写的可执行文件bomb,你可以看到它主函数的C语言代码,除此之外,一概不知,实验分为六个阶段,每个阶段需要输入一串字符,有点像破译密码,如果六次输入的密码都是对的,炸弹解除,否则炸弹会爆炸(退出,并打印爆炸信息),如果是CMU的学生,每个人获得的炸弹是不一样的(高超的 反作弊技巧),每次爆炸会扣0.5分,扣满20分为止。不过我们作为自学党,没有这个限制,用的是自学用bomb,随便炸,根本不虚,不过为了达成良好的学习效果,还是尽可能减少爆炸次数。
C语言bomb源代码
1 |
|
解决方案
根据C语言的bomb源码,我们发现main函数提供了两种读取数据的形式,从文件读取所有数据,或者一行一行从标准输入,也就是键盘,读入数据,分为六个阶段,对应phase_1到phase_6这6个函数,不过很遗憾,我们所能看到的源码只有这些了,这些phase函数具体用来干嘛的,怎么写的,我们一概不知。
根据题目的要求以及提示,我们可以将bomb可执行文件反汇编,对汇编语言代码进行逆向分析,而gdb是我们调试汇编语言的有效工具。
gdb命令
就个人做这个lab的经验而言,这个lab本身就是一个利用gdb反汇编的一个实践操作,以下gdb的指令经常用到,值得牢记,记不住也没关系,就个人经验而言,每次忘了,都去查资料,查多了,也就记住了
命令 | 功能 |
---|---|
gdb filename | 开始调试 |
run | 开始运行 |
run 1 2 3 | 开始运行,并且传入参数1,2,3 |
kill | 停止运行 |
quit | 退出gdb |
break sum | 在sum函数的开头设置断点 |
break *0x8048c3 | 在0x8048c3的地址处设置断点 |
delete 1 | 删除断点1 |
clear sum | 删除在sum函数入口的断点 |
stepi | 运行一条指令 |
stepi 4 | 运行4条指令 |
continue | 运行到下一个断点 |
disas sum | 反汇编sum函数 |
disas 0X12345 | 反汇编入口在0x12345的函数 |
print /x /d /t $rax | 将rax里的内容以16进制,10进制,2进制的形式输出 |
print 0x888 | 输出0x888的十进制形式 |
print *(int*)0x123456 | 将0x123456地址所存储的内容以数字形式输出 |
print (char*)0x123456 | 输出存储在0x123456的字符串 |
x/w $rsp | 解析在rsp所指向位置的word |
x/2w $rsp | 解析在rsp所指向位置的两个word |
x/2wd $rsp | 解析在rsp所指向位置的word,以十进制形式输出 |
info registers | 寄存器信息 |
info functions | 函数信息 |
info stack | 栈信息 |
以上是我个人在整个实验过程中,用的比较频繁的命令,如果还有别的需要可以自行搜索文档或者根据以上命令进行推断,比如x/3w $rsp 就是连续输出三个word。
x86-64寄存器
牢记下图,事半功倍:
实验解析
phase_1
反汇编phase_11
2
3
4
5
6
7
80x0000000000400ee0 <+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
phase_1十分简单粗暴,调用了strings_not_equal函数直接判断两个字符串是否相等,第二个字符串起始地址直接存储在%esi里,作为第二个参数传入,观察第二行我们可以发现用来比较的字符串存在0x402400里,直接用(char*)0x402400解析,得到字符串“Border relations with Canada have never been better.”即为第一步正确答案。
phase_2
来到第二阶段,首先依然是反汇编1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
260x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers> ;读取6个数字,存入栈
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp)
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52> ;第一个数字必须是1
0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb>
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax;上一个数字的地址
0x0000000000400f1a <+30>: add %eax,%eax;上一个数字*2
0x0000000000400f1c <+32>: cmp %eax,(%rbx); an = 2 * an-1
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx
0x0000000000400f29 <+45>: cmp %rbp,%rbx
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx;读取下一个数字的地址
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp
;第六个数字的位置
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq
根据第五行,调用了read_six_numbers这个函数,显然这次要求的输入是6个数字,根据第八行和第九行,第一个数字必须是1,否则会爆炸,我们观察到如果第一个数字是1,跳转到+52,也就是lea 0x4(%rsp),%rbx,%rsp是栈指针的地址,每个int的数据长度为4个bytes,这句话的意思就是说读取下一个数字的地址,存入%rbx里。
(当然,read_six_numbers如果不放心,可以将其反汇编):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
170x000000000040145c <+0>: sub $0x18,%rsp
0x0000000000401460 <+4>: mov %rsi,%rdx
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9
0x000000000040147c <+32>: lea 0x8(%rsi),%r8
0x0000000000401480 <+36>: mov $0x4025c3,%esi; "%d %d %d %d %d %d"
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>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq
下一行有个奇怪的数值0x18,十进制为24,24/4=6正好是6个数字,这一行的目的就是设置一个结束点,放在%rbp中,然后回到+27.
仔细分析27行,不难发现,这段程序是在循环判断一个数组是否为公比为2的等比数列,如果不是则引爆炸弹,由于第一个数字是1,我们不难得出答案:
1 2 4 8 16 32
phase_3
第三阶段,老规矩先看看phase_3是个啥1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
360x0000000000400f43 <+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>;其实就是sscanf,第二个参数为格式字符串
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>; sscanf的返回值应该大于1,否则会触发炸弹
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>;第一个数如果大于7则触发炸弹
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
这段代码一上来调用了sscanf函数,通过查文档发现,这个函数是用来解析字符串里的数字,按照规定格式存到另一个字符串里,并返回所解析的数字的个数,第二个参数就是解析格式,print (char*)0x4025cf,发现格式字符串为“%d %d”,也就是两个int数字,根据+32我们也可以确认如果数字个数(返回值)<=1 也会引爆炸弹,直接gg,接下来+39读取第一个数字,联系下面几行不难得出,第一个数字必须<=7,否则也会引爆炸弹,再往下+50,发现跳转语句,根据第一个数字跳转到不同的行数,获取一个值放入%eax里,要求第二个数必须等于放入的值
这题多解,一个比较好的方法,是带入一个数字,然后逐步运行,看看跳转到哪个条件分支,改条件分支给了%eax什么样的数字,然后让第二个数字等于它即可,当然也可以直接算,以下皆为正确答案:1
2
3
4
5
6
7
80 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327
phase_4
反汇编phase_41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
220x40100c <phase_4> sub $0x18,%rsp
0x401010 <phase_4+4> lea 0xc(%rsp),%rcx # 存第二个数字
0x401015 <phase_4+9> lea 0x8(%rsp),%rdx # 存第一个数字
0x40101a <phase_4+14> mov $0x4025cf,%esi # "%d %d"
0x40101f <phase_4+19> mov $0x0,%eax
0x401024 <phase_4+24> callq 0x400bf0 <__isoc99_sscanf@plt>
0x401029 <phase_4+29> cmp $0x2,%eax
0x40102c <phase_4+32> jne 0x401035 <phase_4+41> # 必须是两个数字
0x40102e <phase_4+34> cmpl $0xe,0x8(%rsp)
0x401033 <phase_4+39> jbe 0x40103a <phase_4+46> # 第一个数字必须 <= 14, >= 0(无符号)
0x401035 <phase_4+41> callq 0x40143a <explode_bomb>
0x40103a <phase_4+46> mov $0xe,%edx # func4 第三个参数为 14
0x40103f <phase_4+51> mov $0x0,%esi # 第二个参数为 0
0x401044 <phase_4+56> mov 0x8(%rsp),%edi # 第一个参数为我们输入的第一个数字
0x401048 <phase_4+60> callq 0x400fce <func4>
0x40104d <phase_4+65> test %eax,%eax
0x40104f <phase_4+67> jne 0x401058 <phase_4+76> # 测试 func4 的返回值,如果不为零,则引爆炸弹
0x401051 <phase_4+69> cmpl $0x0,0xc(%rsp)
0x401056 <phase_4+74> je 0x40105d <phase_4+81> # 测试我们输入的第二个数字,如果为 0,则过关
0x401058 <phase_4+76> callq 0x40143a <explode_bomb>
0x40105d <phase_4+81> add $0x18,%rsp
0x401061 <phase_4+85> retq
这道题,也是要求读取两个数字,第一个数字要让func4返回0,第二个数字是0,反汇编func4
1 | ;这个函数有三个参数,不妨设置为val,i,j |
这个函数有点绕,大概是做一个二分,具体的内容写在注释里,自己可以用笔模拟下这个函数在干嘛,可以推断出C语言代码大致如下1
2
3
4
5
6
7
8
9int func4(int n, int i, int j) {
int val = (j + i) / 2;
if (val <= n) {
if (val >= n) return 0;
else return 2 * func4(n, val+1, j) + 1;
} else {
return 2 * func4(n, i, val-1);
}
}
共有四组可能的答案让这个函数返回0:1
2
3
40 0
1 0
3 0
7 0
这题还是有一定难度的
phase_5
……1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
380x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax ;防护代码,设置一个canary,可以不管它
0x0000000000401073 <+17>: mov %rax,0x18(%rsp)
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax;要求获取一个长度为6的字符串
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
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;某个地方存了个字符串,每次获取第%rdx个字符,%rdx从输入所获得的字符的后四位
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1);保存截取的字符
0x00000000004010a4 <+66>: add $0x1,%rax;每次读取一个字符,类似++i
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>;读取6次
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
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>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq
这个程序比较复杂,重要语句注释写在上面的代码里,意思是读取一个长度为6的字符串,对于每个字符截取后四位数字,用来作为index,获取另一个字符串里对应的字符,并保存起来,产生一个新的长度为6的字符串,要求等于另一个字符串。
解析可以看到,用来被截取的字符串str1为“maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”(我们发现作者在嘲讽我们:) ),用来对比的字符串str2为“flyers”,我们发现flyers6个字符分别出现在str1的第9位,第15位,第14位,第5位,第6位,第7位,输入的字符串后四位的二进制只要分别表示9,15,14,5,6,7即可,翻查ASCII表,可以得到结果:1
ionefg
这题更像密码破解了
phase_6
1 | 0x00000000004010f4 <+0>: push %r14 |
这是最后一道题,也是最难的一题,Prof在上课的时候也强调了hard for best students,仔细观察下这个题,我们发现,phase_6基本上还是在本函数内跳来跳去,一上来先要读取6个数字,要求满足一定的条件,跟之前所做过的2类似。
这个代码非常长,我们可以把它划分为几个部分,每个部分完成不同的功能,即不同的限制条件,第一部分是+23-+60,要求每个数小于等于6,第二部分+60-+95,是两个循环,用来判断数字两两之间不相等,第三部分是+108-128,将读来的6个数与7求差,再存入原来的地方,第四部分130-181,这个部分有一些复杂,相当于是在栈上存了些地址,可以发现+130 的代码为mov 0x8(%rdx),%rdx,这其实就是链表的形式,每次读取next地址,这段代码的意思是,用第三部分与7求出来的差值重新安排链表节点,1对应第一个节点,2对应第二个节点,依次类推;第五部分+183-257,用来判断重新构造出的链表里的数字满足严格递减。
我们利用gdb命令: x/24w 0x6032d0(每个节点占用了4个w)发现序号1-6的节点对应的数字为:332,168,924,691,477,413,为了使其严格递减,序号应该是:3,4,5,6,1,2,考虑到我们之前用7对传入的数字作差,为了得到这个序列,传入的数字应该是,4,3,2,1,6,5这就是我们最终的结果。
总结下,这个函数是传入一个互不相等的,由6个数组成的数组,然后每个数与7作差,根据作差的结果,每个数对应原来链表的第一个节点,重新排序一个链表,要求这个链表的数值降序排列
这一题核心在于分步处理,以及观察出那个链表的存在,这十分具有挑战性,我个人在这部分上卡了很久。
总结
CMU之所以是计算机排名第一的院校,果真有其道理,大牛愿意花时间精心设计作业,寓教于乐,从这个bomblab上可见一斑,每个bomb难度层层递增,cover了教材和课堂上所讲的重要内容,这个作业完成后,我对于汇编语言的了解与认知来到了一个新的层次,解决每个bomb都能给我带来一种成功解谜的成就感,希望中国以后也能出现这种愿意在教学和作业设计上花精力的大牛吧。