CSAPP bomb lab

拆炸弹是一个非常有趣的实验,在这里简单记录一下我的拆炸弹历程。

前置准备

VSCode扩展

安装了用于高亮asm代码的扩展: x86 and x86_64 Assembly.

阅读main.c

阅读代码以了解炸弹的主要流程
该程序的每一个部分都是这样:首先读入input,然后执行phase_x(input),每个phase_x的内部都会出现走向explode_bomb的分支,因此拆除炸弹的关键就在于读懂phase_x的汇编代码,并推算出正确的input,避免代码走到explode_bomb的步骤。

GDB学习

  • GDB基本指令
指令 全称 描述
r run 开始执行程序,直到遇到断点或程序结束
q quit 退出 GDB 调试器
b break 在指定位置设置断点
tbreak temporary break 临时断点,只会停一次
c cont 从当前位置继续执行程序,直到下一个断点或结束
si stepi 执行当前指令,如果是函数调用则进入函数
ni nexti 执行下一条指令,但不进入函数
s step 执行当前行,进入函数
n next 执行当前行,但不进入函数
p print 打印变量的值
set var set variable 修改变量的值
x examine 打印内存中的值
info registers info registers 打印寄存器的值
disas disassemble 反汇编当前函数或指定代码区域
layout asm 显示汇编代码视图
layout regs 显示当前寄存器状态和它们的值
j jump 跳转到程序指定位置
info breakpoints 查看所有断点
delete delete 删除指定断点
disable disable 禁用断点
enable enable 启用断点
watch watch 设置监视点,当变量被修改时停下
backtrace bt 打印调用栈
frame 切换当前栈帧
info locals 打印当前函数的局部变量
info args 打印当前函数参数
finish 执行完当前函数并返回
signal 向程序发送信号
  • 内存检查指令x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
x /[N][F][M] ADDRESS
/*
- N: 可选,显示的单元个数,默认为 1。
- F: 可选,显示格式 (format):
- `x` : 十六进制
- `d` : 有符号十进制
- `u` : 无符号十进制
- `o` : 八进制
- `t` : 二进制
- `f` : 浮点数
- `a` : 地址
- `c` : 字符
- `s` : 字符串
- `i` : 指令 (反汇编)
- M: 可选,单位大小 (size):
- `b` : byte (1字节)
- `h` : halfword (2字节)
- `w` : word (4字节)
- `g` : giant word (8字节)
- ADDRESS: 要查看的内存地址,可以是变量名或表达式。
*/

举例:如果想要查看位于0x404783的字符串,可以使用指令x/s 0x404783

GDB初始化

为了在gdb调试的时候不触碰到explode_bomb(),同时进入每个phase_n的内部观察行为,需要在GDB初始化的时候打断点。

执行以下指令,使得gdb能够读取任何目录下的.gdbinit,该文件将用来配置初始化:

1
2
3
touch .gdbinit
mkdir -p ~/.config/gdb
echo "set auto-load safe-path /" > ~/.config/gdb/gdbinit

然后,打开bomb lab所在目录,建立一个.gdbinit,输入:

1
2
3
4
5
6
7
8
9
10
11
# 默认读入psol.txt
set args psol.txt
# 断点
b phase_1
b phase_2
b phase_3
b phase_4
b phase_5
b phase_6

b explode_bomb

关于psol.txt的坑

在我做完phase_1的时候,将答案填入了psol.txt再次运行,却发现炸弹爆炸。原因是该程序不会读取文件的最后一个字符,因此每次填完新的答案,记得在文件末尾加一个换行。

Phase_1

考查点:函数调用

phase_1的asm代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0000000000400ee0 <phase_1>:

400ee0: 48 83 ec 08 sub $0x8,%rsp

400ee4: be 00 24 40 00 mov $0x402400,%esi

400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>

400eee: 85 c0 test %eax,%eax

400ef0: 74 05 je 400ef7 <phase_1+0x17>

400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>

400ef7: 48 83 c4 08 add $0x8,%rsp

400efb: c3 retq

strings_not_equal的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int strings_not_equal(const char *s1, const char *s2) {
int len1 = string_length(s1);
int len2 = string_length(s2);

if (len1 != len2) {
return 1;
}

// 逐字符比较
while (*s1 != '\0') {
if (*s1 != *s2) {
return 1;
}
s1++;
s2++;
}

return 0;
}

phase_1非常简单,它使用函数strings_not_equal来判断输入字符串与$0x402400处的字符串是否相等,如果相等则炸弹不爆炸,因此只要找到$0x402400处的字符串即可解开炸弹。
执行:

1
x/s 0x402400

输出的字符串就是第一个key string

phase_2

考查点:函数调用、数组

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
0000000000400efc <phase_2>:

400efc: 55 push %rbp

400efd: 53 push %rbx

400efe: 48 83 ec 28 sub $0x28,%rsp

400f02: 48 89 e6 mov %rsp,%rsi

400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>

400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)

400f0e: 74 20 je 400f30 <phase_2+0x34>

400f10: e8 25 05 00 00 callq 40143a <explode_bomb>

400f15: eb 19 jmp 400f30 <phase_2+0x34>

400f17: 8b 43 fc mov -0x4(%rbx),%eax

400f1a: 01 c0 add %eax,%eax

400f1c: 39 03 cmp %eax,(%rbx)

400f1e: 74 05 je 400f25 <phase_2+0x29>

400f20: e8 15 05 00 00 callq 40143a <explode_bomb>

400f25: 48 83 c3 04 add $0x4,%rbx

400f29: 48 39 eb cmp %rbp,%rbx

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

400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp

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 retq

phase_2开始,我发现一种解决问题的好办法,就是边看边分析其c代码,这样我们能够使得代码越来越可读. phase_2的c代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void phase_2() {
int numbers[6];
read_six_numbers(numbers);

if (numbers[0] != 1) {
explode_bomb();
}

for (int i = 1; i < 6; i++) {
if (numbers[i] != numbers[i - 1] * 2) {
explode_bomb();
}
}
}

其中read_six_numbers会调用sscanf,从stdin中的读取6个数字.

read_six_numbers关键部分:

1
2
3
4
5
401480: be c3 25 40 00 mov $0x4025c3,%esi

401485: b8 00 00 00 00 mov $0x0,%eax

40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>

sscanf的函数签名为:

1
int sscanf(const char *str, const char *format, ...);

其格式字符串是第二个参数,也就是esi寄存器保存的参数,执行:

1
x/s $0x4025c3

可以看到:

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

phase_2的代码可知,这6个数字存在这样的规律:第一个数字是1,且后续每个数字都是前者的2倍,由此,答案了然.

phase_3

考查点:switch 跳表

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
26
27
28
29
30
31
32
33
34
35
36
37
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
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>
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
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
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq

这个炸弹非常简单,关键在于找到跳表并确定正确的、没有炸弹的分支。其c代码如下:

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
26
27
void phase_3(const char *input) {
int x, y;
if (sscanf(input, "%d %d", &x, &y) < 2) {
explode_bomb();
}

if (x > 7) {
explode_bomb();
}

int val;
switch (x) {
case 0: val = 0xcf; break; // 207
case 1: val = 0x2c3; break; // 707
case 2: val = 0x100; break; // 256
case 3: val = 0x185; break; // 389
case 4: val = 0xce; break; // 206
case 5: val = 0x2aa; break; // 682
case 6: val = 0x147; break; // 327
case 7: val = 0x137; break; // 311
default: val = 0; break;
}

if (y != val) {
explode_bomb();
}
}

因此,我们需要找这样的2个数:a == case(b),其中case是switch分支形成的映射。

答案有很多可能,例如:

1
"256 3"

phase_4

这里就不再展示asm了,而是直接展示其对应的c代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
int func4(int a, int b, int c) {
int d = (c < b) ? 1 : 0; // 判断区间是否合法
int e = (c - b + d) / 2; // 中点偏移
int mid = e + b; // 计算中点

if (mid > a) {
return 2 * func4(a, b, mid - 1);
} else if (mid < a) {
return 2 * func4(a, mid + 1, c) + 1;
} else {
return 0;
}
}

这是一个 二分搜索递归函数,它返回的是根据路径计算出来的一个整数编码(往左走结果乘 2,往右走结果乘 2 再加 1,找到目标返回 0)。

1
2
3
4
5
6
7
8
9
10
11
void phase_4(const char *input) {
int a, b;

if (sscanf(input, "%d %d", &a, &b) != 2 || a > 14) {
explode_bomb();
}

if (func4(a, 0, 14) != 0 || b != 0) {
explode_bomb();
}
}

解题条件

要让 phase_4 不炸:

  1. 输入必须是 两个整数
  2. 第一个数 a ≤ 14
  3. func4(a, 0, 14) == 0
    • 也就是说 a 必须刚好是递归二分的 mid,直接命中,不进入左子树或右子树。
  4. 第二个数 b == 0
    从而答案为:
1
"7 0"

phase_5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void phase_5(const char *in) {
// 1) 长度必须为 6
if (string_length(in) != 6) explode_bomb();

// 2) 把每个字符的低 4 位当作下标,查表生成新串
// 表地址 0x4024B0,目标字符串在 0x40245E("flyers")
char out[7]; // 6 + '\0'
for (int i = 0; i < 6; i++) {
unsigned idx = (unsigned char)in[i] & 0x0F; // 只用低 4 位
out[i] = *(char *)(0x4024B0 + idx); // 查表
}
out[6] = '\0';

// 3) out 必须等于 "flyers"
if (strings_not_equal(out, "flyers")) explode_bomb();
}

关键asm:

1
2
3
4
5
40108b: 0f b6 0c 03           movzbl (%rbx,%rax,1),%ecx   ; 取输入字符
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx ; 只保留低 4 位
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx ; 查表取字符
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) ; 存到输出串

关键:输入的串中的每个字节经过*(char *)(0x4024B0 + idx)这样的映射后,所得串应该是"flyers". 因此,我们先来查看0x4024B0中保存了什么:

1
2
(gdb) x/s 0x4024B0
0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

答案构造

  • 只看每个输入字符的低 4 位(与 0x0F
  • 查的是一张 16 字节的表(典型 Bomb Lab 表是 "maduiersnfotvbyl";也可以在 gdb 里用 x/s 0x4024b0 实测确认)。
  • 目标是得到 "flyers",即需要索引出 f l y e r s 这 6 个字符。
    若表为 "maduiersnfotvbyl",其索引如下:
索引 字符
0 m 1 a
8 n 9 f
因此 `"flyers"` 对应索引序列:[9, 15, 14, 5, 6, 7]。
  • 只要让 6 个输入字符的低 4 位分别等于上述索引即可。
    举例选一组好记的可打印字符(低 4 位分别为 9, F, E, 5, 6, 7):
    • 9 → 'i' (0x69)
    • 15 → 'o' (0x6F)
    • 14 → 'n' (0x6E)
    • 5 → 'e' (0x65)
    • 6 → 'f' (0x66)
    • 7 → 'g' (0x67)

一个可行输入:ionefg(长度正好 6).

说明:同一个低 4 位有很多选择,比如
9 可用 '9','I','Y','i','y'
F 可用 '/' ,'?' ,'O','_','o' 等(十六进制末位为 F 的字符)。


CSAPP bomb lab
http://example.com/2025/09/04/csapp-bomblab/
Author
Newtown
Posted on
September 4, 2025
Licensed under