这篇文章单独记录 Bomb Lab。建立「汇编 -> C 逻辑」的阅读能力。
0. 工作流
从零创建工作流:
1 2 3
| objdump -d bomb > bomb.asm touch solution.txt gdb bomb
|
进入 GDB 后:
1 2
| b explode_bomb run solution.txt
|
含义:
objdump -d bomb > bomb.asm:把 bomb 反汇编成 bomb.asm
touch solution.txt:创建答案文件
gdb bomb:用调试器打开炸弹
b explode_bomb:在爆炸函数前设置断点,防止真的炸
run solution.txt:让炸弹从答案文件逐行读取输入
1. 必备寄存器
x86-64 Linux 下,整数和指针参数通常这样传:
1 2 3 4 5 6 7 8 9
| 第 1 个参数 -> %rdi 第 2 个参数 -> %rsi 第 3 个参数 -> %rdx 第 4 个参数 -> %rcx 第 5 个参数 -> %r8 第 6 个参数 -> %r9
返回值 -> %rax 栈顶 -> %rsp
|
注意:
%esi 是 %rsi 的低 32 位
%eax 是 %rax 的低 32 位
- 写 32 位寄存器时,高 32 位会被清零
2. Phase 1:字符串比较
汇编:
1 2 3 4 5 6 7 8
| 0x0000000000400ee0 <+0>: sub $0x8,%rsp //栈在x86-64中向低地址生长,给栈调整 8 字节,主要是为了满足函数调用前 16 字节对齐的 ABI 要求。 0x0000000000400ee4 <+4>: mov $0x402400,%esi //$0x402400中的"$"表示立即数,0x402400 是隐藏字符串的地址;把这个地址放进 %rsi,作为 strings_not_equal 的第二个参数。 0x0000000000400ee9 <+9>: call 0x401338 <strings_not_equal> //进入phase1时,%rdi已经保存了用户输入字符串,mov只是准备第二个参数%rsi,call string_not_equal等价于string_not_equal(input, secret) 0x0000000000400eee <+14>: test %eax,%eax 0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23> //jump if equal 0x0000000000400ef2 <+18>: call 0x40143a <explode_bomb> 0x0000000000400ef7 <+23>: add $0x8,%rsp //把栈恢复原状 0x0000000000400efb <+27>: ret
|
还原成 C:
1 2 3 4 5 6 7 8
| void phase_1(char *input) { char *secret = (char *)0x402400;
if (strings_not_equal(input, secret) != 0) { explode_bomb(); } }
|
关键点:
%rdi:用户输入字符串
%rsi:隐藏答案字符串
%eax:strings_not_equal 的返回值
- 返回
0 表示字符串相同
- 返回非
0 表示字符串不同
调试命令:
1 2 3
| x/s 0x402400 x/s $rdi x/s $rsi
|
3. Phase 2:六个整数
Phase 2 会读取 6 个整数,放在 phase_2 自己的栈帧里,可以理解成一个局部数组:
每个 int 是 4 字节,所以 6 个整数一共占 24 字节。后面的循环用 %rbx 当当前元素指针,从 &nums[1] 开始,每次加 4 指向下一个 int;用 %rbp 当结束边界,也就是 &nums[6]。注意 nums[6] 不是真正的数组元素,只是数组末尾后的边界地址。
lea 是 load effective address,常见用途是计算地址但不访问内存。例如 lea 0x4(%rsp), %rbx 表示把 %rsp + 4 这个地址放进 %rbx,而不是读取 %rsp + 4 里面的值。
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
| 400efc: 55 push %rbp //保存 %rbp,后面要拿它当结束边界指针 400efd: 53 push %rbx //保存 %rbx,后面要拿它当当前元素指针 400efe: 48 83 ec 28 sub $0x28,%rsp //在栈上开 0x28 字节空间,主要用来放 nums 数组 400f02: 48 89 e6 mov %rsp,%rsi //把 nums 起始地址传给 read_six_numbers 的第 2 个参数 400f05: e8 52 05 00 00 call 40145c <read_six_numbers> //read_six_numbers(input, nums) 400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) //比较 nums[0] 和 1 400f0e: 74 20 je 400f30 <phase_2+0x34> 400f10: e8 25 05 00 00 call 40143a <explode_bomb> 400f15: eb 19 jmp 400f30 <phase_2+0x34> 400f17: 8b 43 fc mov -0x4(%rbx),%eax //取前一个数 nums[i - 1] 400f1a: 01 c0 add %eax,%eax //%eax = nums[i - 1] * 2 400f1c: 39 03 cmp %eax,(%rbx) //比较 nums[i] 和 nums[i - 1] * 2 400f1e: 74 05 je 400f25 <phase_2+0x29> 400f20: e8 15 05 00 00 call 40143a <explode_bomb> 400f25: 48 83 c3 04 add $0x4,%rbx //%rbx 指向下一个 int 400f29: 48 39 eb cmp %rbp,%rbx //是否已经到达结束边界 &nums[6] 400f2c: 75 e9 jne 400f17 <phase_2+0x1b> 400f2e: eb 0c jmp 400f3c <phase_2+0x40> 400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx //%rbx = &nums[1] 400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp //%rbp = &nums[6],用于退出循环 400f3a: eb db jmp 400f17 <phase_2+0x1b> //进入循环 400f3c: 48 83 c4 28 add $0x28,%rsp 400f40: 5b pop %rbx 400f41: 5d pop %rbp 400f42: c3 ret
|
开辟空间后,栈大概长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 高地址 +--------------------+ | 保存的 %rbp | +--------------------+ | 保存的 %rbx | +--------------------+ | nums[5] | %rsp + 0x14 +--------------------+ | nums[4] | %rsp + 0x10 +--------------------+ | nums[3] | %rsp + 0x0c +--------------------+ | nums[2] | %rsp + 0x08 +--------------------+ | nums[1] | %rsp + 0x04 +--------------------+ | nums[0] | %rsp +--------------------+ 低地址
|
循环部分可以还原成:
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (nums[0] != 1) { explode_bomb(); }
int *p = &nums[1]; int *end = &nums[6];
while (p != end) { if (*p != *(p - 1) * 2) { explode_bomb(); } p++; }
|
所以 Phase 2 的答案规律是:
read_six_numbers
read_six_numbers 的功能是执行类似下面的代码:
1 2 3
| sscanf(input, "%d %d %d %d %d %d", &nums[0], &nums[1], &nums[2], &nums[3], &nums[4], &nums[5]);
|
对应汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 0x000000000040145c <+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 0x0000000000401485 <+41>: mov $0x0,%eax 0x000000000040148a <+46>: call 0x400bf0 <__isoc99_sscanf@plt> 0x000000000040148f <+51>: cmp $0x5,%eax 0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61> 0x0000000000401494 <+56>: call 0x40143a <explode_bomb> 0x0000000000401499 <+61>: add $0x18,%rsp 0x000000000040149d <+65>: ret
|
这里不是指令重排,而是在按照 x86-64 调用约定准备 sscanf 的参数。sscanf 一共有 8 个参数:
1 2 3 4 5 6 7 8
| 第 1 个参数 %rdi input 第 2 个参数 %rsi "%d %d %d %d %d %d" 第 3 个参数 %rdx &nums[0] 第 4 个参数 %rcx &nums[1] 第 5 个参数 %r8 &nums[2] 第 6 个参数 %r9 &nums[3] 第 7 个参数 (%rsp) &nums[4] 第 8 个参数 0x8(%rsp) &nums[5]
|
用 GDB 查看格式字符串:
可以看到:
sscanf 的返回值放在 %eax,表示成功读取了几个数据。后面:
1 2
| cmp $0x5,%eax jg 0x401499
|
是在检查是否成功读取了 6 个整数;如果 %eax <= 5,说明没有读够 6 个数,就会调用 explode_bomb。