这篇文章单独记录 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:隐藏答案字符串
  • %eaxstrings_not_equal 的返回值
  • 返回 0 表示字符串相同
  • 返回非 0 表示字符串不同

调试命令:

1
2
3
x/s 0x402400
x/s $rdi
x/s $rsi

3. Phase 2:六个整数

Phase 2 会读取 6 个整数,放在 phase_2 自己的栈帧里,可以理解成一个局部数组:

1
int nums[6];

每个 int 是 4 字节,所以 6 个整数一共占 24 字节。后面的循环用 %rbx 当当前元素指针,从 &nums[1] 开始,每次加 4 指向下一个 int;用 %rbp 当结束边界,也就是 &nums[6]。注意 nums[6] 不是真正的数组元素,只是数组末尾后的边界地址。

leaload 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 的答案规律是:

1
1 2 4 8 16 32

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 查看格式字符串:

1
x/s 0x4025c3

可以看到:

1
"%d %d %d %d %d %d"

sscanf 的返回值放在 %eax,表示成功读取了几个数据。后面:

1
2
cmp $0x5,%eax
jg 0x401499

是在检查是否成功读取了 6 个整数;如果 %eax <= 5,说明没有读够 6 个数,就会调用 explode_bomb