目录
前言
该实验是《深入理解计算机系统》(英文缩写CSAPP)课程附带实验——Lab2:Bomb Lab,对应书中第三章内容(程序的机器级表示),主函数有六个炸弹函数,先是获得输入input = read_line(); 接着运行 phase_1(input); 判断是否是正确答案,如果是正确答案,调用phase_defused(); 来获取接触炸弹的方式。
一.实验总说明
A “binary bomb” is a program provided to students as an object code file.When run, it prompts the user to type in 6 different strings. If any of these is incorrect, the bomb “explodes,” printing an error message and logging the event on a grading server. Students must “defuse” their own unique bomb by disassembling and reverse engineering the program to determine what the 6 strings should be. The lab teaches students to understand assembly language, and also forces them to learn how to use a debugger. It’s also great fun. A legendary lab among the CMU undergrads.
Here’s a Linux/x86-64 binary bomb that you can try out for yourself. The feature that notifies the grading server has been disabled, so feel free to explode this bomb with impunity. If you’re an instructor with a CS:APP account, then you can download the solution.
“二进制炸弹”是作为目标代码文件提供给学生的程序。当运行时,它提示用户输入6个不同的字符串。 如果其中任何一个是不正确的,炸弹“爆炸”,打印一个错误消息,并在分级服务器上记录事件。学生们必须通过拆卸和反向工程程序来“拆除”他们自己独特的炸弹,以确定6个字符串应该是什么。该实验室教学生理解汇编语言,并迫使他们学习如何使用调试器。这也很有趣。卡内基梅隆大学的传奇实验室。
这里有一个Linux/x86-64二进制炸弹,您可以自己尝试一下。 通知分级服务器的功能已经被禁用,所以可以自由引爆这个炸弹而不受惩罚。如果你是一个拥有CS:APP账户的教师,那么你可以下载解决方案。
二.实验过程记录
准备
- 本实验需要用到gdb调试器,在Lab1配置实验环境时已经安装过gdb调试器,命令:
> sudo apt-get install gdb
- gdb调试器的一些指令
r 运行程序
b <*0x某某某> 在某个地址设置断点,具体哪里,可以看反汇编的代码,可以根据那个直接复制粘贴设断点的
d 删除所有断点
d <断点号> 删除指定断点
info b 查看所有断点信息
continue 断点处继续执行
display < < <$寄存器 > > > 跟踪寄存器,碰到断点停下时会显示出所有跟踪的寄存器的当前值,非常好用的一个命令,注意的是gdb中表示寄存器的话前面用的不是百分符号%,而是美元符号$
x/参数 <地址> 访问地址的内存,其实就是间接访问,也是很好用的指令,关于参数,s是输出为字符串,d为输出为十进制,x为输出为十六进制,b、w、l、q控制输出字节,默认是w,四字节,s字符串不受这个控制除外。
info r 查看所有寄存器的值
print (可加强制转换符号)<数字> 跟C语言的基本性质一样的,理解即可
- 提前生成Bomb.c的反汇编文件,是后面需要用到的,只需生成一次 在终端打开bomb文件夹,输入
> objdump -d bomb > bomb.asm
就能看到 在bomb文件夹多出来了一个这样子的文件
或
> objdump -d mstore
可查看汇编代码
Phase 1 字符串比较
首先通过gdb载入bomb,在终端输入 gdb bomb
(如果不进入gdb调试模式,可直接用命令./bomb
运行可执行文件(在最后知道所有拆弹密码的时候可以用))
此时输入r
,运行bomb,然后输入任意字符串,看是否通过,效果如下:
很明显,炸弹炸了
下面通过反汇编文件bomb.asm进行分析
首先来到phase_1处
对汇编代码进行分析:
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp //把栈指针减少8,给局部变量提供空间
400ee4: be 00 24 40 00 mov $0x402400,%esi //向寄存器%esi存放0x402400(可以强制类型转换为一个字符串)
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal> /*调用函数 strings_not_equal,判断输入的字符串和程序
内置的字符串(即%esi中存放的数据)是否相同,相同则返回0 */
400eee: 85 c0 test %eax,%eax /*test指令同逻辑与and运算,但只设置条件码寄存器,不改变目的寄存器的值,
test %eax,%eax用于测试寄存器%eax是否为空,由于寄存器%rax一般存放函数
的返回值,此处应该存放的是函数 strings_not_equal的值,而%eax是%rax的低
32位表示,所以不难分析出,当%eax值为0时,test的两个操作数相同且都为0,条
件码ZF置位为1,即可满足下一行代码的跳转指令*/
400ef0: 74 05 je 400ef7 <phase_1+0x17> //test和je的组合用法,当ZF置位时,je跳转,跳过400ef2,直接到400ef7处
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb> //调用函数 explode_bomb,发生爆炸
400ef7: 48 83 c4 08 add $0x8,%rsp //栈指针加8,函数回收栈指针
400efb: c3 retq //返回
因此,0x402400中存放的字符串即拆弹密码
两种解码方式:
- 直接通过gdb调试查看(
print (char*)0x402400
)
- 因为此时已经经过了指令
400ee4: mov $0x402400,%esi
,在该指令的下一行设置断点,即可查看寄存器中存放的数据
先输入b *0x400ee9
设置断点,输入r
运行程序,然后随便输入一串字符串abcde,当程序运行到断点处自动停止 ,然后通过指令x/s $esi
看寄存器中存放数据,s即为所需字符串
输入d
,删除断点,验证密码是否正确:
Phase 1已拆除
Phase1拆弹密码:Border relations with Canada have never been better.
Phase 2 循环
对前几行代码进行分析
0000000000400efc <phase_2>:
400efc: 55 push %rbp //把数据压入栈
400efd: 53 push %rbx //把数据压入栈
400efe: 48 83 ec 28 sub $0x28,%rsp //把栈指针减少40,提供局部变量空间
//申请空间
400f02: 48 89 e6 mov %rsp,%rsi //把栈指针状态存入%rsi中
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> // 调用函数read_six_numbers,读取6个数字,此时(%rsp)已被赋值
调用函数read_six_numbers,读取6个数字,先尝试输入6个数字,发现:
根据函数read_six_numbers的地址40145c找到相应代码块进行分析
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp //栈指针减少24(6个int型数据)
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 //401460~40147c为把6个int型数据的地址存放在各个寄存器中
401480: be c3 25 40 00 mov $0x4025c3,%esi //类似Phase1向寄存器中转入数据(对应字符串)
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用scanf函数
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
发现命401480: mov $0x4025c3,%esi
,类似Phase 1向寄存器中转入数据(对应字符串),于是输入print (char*)0x4025c3
进行测试,发现
验证了输入6个整型数据的猜想
返回看Phase 2剩下的汇编代码:
0000000000400efc <phase_2>:
400efc: 55 push %rbp //把数据压入栈
400efd: 53 push %rbx //把数据压入栈
400efe: 48 83 ec 28 sub $0x28,%rsp //把栈指针减少40,提供局部变量空间
//申请空间
400f02: 48 89 e6 mov %rsp,%rsi //把栈指针状态存入%rsi中
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> // 调用函数read_six_numbers,读取6个数字,此时(%rsp)已被赋值
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) //(%rsp)的值即输入的第一个参数的值,将(%rsp)的值与1比较
400f0e: 74 20 je 400f30 <phase_2+0x34> //若相等则跳转至400f30
400f10: e8 25 05 00 00 callq 40143a <explode_bomb> //否则爆炸
400f15: eb 19 jmp 400f30 <phase_2+0x34> //jmp指令无条件跳转,直接跳转至400f30
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax //%eax的值*2
400f1c: 39 03 cmp %eax,(%rbx) //比较%eax的值和此时%rbx对应的内存的值
400f1e: 74 05 je 400f25 <phase_2+0x29> //若相等则跳转至400f25
400f20: e8 15 05 00 00 callq 40143a <explode_bomb> //否则爆炸
400f25: 48 83 c3 04 add $0x4,%rbx //%rbx的值(地址)加4
400f29: 48 39 eb cmp %rbp,%rbx //比较两个寄存器的值,判断是否比较完6个数
400f2c: 75 e9 jne 400f17 <phase_2+0x1b> //若不相等,则进行跳转至400f17,即循环没结束
400f2e: eb 0c jmp 400f3c <phase_2+0x40> //否则直接跳转至400f3c,循环结束标志
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx //将%rsp+4代表的地址移入%rbx,即第2个输入数的地址
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp //将%rsp+24代表的地址移入%rbp,即第6个输入数的后一块地址
400f3a: eb db jmp 400f17 <phase_2+0x1b> //直接跳转至400f17
//释放空间
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq
发现是一个循环操作:
首先,将 (%rsp)
的值与立即数$0x1
进行比较,由此可知,第一个输入数为1,后跳转至400f30,用lea
指令分别加载%rsp+4和%rsp+24
对应的地址到%rbx
和%rbp
,由int型数据所占字节大小,可知%rbx和%rbp分别存放第2个输入数的地址和第6个输入数的后一块的地址
然后跳转至400f17,此时(%rbx-4)
对应的值即(%rsp)
对应的值,将其存放值%eax
中,并将该值*2后与(%rbx)
对应的值(即第二个输入值)进行比较,即后一个数是前一个数的2倍,由此可知第二个输入值为2,后跳转至400f25,得到%rbx=%rbx+4
,与%rbp
进行比较(即当下%rbx对应的值(地址)是否为%rbp对应的值(地址)),若不相等则又跳转至400f17重复操作,若相等,则跳转至400f3c,结束循环,可知这是一个循环操作,看是否比较完6个数。
由上述可知,这6个数分别为1,2,4,8,16,32
上述循环中各寄存器对应的值为:
%rbx | %rbp | %eax |
---|---|---|
%rsp+4 | %rsp+24 | (%rsp)*2=2 |
%rsp+8 | (%rsp+4)*2=4 | |
%rsp+12 | (%rsp+8)*2=8 | |
%rsp+16 | (%rsp+12)*2=16 | |
%rsp+20 | (%rsp+16)*2=32 | |
%rsp+24 |
循环部分对应C代码:
int i;
int a[6];
a[0] = 1;
for(i=1;i<6;i++)
{
a[i] = a[i-1]*2;
}
终端验证:
Phase 2已拆除
Phase 2拆弹密码:1 2 4 8 16 32
这一关个人觉得困难的部分在于对寄存器及内存地址的理解,例如寄存器%rsp的值为一个地址(栈指针),而(%rsp)的值则为该地址所储存的值,为间接寻址的过程;又如寄存器%eax的值为一个数据值。
即一个寄存器的值可以是一个地址值(如本关中的%rsp,rbx,%rbp的值),也可以是一个数据值(如本关中的%eax的值),具体要看后续操作该寄存器时是否加( )。
理解了这个问题,后续只剩下循环操作,以C语言的角度入手,很快就迎刃而解了。
Phase 3 条件/分支
对汇编代码进行分析:
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp //给局部变量腾出空间
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx //加载地址,将0xc(%rsp)设为num2
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx //加载地址,将0x8(%rsp)设为num1
400f51: be cf 25 40 00 mov $0x4025cf,%esi //因为后续调用了函数scanf,此处需对输入格式进行测试
//经测试,0x4025cf对应字符串"%d %d"
//因此可以猜测开头%rcx存放的为输入的第一个数据(设为num1)的地址,%rdx存放的为输入的第二个数据(设为num2)的地址
400f56: b8 00 00 00 00 mov $0x0,%eax //初始化%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用scanf函数,此时%eax放scanf函数的返回值(输入数据的个数)
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27> //jg:有符号大于则跳转,说明scanf输入数据的个数必须大于1
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb> //否则爆炸
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a> //ja:无符号大于则跳转,至爆炸,说明num1为无符号数,大于0且需要小于等于7,所以num1=[0,7]
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax //将num1储存到%eax中
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) //间接跳转,(此处%rax不完全等于%eax,由num1=1(%rax=1)时的跳转地址可推论)
//0x402470+%rax*8的计算结果作为地址,跳转到该地址继续执行
//根据该指令和后续的指令格式,很容易判断此处是switch语句的跳转表,跳转表的首地址为0x402470
//以 *0x402470 处的值为基地址,再加上8 * %rax 进行跳转,不同的 %rax 跳转到不同的位置。
400f7c: b8 cf 00 00 00 mov $0xcf,%eax //case 0 %eax=0xcf=207
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax //case 2 %eax=0x2c3=707
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax //case 3 %eax=0x100=256
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax //case 4 %eax=0x185=389
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax //case 5 %eax=0xce=206
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax //case 6 %eax=0x2aa=682
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax //case 7 %eax=0x147=327
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax //case 1 %eax=0x137=311
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax //比较%eax的值和num2
400fc2: 74 05 je 400fc9 <phase_3+0x86> //je:若相等则跳转至结束
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb> //否则爆炸
400fc9: 48 83 c4 18 add $0x18,%rsp //释放空间
400fcd: c3 retq
可以通过x
命令(因为上面判断num1小于等于7,因此可知跳转表中应该存储有8个地址。x表明以十六进制的形式显示地址,g表示每8个字节的内存,因为这是x64平台,所以地址占8个字节。
)
查看跳转表中储存的地址:
仔细观察这些地址,可以发现都是函数phase_3范围内的地址。
当num1等于0时,跳转到0x0000000000400f7c处执行。如果num2不等于0xcf,则触发炸弹。
当num1等于1时,跳转到0x0000000000400fb9处执行。如果num2不等于0x137,则触发炸弹。
当num1等于2时,跳转到0x0000000000400f83处执行。如果num2不等于0x2c3,则触发炸弹。
当num1等于3时,跳转到0x0000000000400f8a处执行。如果num2不等于0x100,则触发炸弹。
当num1等于4时,跳转到0x0000000000400f91处执行。如果num2不等于0x185,则触发炸弹。
当num1等于5时,跳转到0x0000000000400f98处执行。如果num2不等于0xce,则触发炸弹。
当num1等于6时,跳转到0x0000000000400f9f处执行。如果num2不等于0x2aa,则触发炸弹。
当num1等于7时,跳转到0x0000000000400fa6处执行。如果num2不等于0x147,则触发炸弹。
所以拆弹密码有8种:0 207;1 311;2 707;3 256;4 389;5 206;6 682;7 327
输入其中任意几种或者全部输入都可以。
对应C代码:
void phase_3(const char *input)
{
// 0x8(%rsp) 0xc(%rsp)
int num1, num2;
// %rdi %rsi %rdx %rcx
int result = sscnaf(input, "%d %d", &num1, &num2); //返回输入数据的个数
if (result <= 1) {
explode_bomb();
}
switch (num1) {
case 0: // 0 207
if (num2 != 0xcf) {
explode_bomb();
}
break;
case 1: // 1 311
if (num2 != 0x137) {
explode_bomb();
}
break;
case 2: // 2 707
if (num2 != 0x2c3) {
explode_bomb();
}
break;
case 3: // 3 256
if (num2 != 0x100) {
explode_bomb();
}
break;
case 4: // 4 389
if (num2 != 0x185) {
explode_bomb();
}
break;
case 5: // 5 206
if (num2 != 0xce) {
explode_bomb();
}
break;
case 6: // 6 682
if (num2 != 0x2aa) {
explode_bomb();
}
break;
case 7: // 7 327
if (num2 != 0x147) {
explode_bomb();
}
break;
default:
explode_bomb();
break;
}
}
终端验证:
Phase 3已拆除
Phase 3拆弹密码:0 207;1 311;2 707;3 256;4 389;5 206;6 682;7 327 (任选其一)
Phase 4 递归调用和栈
对汇编代码进行分析:
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp //申请空间
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx //传参,加载有效地址,将0xc(%rsp)设为num2
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx //传参,加载有效地址,将0x8(%rsp)设为num1
40101a: be cf 25 40 00 mov $0x4025cf,%esi //scanf函数输入格式, %d %d
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用scanf函数
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29> //当scanf输入数据个数不等于2时,跳转至爆炸
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e> //jdb:无符号小于等于跳转,当num1小于等于14时,跳转至40103a,否则爆炸,所以num1的限制条件为[0,14]
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx //%edx=14
40103f: be 00 00 00 00 mov $0x0,%esi //%esi=0
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi //%edi=num1
401048: e8 81 ff ff ff callq 400fce <func4> //调用函数fun4,三个参数%edx,%esi,%edi
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c> //如果fun4返回值%eax不等于0,则跳转至爆炸,所以需要知道当num1为何值时,%eax为0
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51> //如果num2=0,则跳转至结束,否则爆炸,所以输入的第二个数据只能为0
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp //释放空间
401061: c3 retq
由
401051: cmpl $0x0,0xc(%rsp)
401056: je 40105d <phase_4+0x51>
40104d: test %eax,%eax
40104f: jne 401058 <phase_4+0x4c>
可知解码的条件为:
- num1满足函数func4的返回值%eax为0
- num2唯一值为0
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp //申请空间
//"="后为初始计算值
400fd2: 89 d0 mov %edx,%eax //%eax=%edx=14
400fd4: 29 f0 sub %esi,%eax //%eax=%eax-%esi=14-0=14
400fd6: 89 c1 mov %eax,%ecx //%ecx=%eax=14
400fd8: c1 e9 1f shr $0x1f,%ecx //%ecx的值逻辑右移31位,即%ecx=%ecx>>31=1110>>31=14/2^31=0
400fdb: 01 c8 add %ecx,%eax //因为是逻辑右移,也就是如果%eax为正数则不变,为负数则+1;%eax=%eax+%ecx=14+0=14
400fdd: d1 f8 sar %eax //算术右移1位,%eax=%eax/2=14/2=7
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx //%eax为%rax的第32位表示,%esi为%rsi的低32位表示,初始时%rax=%eax,%rsi=%esi
//加载有效地址,%ecx=%rax+%rsi=7+0=7
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24> //有符号小于等于则跳转, 若%ecx(初始为7)小于等于num1,则跳转至400ff2,%eax=0,说明num1=7,为其中一个解
400fe6: 8d 51 ff lea -0x1(%rcx),%edx //当num1小于等于%ecx时,加载有效地址,%edx=%rcx-1=7-1=6 (%ecx为%rcx的低32位表示)
400fe9: e8 e0 ff ff ff callq 400fce <func4> //递归调用,减小%ecx的值,直至num1>%ecx,因为num1是无符号数,取值范围[0,14],所以%ecx下限为0
400fee: 01 c0 add %eax,%eax //%eax=%eax*2
400ff0: eb 15 jmp 401007 <func4+0x39> //递归出口
400ff2: b8 00 00 00 00 mov $0x0,%eax //%eax=0
400ff7: 39 f9 cmp %edi,%ecx //若%ecx大于等于num1,递归结束,返回phase_4
400ff9: 7d 0c jge 401007 <func4+0x39> //递归出口
400ffb: 8d 71 01 lea 0x1(%rcx),%esi //%esi=%rcx+1
400ffe: e8 cb ff ff ff callq 400fce <func4> //递归调用
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax //%eax=%rax+1+%rax
401007: 48 83 c4 08 add $0x8,%rsp //释放空间
40100b: c3 retq
函数func4对应C代码:
//x: %edi y:%esi z:%edx k: %ecx t:%eax
int func4(int x,int y,int z)
{//x in %rdi,y in %rsi,z in %rdx,t in %rax,k in %ecx
//y的初始值为0,z的初始值为14
int t=z-y;
int k=t>>31;
t=(t+k)>>1;
k=t+y;
if(k>x)
{
z=k-1;
func4(x,y,z);
t=2t;
return t;
}
else
{
t=0;
if(k<x)
{
y=k+1;
func4(x,y,z);
t=2*t+1;
return t;
}
else
{
return t; //显然,要使返回值t(%eax)为0,其中一个答案为x=k=7
}
}
}
由上述分析可知其中一解为7 0
终端验证:
其余解因为倒推有难度,故将num1∈[0,14](无符号整数)逐一列举代入func4进行验证,得到可行解num1=0,1,3,7
终端验证:
Phase 4已拆除
Phase 4拆弹密码:0 0;1 0;3 0;7 0(任选其一)
Phase 5 指针
对汇编代码进行分析:
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
//申请空间
401067: 48 89 fb mov %rdi,%rbx //%rbx存放着输入的字符串的地址
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp) //40106a~401073为把fs段偏移0x28的一个数据储存到%rsp+0x18处,这是为了防止缓存区溢出。(与金丝雀值有关)
//(物理地址=段地址*16+偏移)
401078: 31 c0 xor %eax,%eax //自己与自己异或,清零
40107a: e8 9c 02 00 00 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb> //40107a~401084为比较我们输入的字符串长度是否为6,否则爆炸。
//说明此题要求输入一个长度为6的字符串。接下来跳到0x4010d2处的代码
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx //第一轮循环时,%rbx中存放着输入的字符串的地址,此时%rax=0x0,因此%ecx就存放着字符串的第一个字符,每次循环%rax+1
//设六个字符分别为ch[0],ch[1],ch[2],ch[3],ch[4],ch[5],则每轮循环到这里,%ecx=ch[%rax]
40108f: 88 0c 24 mov %cl,(%rsp) //%cl是%ecx的低八位
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx //%edx是%rdx的低32位,40108b~401096目的为只取(%ecx)的最低四位,存放到%edx中
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx //用命令(gdb)x/s 0x4024b0查看发现0x4024b0 对应字符串:
// maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?
(%rdx)在这里起到了索引的作用,比如(%rdx)=0x1,就是将a字符传给%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) //%dl是%edx的低8位,将上一句的得到的字符传入栈中保存,(%rax)同样作为栈的索引,第一个字符就储存在(%rsp+0x10)
//退出循环时,栈上从(%rsp+0x10)开始按顺序存储着6个索引到的字符
4010a4: 48 83 c0 01 add $0x1,%rax //%rax 每次循环后+1
4010a8: 48 83 f8 06 cmp $0x6,%rax //直到=6时退出循环
4010ac: 75 dd jne 40108b <phase_5+0x29> //40108b~4010ac为一个循环,%eax在跳转前已被清空
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)
4010b3: be 5e 24 40 00 mov $0x40245e,%esi //查看0x40245e位置内容为"flyers"
//接下来就是调用strings_not_equal函数,判断栈上的六个字符与这6个字符是否相等。操作与phase_1相同。
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax //清空%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29> //跳转至40108b
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
//释放空间
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq
由上述分析,可知这一关通过取我们输入六个字符的ASCII码的低四位作为索引值,查找maduiersnfotvbyl里的字符组成的,最后返回的字符应该是flyers。
maduiersnfotvbyl中f为第9位,l为第15位,y第14位,e第5位,r第6位,s第7位
即我们需要输入6个字符,使它们ASCII码低四位分别是:1001, 1111, 1110, 0101, 0110, 0111
查看ASCII表可找到对应字符,a的ASCII码为01100001,因此,其中一种解码可为ionuvw;ionefg;9?>567(答案不唯一)
终端验证:
Phase 5已拆除
Phase 5拆弹密码:ionuvw;ionefg;9?>567 … (不唯一)
Phase 6 链表/指针/结构
分几部分对汇编代码进行分析:
- 第一部分:
00000000004010f4 <phase_6>:
//第一部分:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
4010fc: 48 83 ec 50 sub $0x50,%rsp //申请空间
401100: 49 89 e5 mov %rsp,%r13 //%r13=%rsp
401103: 48 89 e6 mov %rsp,%rsi //%rsi=%rsp
//4010f4~401103为保存参数,分配栈帧
401106: e8 51 03 00 00 callq <read_six_numbers> //输入6个数,调用的结果是调用者的栈上按顺序存储输入的6个数
40110b: 49 89 e6 mov %rsp,%r14 //%r14=%rsp
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d //%r12d=0 %r12d当做数组索引,类似i=0
401114: 4c 89 ed mov %r13,%rbp //初始 %rbp=%r13=%rsp
401117: 41 8b 45 00 mov 0x0(%r13),%eax //%eax=num[i]
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34> //无符号数比较,说明num为无符号数,即大于等于0,40111b~401121 num[i]-1<=5,所以num[i]<=6
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx //401128~401132 退出大循环的条件:6个数字全部遍历到
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41> //401145~40114b 小循环,判断数组元素是否相等
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>// 40114d~401151 大循环,每次将%r13加4,之后回到401114,%r13赋给了%eax
以上部分对应C代码:
r14 = 0;
r13 = 0;
r12d = 0;
while(1){
rbp = r13;
if(num[r13] - 1 > 5)
goto bomb;
r12d++;
if(r12d == 6)
break;
for(ebx = r12d; ebx <= 5; ebx++){
if(num[ebx] == num[rbp])
goto bomb;
}
r13++;
}
这一部分有两层循环,说明输入的每个数字要求不大于6,且互不相同
- 第二部分
//第二部分
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi //0x18=24,刚好为6个int型数据所占字节,将 %rsi 指向栈中跳过读入数据位置作为结束标记
401158: 4c 89 f0 mov %r14,%rax //%rax=%r14=%rsp (%rax)存放输入数
40115b: b9 07 00 00 00 mov $0x7,%ecx //%ecx=7
401160: 89 ca mov %ecx,%edx //%edx=%ecx=7
401162: 2b 10 sub (%rax),%edx //7-(%rax)=7-(%r14) 立即数7减去 %r14 指向的数据
401164: 89 10 mov %edx,(%rax) //将7减的结果存回 %r14 执行的内存单元
401166: 48 83 c0 04 add $0x4,%rax // %rax 指向下一个输入数
40116a: 48 39 f0 cmp %rsi,%rax // 比较是否达到输入数组的末尾
40116d: 75 f1 jne 401160 <phase_6+0x6c>
以上部分对应C代码:
rsi=7;
for(rax = 0; rax != rsi; rax++)
{
num[rax] = 7 - num[rax];
}
这一部分的作用为使用立即数7减去每个输入数据,覆盖原来的数据
- 第三部分
401183中有一个数据:0x6032d0,先通过gdb调试查看:
发现最后8字节数字每次都加了16字节,类似通过指针访问下一结点,并且可以通过前面的node1、node2、node3知道这是一个链表的结点
然后访问6304480,即node1的指针
发现这个指针指向的是下一个结点 node2,类似地如果访问6304496 得到的会是node3和后续结点
由此可以推断出: 前面的 332、168、924是结点数据, 1 2 3是结点编号,最后8字节是next指针
该链表每个结点的结构为:
struct node{
int value;
int number;
node* next;
}
//第三部分
40116f: be 00 00 00 00 mov $0x0,%esi //将 %rsi 置0
401174: eb 21 jmp 401197 <phase_6+0xa3> //跳转至401197
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx //将 0x8(%rdx) 指向内存单元的内容(即下一结点的指针值)复制到 %rdx, 指向链表下一个元素
40117a: 83 c0 01 add $0x1,%eax //将 %eax 加1
40117d: 39 c8 cmp %ecx,%eax //比较 %ecx 和 %eax 是否相等
40117f: 75 f5 jne 401176 <phase_6+0x82> //不相等,继续遍历链表 【【最终 %rdx 指向链表的第 %ecx 个节点】】
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx //重置链表首地址,%edx存放链表首结点地址
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) /(%rsp+32+%rsi*2)=%rdx
40118d: 48 83 c6 04 add $0x4,%rsi //%rsi=%rsi+4
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7> //当%rsi=24时,跳转至4011ab
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx //将 (%rsp + %rsi) 指向的数据复制到 %ecx,%ecx存放输入数据
40119a: 83 f9 01 cmp $0x1,%ecx //比较 %ecx 是否小于等于1
40119d: 7e e4 jle 401183 <phase_6+0x8f> //若%ecx小于等于1,跳转(因为%ecx代表结点,结点标号从1开始,所以输入数据的范围为[1,6])
//即%ecx=1时,%edx存放链表首结点地址
40119f: b8 01 00 00 00 mov $0x1,%eax //若%ecx>1, 则%eax=1
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx //%edx存放链表首结点地址
4011a9: eb cb jmp 401176 <phase_6+0x82>
该循环根据输入数将链表中对应的第输入数个结点的地址复制到 0x20(%rsp)
开始的栈中
- 第四部分
//第四部分
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx //将(%rsp+32)的链表节点地址复制到 %rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax //将 %rax 指向栈中下一个链表结点的地址(%rsp+40)
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi //将 %rsi 指向保存的链表节点地址的末尾(%rsp+80)
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) //将栈中指向的后一个节点的地址复制到前一个节点的next指针位置
4011c4: 48 83 c0 08 add $0x8,%rax //移动到下一个节点
4011c8: 48 39 f0 cmp %rsi,%rax //判断6个节点是否遍历完毕
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx //继续遍历
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) //末尾链表next 为 NULL 则设置为0x0
//该循环按照7减去输入数据的索引重新调整链表
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax //将 %rax 指向 %rbx 下一个链表结点
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx) //比较链表结点中第一个字段值的大小,如果前一个节点值大于后一个节点值,跳转
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx //将 %rbx 向后移动,指向栈中下一个链表节点的地址
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb> //判断循环是否结束
//该循环判断栈中重新调整后的链表结点是否按照降序排列
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r13 //释放空间
401203: c3 retq
因为第四部分要求:链表第一项数据 > 第二项数据 >·····
我们根据gdb调试 看地址0x6032d0
得node[i].value排序为:node[3]>node[4]>node[5]>node[6]>node[1]>node[2]
又因为这个顺序,是经过了numx = 0x7 - numx 则原输入数据应该是4 3 2 1 6 5
终端验证:
Phase 6已拆除
Phase 6拆弹密码:4 3 2 1 6 5
Secret Phase 二叉树
在bomb.c中,最后有一段注释:
说明这个炸弹之中还有一个隐藏关卡。
- 寻找进入secret_phase 的入口
在bomb.asm中发现了如下汇编代码:
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line>
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff callq 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq
401293: 90 nop
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop
于是,需要寻找 secret_phase 的入口,即哪个函数调用了secret_phase,在bomb.asm中搜索发现
00000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal>
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
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
40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq
401659: 90 nop
40165a: 90 nop
40165b: 90 nop
40165c: 90 nop
40165d: 90 nop
40165e: 90 nop
40165f: 90 nop
phase_defused调用了secret_phase,而bomb.c中每个phase后面都用到了phase_defused
- 对phase_defused的汇编代码进行分析
上半部分:
00000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>
//num_input_strings 表示我们已经输入了多少串字符串了,判断是否等于6,
//如果不等于6,直接跳转到最下方,则secret_phase无法进入
//所以进入secret_phase的则先决条件是:完成phase 1 - 6
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用sscanf函数
4015ff: 83 f8 03 cmp $0x3,%eax //判断返回值%eax是否等于3
401602: 75 31 jne 401635 <phase_defused+0x71> //如果返回值%eax不等于3的话,则跳转到最下方,跳过了401630 callq 401242 <secret_phase>
//即secret_phase无法进入,所以必须要让sscanf函数的返回值为3
sscanf函数原理(sscanf函数用法详解)为每次读取都是对指定字符串,作格式化读取,如:
char buf[512] = ;
sscanf("123456 ", "%s", buf);
printf("%s\n", buf);
结果为:123456
而在调用前的mov进寄存器的参数,有两条mov的语句
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
根据原理,能够猜测出,一个是指定的字符串,一个格式化读取字符串
调试查看这两个参数具体对应什么字符串:
先输入6个之前我们完成的字符串,并于0x4015fa断点
最后得到格式化字符串 %d %d %s ,指定字符串7 0
而7 0就是phase 4的解码,联系sscanf函数的返回值%eax需要等于3,可以猜想需要在7 0 后面再输入一串字符串,即可进入隐藏关卡
对剩余部分的汇编代码分析:
401604: be 22 26 40 00 mov $0x402622,%esi //%esi=0x402622
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal> //调用字符串比较函数,判断输入的字符串和%esi中存的字符串是否相等
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71> //若相等,则跳转至401635
//(gdb) print (char*) 0x402622 得到字符串 DrEvil
//以下的代码不影响解码,暂且不做分析
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
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
40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq
401659: 90 nop
40165a: 90 nop
40165b: 90 nop
40165c: 90 nop
40165d: 90 nop
40165e: 90 nop
40165f: 90 nop
可得进入隐藏关卡的条件为 在第四关的解码7 0 后面加上字符串DrEvil
可使用命令 touch answers.txt
新建名为answers的.txt文件,将所有拆弹密码写入该文件,然后用命令 ./bomb answers.txt
运行bomb可执行文件 (之前的每一关调试结束后,也可以将新的拆弹密码写入.txt文件,用这个方法验证是否爆炸,就不用每次重复输入之前关卡的拆弹密码了)
终端验证:
然后回到secret_phase的源码进行分析
- 对secret_phase的汇编代码进行分析
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line> //调用read_line函数,读取字符串
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt> //调用strtol函数,将字符串转换为整型数据num,存在%rax中
40125a: 48 89 c3 mov %rax,%rbx //%rbx=%rax=num
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a> //num-1>1000(0x3e8),则会爆炸,所以输入的数字必须小于等于1001
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi //%esi=%ebx=num %esi存放输入的数据num,作为参数代入fun7
40126e: bf f0 30 60 00 mov $0x6030f0,%edi //%edi=6030f0 作为参数代入fun7
401273: e8 8c ff ff ff callq 401204 <fun7> //调用函数fun7
401278: 83 f8 02 cmp $0x2,%eax //将fun7的返回值%eax与2比较
//因为fun7为调用phase_defusd之前最后调用的一个函数,【【所以如果%eax=2,则跳过炸弹,拆弹成功!】】
//所以需要对fun7进行分析
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq
401293: 90 nop //nop 方便指令读取,不影响分析
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop
因为fun7为调用phase_defusd之前最后调用的一个函数,所以如果fun7的返回值 %eax=2,则跳过炸弹,拆弹成功,所以需要对fun7进行分析
- 对fun7的汇编代码进行分析
首先查看0x6030f0存放的数据
发现是一个跟phase 6类似的结构体, 6304xxx应该为指针,而且意外发现phase 6的指针数组就在下方
这里其实是一个带着两个指针的结构体,前面的7个结构体的两个指针都是带有值的,指向其他的结构体,最后的8个结构体指针是不带有值的,仅仅有头部数据
指针所指的数据结构是二叉树
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34> //返回空指针-1
40120d: 8b 17 mov (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx //%esi=num,%edx=(%edi) 比较输入数和指针所指的值
401211: 7e 0d jle 401220 <fun7+0x1c> //如果小于等于,则跳转至401220
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi //如果大于
401217: e8 e8 ff ff ff callq 401204 <fun7> //递归调用fun7
40121c: 01 c0 add %eax,%eax //%eax=2*%eax
40121e: eb 1d jmp 40123d <fun7+0x39> //return
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39> //如果输入数和当前指针所指的值相等,则返回0(递归出口)
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi //%rdi=(%rdi+16)
40122d: e8 d2 ff ff ff callq 401204 <fun7> //递归调用fun7
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax //%eax=2*%eax+1
401236: eb 05 jmp 40123d <fun7+0x39> //return
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 retq
fun7对应C代码:
int func7(Type *p, int input)
{
if(p == NULL)
return -1;
if(&p <= input)
{
if(&p == input)
return 0;
else
{
p = p + 0x10;
int n = func7(p, input);
return 2 * n + 1;
}
}
else
{
p = p + 0x8;
int n = func7(p, input);
return 2 * n;
}
}
需要得到返回值 %eax=2,说明递归顺序为:
- 最底层得到
0
return 0
- 向上经过一层
%eax = %eax*2 + 1
得到1return 1
- 再向上经过一层
% eax = %eax*2
得到2return 2
p所指向的数据结构是二叉搜索树,该树的结构为p = p + 0x10是加载右结点,p = p + 0x8是加载左结点。返回路径如下图:
顺推思路:
- 首先来到二叉树的首地址0x6030f0对应的数据:36,因为36需要大于x,才能得到
%eax = %eax * 2
,那么指针值应该为%rdi + 8
(加载左结点),指针值为6304016,查看得到值为8 - 来到8对应的位置,我们想要数据
%eax = %eax*2 + 1
,则8需要小于等于x,那么指针值应该为0x603110 + 16
(加载右结点),指针值为 6304080,查看得到的值为22 - 最后我们得到了数据22,当我们输入22的时候,因为和指针所处位置对应头部数据的值相等,所以
%eax = 0
因此22为可行解。
而当查看22对应位置时,发现该位置还有两个指针,且不是空指针, 猜想如果 22大于所需解码,返回值为%eax= %eax*2
,同样符合要求,那么指针值应该为0x603150 + 8
(加载左结点),指针值为6304368,查看得到值为20,该位置指针为空,不继续指向下一结点,所以20也为可行解。
对应递归顺序为:
- 最底层
%eax = 0
return 0
- 倒数第二层
%eax = %eax*2
return 0
- 倒数第三层
% eax = %eax*2 +1
return 1
- 倒数第四层
%eax = %eax*2
return 2
Secret Phase拆弹密码:22或20
将全部解码写入answers.txt(多解的关卡可以写一个解,也可以同时写多个解,均可运行)
终端验证:
炸弹拆除!
参考:
关于栈帧:栈帧详解
实验过程:
- CSAPP Lab2 实验记录 ---- Bomb Lab(Phase 1 - Phase 6详细解答 + Secret
Phase彩蛋解析) - CSAPP Lab2 bomblab 炸弹实验
6+1 - CS:APP二进制炸弹phase3
- 《深入理解计算机系统》(CSAPP)实验二 —— Bomb
Lab - CSAPP-Lab02 Bomb Lab
详细解析
三.小结
该实验比起Data Lab难度大了不少,在做到Phase 4的时候,难度一下就上来了,一直在各种寄存器之间转来转去,到后面几个Phase,涉及指针和结构体的时候,好想摆烂 ,后面跟着参考博客一点一点扒,发现实验的设计有好多巧妙之处。
作为经典实验,确实对从汇编角度的寄存器、栈、指针等的理解以及gdb的调试方法有不小帮助。