一、拆弹环境
操作系统:Ubuntu
GDB:GNU gdb (Ubuntu 12.0.90-0ubuntu1) 12.0.90(gdb -v
查看)
linux—常用gdb调试命令汇总_就要 宅在家的博客-CSDN博客
二、安装炸弹
下载炸弹:wget csapp.cs.cmu.edu/3e/bomb.tar
解压炸弹:tar xvf bomb.tar
生成一个bomb
目录,包含三个文件:bomb
(可执行文件), bomb.c
(文本), README
(文本),在新建一个文件ans.txt
用于记录密码比较方便
三、拆解炸弹
优质文章: 手把手教你拆解 CSAPP 的 炸弹实验室 BombLab
首先,大致浏览一下bomb.c了解一下构架
- 思路:为了不call explode_bomb,需要合理利用各个跳转指令,使phase_1、phase_2、…phase_6函数在不调用函数explode_bomb的情况下成功ret
comb目录下输入gdb bomb
,开始运行
phase_1
-
为了不进行
call 0x40143a <explode_bomb>
必须让je 0x400ef7 <phase_1+23>
成功执行,从而跳转到add的位置,再ret返回 -
向上追溯,为了让
je 0x400ef7 <phase_1+23>
成功执行,源操作数与目的操作数就应该相等,又由于在这段代码中test %eax,%eax
的结果用于判断%eax
的值是否为0(ZF==1),可得%eax
的值必须为0 -
由于函数的返回值一般存储在
%eax
容器中,再向上追溯strings_not_equal的返回值必须是0 -
虽然也可以从字面上理解strings_not_equal,就是要让两个字符串相同,而第一个字符串就是通过栈传递的密码,第二个字符串就是%esi的值。由于%esi的值已知,执行
得到密码"Border relations with Canada have never been better." -
如果不取巧,查看函数strings_not_equal的执行过程判断
传参对应的寄存器分别为:第一个参数%rdi,第二个参数%rsi…第一个红框表示传入两个字符串,然后两个箭头代表计算两个字符串长度;第二个红框表示表示若字符串一样长则继续比较,不跳转,若不一样长则跳转(ret=1爆炸);第三个红框检查是否都为空,接着检查字符串每位数据是否相同…若都一致,则(ret=0,第一个炸弹拆解成功)
phase_2
-
由read_six_numbers函数可知,密码为6个数,具体可查看read_six_numbers的执行过程得出结论
首先可得到相对偏移量从0x0到0x14,间隔为4的6个空间,第二个框大概率是格式字符串的地址(可由x/s 0x4025c3结果验证),第三个框大概是程序计数器(0~5间6个数,验证是否输入6个数,目的也在此)。验证完成后,又add回去,释放空间 -
由<+14><+18><+20>的语句可得,此时(%rsp)必然等于1,即该开辟的栈中地址最小的位置数据为1
-
由<+52><+62><+27><+30><+32><+34><+36>的语句可得,此时%eax的值必然等于(%rbx),即该开辟的栈中地址倒二小的位置数据为2
-
由<+41><+45><+48><+27>…可知,此时%rbp的值等于%rbx的值等于4,即该开辟的栈中地址倒三小的位置数据为4
-
…(以此类推,相当于循环)结果为
-
用C语言goto语句模拟汇编代码控制流程更为清晰
void phase_2() {
int numbers[6];
int *rbx, *rbp;
start:
rbp = &numbers[0];
rbx = &numbers[1];
read_six_numbers(numbers); // call 0x40145c <read_six_numbers>
if (*rbp != 1) {
explode_bomb(); // call 0x40143a <explode_bomb>
goto start;
}
loop:
int eax = *(rbx - 1);
eax += eax;
if (eax != *rbx) {
explode_bomb(); // call 0x40143a <explode_bomb>
goto start;
}
rbx++;
if (rbx != rbp) {
goto loop;//实际上有一个循环
}
return;
}
- 成功(但先试了32 16 8 4 2 1失败了(⊙﹏⊙))
phase_3
- 从<+0>到<+34>这一段与 phase_2中read_six_numbers函数的过程大致相同,0xc(%rsp)存第一个参数的地址,0x8(%rsp)存第二个参数的地址,立即数0x4025cf作为第三个参数存格式字符串的地址,立即数0作第四个参数,结果说明密码是2个十位数整数
- 然后<+39><+46>得到第一个十进制整数<=7,一直到<+50>跳转到0x402470+%rax的值*8所存储的地址处(这里%rax是我们输入的第一个值)
- 就将计就计,查询0x402470后的指令,经反复调试与观察发现后16个指令的地址
刚好对应
可知,这是switch汇编代码,通过跳转表来访问代码位置 - 有8种答案
phase_4
- 过程和前面几个phase很像,能推出的结论就是①密码为两个10进制的数字②%eax=0
③0x8(%rsp)<=0xe=14(第一个数小于等于14) && 0xc(%rsp)=0(第二个数等于0) ④<+45>到<+56>在为进入func4准备数据
- 由<+27><+48>可看出这是递归操作,大致理一下思路
- - 可以大致分为三条路,①路只有当两个数相等时能行(两个数都为0),②路万万行不通 ③路也许行得通,但得带值去试
phase_5
- 从<+0>到<+34>可知字符串长度为6(xor %eax %eax异或,目的是清空eax寄存器,将其值设为0)
- <+41>到<+74>之间在循环[for(rax=0;rax!=6;rax++)],可分析一下<+41>源操作数是个什么东东
p *(char*)($rbx)
p *(char*)($rbx+0x1)
p *(char*)($rbx+0x2)
p *(char*)($rbx+0x3)
…刚好对应猜的6个数,说明%rbx指向字符串最低位;值得注意的是%ecx和%cl是一个寄存器,只是一个32字节一个8字节,%edx和%dl同理;and $0xf,%edx
是将%edx寄存器的值与0xf进行按位运算,并将结果存回%edx中因为0xf的二进制表示为00001111,与%edx寄存器的每一位进行与操作,可以将%edx寄存器的高四位都置为0,只有低四位保留下来; - 于是<+41>到<+74>具体过程如下:假设输入的字符串为abcdef,每个字符刚好对应一个循环,对应一个字符输出,所以这个区间同样生成长度为6的新字符串。拿a举例,a的ASICC码为98,98的二进制为01100010,与0xf进行按位与操作后,结果为00000010,即%rdx=2;由
x/s 0x4024b0
的结果maduiersnfotvbyl…(后面不需要知道,因为二进制0000~1111只有16个数),movzbl 0x4024b0(%rdx) ,%edx
,计算内存地址0x4024b0+2的字节内容d加载到%edx寄存器中(%edx=d),以此类推 - 继续向后看,从<+81>到<+100>类似于phase_1的操作,说明<+41>到<+74>得到的字符串要等于0x40245e的字节内容。
x/s 0x40245e
为flyers - 最后,反向推,flyers对应maduiersnfotvbyl的9/15/14/5/6/7,进行二进制,查ASCII码,得出输入的密码应该为ionefg