Bomb Lab主要考察汇编语言。
gdb(GNU debugger)用于调试C语言程序,CSAPP提供的参考手册参看这里。
常用的命令有:
1 | disas // 反汇编当前函数 |
下面,开始实验:
1 | gdb bomb |
按下Ctrl+X+A
可以进入GDB的文本用户界面tui(Text User Interface),默认显示的是源代码。
本实验考查的是汇编语言,因此,使用命令layout asm
查看汇编代码。
阶段一
1 | /* Hmm... Six phases must be more secure than one phase! */ |
函数phase_1的汇编代码如下:
1 | (gdb) disas phase_1 |
其中,核心代码为第<+4>~<+16>行。
mov $0x402400,%esi
:将地址0x402400中的值复制到寄存器%esi(用于存储函数调用时的第二个参数)中。
call 0x401338 <strings_not_equal>
:判断输入的字符串input(存储在寄存器%edi中)和%esi中存储的字符串是否相等;
test %eax,%eax
:测试%eax & %eax(寄存器%eax用于存储函数调用的返回值);
je 0x400ef7 <phase_1+23>
:当test的结果为0(即%eax中的值为0)时,跳转到0x400ef7 <phase_1+23>;否则,调用函数explode_bomb;
为了跳过函数explode_bomb,必须保证输入的字符串input与地址0x402400中的字符串相等。因此,需要输入的字符串必须为:
1 | (gdb) x/s 0x402400 |
阶段二
1 | (gdb) disas phase_2 |
<+9>调用了read_six_numbers函数,从函数名来看,需要输入六个数字。而<+14>的cmpl $0x1,(%rsp)表明第一个数字必须为1。
1 | %rsp | %rsp + 0x4 | %rsp + 0x8 | %rsp + 0xc | %rsp + 0x10 | %rsp + 0x14 | %rbp = %rsp + 0x18 | |
由<+27>~<+34>可知:%rbx - 2 %eax == 0,即当前数字必须等于前一个数字的2倍。
因此,六个数字分别为1、2、4、8、16、32。
接着,再回过头来看下read_six_numbers:
1 | (gdb) disas read_six_numbers |
这里调用了sscanf函数,其第二个参数为输入字符串的格式,对应第13行的mov $0x4025c3,%esi:
1 | (gdb) x/s 0x4025c3 |
因此,输入的字符串为1 2 4 8 16 32
。
阶段三
1 | (gdb) disas phase_3 |
这里调用了sscanf函数,通过查看<phase_1+14>中的mov $0x4025cf,%esi,可以发现其输入的字符串格式为:
1 | (gdb) x/s 0x4025cf |
也就是说,phase_3需要输入两个数字。根据<phase_3+9>、<phase_3+39>和<phase_3+44>可以得出:0<=输入的第一个数字<=7。
在<phase_3+50>中,*0x402470(,%rax,8)的值为*(8 %rax + 0x402470),具体的跳转地址取决于%rax中的值,即输入的第一个数字。
地址表达式 | 对应的跳转地址 | 第二个数字(十六进制) | 对应的十进制 |
---|---|---|---|
$*(0x402470 + 0 \times 8)$ | 0x400f7c | 0xcf | 207 |
$*(0x402470 + 1 \times 8)$ | 0x400fb9 | 0x137 | 311 |
$*(0x402470 + 2 \times 8)$ | 0x400f83 | 0x2c3 | 707 |
$*(0x402470 + 3 \times 8)$ | 0x400f8a | 0x100 | 256 |
$*(0x402470 + 4 \times 8)$ | 0x400f91 | 0x185 | 389 |
$*(0x402470 + 5 \times 8)$ | 0x400f98 | 0xce | 206 |
$*(0x402470 + 6 \times 8)$ | 0x400f9f | 0x2aa | 682 |
$*(0x402470 + 7 \times 8)$ | 0x400fa6 | 0x147 | 327 |
因此,可选的输入字符串为:
1 | 0 207 |
阶段四
1 | (gdb) disas phase_4 |
1 | (gdb) x/s 0x4025cf |
因此,可以得出如下信息:
- 需要输入两个数字,格式为”%d %d”
- 第一个数字<=14,且为函数func4的第一个参数(func4必须返回0)
- 第二个数字为0
接下来看看函数func4:
1 | (gdb) disas func4 |
将上述汇编代码转换为C语言代码,可以得到函数func4:
1 | //b = 0, c = 14, 返回值必须为0 |
因此,最终的答案为7 0
。
阶段五
1 | (gdb) disas phase_5 |
- 根据phase_5<+24>~<+34>可知,输入字符串的长度必须等于6;
- phase_5<+41>~<+74>是一个循环,共执行六次:使用当前字符的低4位作为索引值(最大索引为15),从0x4024b0中取得对应的字符:
$$
\%rdx \leftarrow \%rdx + 0x4024b0
$$
而0x4024b0中的字符串前16位为maduiersnfotvbyl
:
1 | (gdb) x/s 0x4024b0 |
转换后的字符串在内存中的布局:
1 | %rsp + 0x10 | %rsp + 0x11 | %rsp + 0x12 | %rsp + 0x13 | %rsp + 0x14 | %rsp + 0x15 | %rsp + 0x16 |
- 根据phase_5<+81>~<+100>可知,经过转换后的字符串必须等于0x40245e中存储的字符串:
1 | (gdb) x/s 0x40245e |
因此,对于输入的6个字符,其低四位依次为:
字符 | 对应的索引(十进制) | 对应的索引(二进制) |
---|---|---|
f | 9 | 1001 |
l | 15 | 1111 |
y | 14 | 1110 |
e | 5 | 0101 |
r | 6 | 0110 |
s | 7 | 0111 |
本题有非常多的可选答案,如ionefg、IONEFG等。
阶段六
1 | (gdb) disas phase_6 |
- 根据<+18>可知,需要输入六个数字,格式为
%d %d %d %d %d %d
; - 根据<+32>~<+93>可知,六个数字的内存布局为:
1 | %rsp | %rsp + 0x4 | %rsp + 0x8 | %rsp + 0xc | %rsp + 0x10 | %rsp + 0x14 | |
根据<+35>~<+56>可知,输入的六个数字 - 1 <= 5,即0 <= 六个数字 <= 6;
根据<+62>~<+87>可知,任何一个数字不能和其他五个数字相等;
根据<+103>~<+112>可知,对六个数字依次执行如下操作:
$$
x = 7 - x
$$
- 根据<+123>~<+181>可以得到:
1 | %rsi = 0 |
将从0x6032d0开始的六个地址存入%rsp+0x20 ~ %rsp+0x48
中,内存布局为:
1 | | %rsp + 0x20 | %rsp + 0x28 | %rsp + 0x30 | %rsp + 0x38 | %rsp + 0x40 | %rsp + 0x48 | |
从地址0x6032d0开始,六个节点的信息为:
1 | // 每个节点占8个字节,六个节点共占48个字节,即24w |
节点定义如下:
1 | struct node |
- 根据<+183>~<+220>可知,
%rsp+0x20 ~ %rsp+0x48
被依次链接起来,形成一个链表; - 根据<+235>~<+257>可知,链表中节点的值必须按照递减排列:
节点值 | 节点编号 | 输入的数字 = 7 - 节点编号 |
---|---|---|
924 | 3 | 4 |
691 | 4 | 3 |
477 | 5 | 2 |
443 | 6 | 1 |
332 | 1 | 6 |
168 | 2 | 5 |
因此,输入的字符串为4 3 2 1 6 5
。