PKU,2025Fall,ICS,BombLab
⚠️ 致各位同学:
本笔记的目的是借鉴交流,而非仅仅提供答案,请勿抄袭,后果自负。
你可以参考我的想法、了解有哪些注意的地方,但是在自己实现的时候不要参考,避免抄袭。
BombLab的难度相较于DataLab要高很多,不仅是因为编程语言从C变成了汇编语言,而且对逻辑推理、程序的机器表示的理解要求很高,所以完成BombLab的时间很长,每道题都需要花费1~2小时来阅读代码,还要调试程序、设置断点、分析内存。但是不可否认的是,BombLab是一个非常有创意的Lab,对你理解程序的机器级表示、知道汇编代码的功能和逻辑有很大的帮助。
邪恶的 Gin 先生在我们的 64 位 Linux 机器上放置了一系列“二进制炸弹”。二进制炸弹是一个由多个阶段组成的程序。每个阶段都期望你在标准输入中输入特定的字符串。如果你输入了正确的字符串,那么这个阶段就会被解除,炸弹会继续进行下一阶段。否则,炸弹会打印出”BOOM!!!”然后终止。当所有阶段都被解除时,炸弹就被解除了。
我们有太多炸弹需要处理,所以我们给每个学生分配了一个炸弹来解除。你的任务,你不得不接受的任务,是在截止日期前解除你的炸弹。我们相信,就像柯南一样,你也已经在夏威夷学过炸弹拆除。祝你好运,欢迎加入炸弹拆除小队!
BombLab的内容很简单(到底哪里简单了?),就是你需要对每一个Phase输入一行正确的字符串,如果输入错误则炸弹爆炸,并且自动向AutoLab服务器报告一次。每爆炸一次扣除0.5分,上限20分。所以,一定要小心拆弹(毕竟大家没有在夏威夷学过拆弹)。
前四个phase每个15分,最后两个各20分,而且难度递增。(难道不是指数式增长吗)
运行的时候,可以使用:
./bomb answer.txt
代码会从answer.txt读取行直到结束EOF,再切换到stdin以避免重复输入。
AutoLab会随时更新你(和你的同学)的进度。
在AutoLab上下载tar文件并上传到CLab上后,解压完成可以得到三个文件:
README:这是专属于你的炸弹(为什么这还有定制化),里面有你的邮箱。
bomb:二进制可执行文件(二进制炸弹)
bomb.c:炸弹的基本逻辑(就是main函数啦)
这个Lab最常用到的工具就是gdb,也就是GNU调试器。你可以逐行跟踪程序,检查内存和寄存器,查看源代码和汇编代码(实际上也没有原代码),设置断点,设置内存监视点,并编写脚本。
而objdump是Linux下一个反汇编工具。这将帮助我们解决这些Phase。
以下是一些常用的指令(由BombLab的WriteUp提供):
| 指令 | 全称 | 描述 |
|---|---|---|
r |
run | 开始执行程序,直到下一个断点或程序结束 |
q |
quit | 退出 GDB 调试器 |
ni |
nexti | 执行下一条指令,但不进入函数内部 |
si |
stepi | 执行当前指令,如果是函数调用则进入函数 |
b |
break | 在指定位置设置断点 |
c |
cont | 从当前位置继续执行程序,直到下一个断点或程序结束 |
p |
打印变量的值 | |
x |
打印内存中的值 | |
j |
jump | 跳转到程序指定位置 |
disas |
反汇编当前函数或指定的代码区域 | |
layout asm |
显示汇编代码视图 | |
layout regs |
显示当前的寄存器状态和它们的值 |
其中p和x有一些更重要的用法:
p $rax # 打印寄存器 rax 的值
p $rsp # 打印栈指针的值
p/x $rsp # 打印栈指针的值,以十六进制显示
p/d $rsp # 打印栈指针的值,以十进制显示
x/2x $rsp # 以十六进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。
x/2d $rsp # 以十进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。
x/2c $rsp # 以字符格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。
x/s $rsp # 把栈指针指向的内存位置 M[%rsp] 当作 C 风格字符串来查看。
x/b $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 1 字节。
x/h $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 2 字节(半字)。
x/w $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 4 字节(字)。
x/g $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 8 字节(双字)。
info registers # 打印所有寄存器的值
info breakpoints # 打印所有断点的信息
delete breakpoints 1 # 删除第一个断点,可以简写为 d 1
这些命令在 / 后面的后缀(如 2x、2d、s、g、20c)指定了查看内存的方式和数量。具体来说:
第一个数字(如 2、20)指定要查看的单位数量。
第二个字母(如 x、d、s、g、c)指定单位类型和显示格式,其中:
c / d / x 分别代表以字符 / 十进制 / 十六进制格式显示内存内容。
s 代表以字符串格式显示内存内容。
b / h / w / g 分别代表以 1 / 2 / 4 / 8 字节为单位(unit)显示内存内容。
当使用 x/b、x/h、x/w、x/g 时,unit 会保留对应改变,直到你再次使用这些命令。
阅读bomb.c文件,我们可以发现其逻辑非常清晰:
...
input = read_line();
phase_x(input);
phase_defused(fd);
print("...");
...
也就是说,函数会先读取一行输入,然后传入对应的Phase。我们现在可以简单的猜测,其内部应该存在某种在一定条件下会跳转到炸弹爆炸的函数的部分,而炸弹爆炸后会向服务器发送消息。
解读这个程序,我们还需要阅读其汇编代码,所以我们先来反汇编这个二进制程序:
objdump -d bomb > bomb.asm
这样我们就把汇编代码全部放在了bomb.asm这个文件中。
gdb有一个很好用的东西叫.gdbinit,他可以避免你每次启动调试都要要重新设置断点、打开界面。我们可以通过如下方式进行配置:
# 创建当前目录下的 .gdbinit 文件
touch .gdbinit
# 创建 .config/gdb 文件夹
mkdir -p ~/.config/gdb
# 允许 gdb 预加载根目录下所有的文件
echo "set auto-load safe-path /" > ~/.config/gdb/gdbinit
然后,就可以在该目录下的.gdbinit写入:
set args answer.txt
# 将输入定向到 answer.txt 文件中
layout asm
layout regs
# 分别在终端显示汇编和寄存器的实时详细信息
# 可选
b phase_1
b phase_2
b phase_3
b phase_4
b phase_5
b phase_6
# 为每个 phase 打断点,方便调试
# 每完成一个 phase,就可以注释掉一行。
我在25年做这个Lab时,有一个同学的炸弹爆炸了50+次,非常的骇人(骇死我力),所以我们可以考虑,怎么让炸弹先安全化,再去拆弹?
有两种方法:
找到相关代码,再利用 hexedit 工具修改二进制码,替换条件跳转指令或者使用 nop 无义指令替换危险指令。(有点像AttackLab)
另一种则简单得多,就是在对应函数处设置断点,并且跳过向服务器发送消息的函数。
下面展示第二种方法:
阅读bomb.asm文件,我们可以找到两个函数:explore_bomb和send_msg。从函数名我们可以猜测这是炸弹爆炸和发送信息的函数,而且在explore_bomb函数中的:
217c: e8 55 fe ff ff call 1fd6 <send_msg>
让我们确定send_msg是在explore_bomb内调用。所以,我们可以在.gdbinit中继续写入:
# 为 explode_bomb 中触发 send_msg 函数的地方设置断点
b *(explode_bomb + 0x44)
# 为此断点编程
command
# 直接跳到 exit 退出函数处,跳过发送信息流程
j *(explode_bomb + 0x81)
end
# 炸弹已经安全化,可以放心地拆弹了,开始运行程序
r
切记不可轻易将这段代码注释!
现在我们来一次解决这些炸弹:
在bomb.asm中我们可以找到phase_1到phase_6的函数。先来看看phase_1:
0000000000001784 <phase_1>:
1784: f3 0f 1e fa endbr64
1788: 48 83 ec 08 sub $0x8,%rsp
178c: 48 8d 35 45 2a 00 00 lea 0x2a45(%rip),%rsi # 41d8 <_IO_stdin_used+0x1d8>
1793: e8 8b 06 00 00 call 1e23 <strings_not_equal>
1798: 85 c0 test %eax,%eax
179a: 75 05 jne 17a1 <phase_1+0x1d>
179c: 48 83 c4 08 add $0x8,%rsp
17a0: c3 ret
17a1: e8 92 09 00 00 call 2138 <explode_bomb>
17a6: eb f4 jmp 179c <phase_1+0x18>
(其实main函数也有一部分,不过我觉得不影响,毕竟我打算做的时候都不知道main函数里面还有东西。)接下来我们可以逐行解读一下代码:
endbr64:无关紧要;
sub $0x8,%rsp:将栈指针%rsp减0x8,即为函数phase_1分配栈空间。
lea 0x2a45(%rip),%rsi:将(%rip + 0x2a45)处存储的值提取到%rsi中。
call 1e23 <strings_not_equal>:调用strings_not_equal函数,比对字符串是否相等。(从strings_not_equal可以知道传入的参数在%rdi和%rsi中)而%rdi寄存器在phase_1中一直没有修改,也就是在main函数中的%rdi:保存read_line函数的传入值。
test %eax,%eax:将 %eax 寄存器的值与其自身进行与运算,然后将结果存入 %eax。并且同时设置条件码(Condition Code),其中有一个叫做 ZF 的标志位,如果 %eax 的值为 0,则 ZF 为 1,否则为 0。
jne 17a1 <phase_1+0x1d>:条件跳转指令,jne指令的跳转条件的~ZF,如果%eax为1(即strings_not_equal函数返回True,函数返回值均存储在%rax / %eax中),则ZF为 0,程序进行跳转。跳转目标则是位于17a1的代码,也就是调用explode_bomb函数的地方。
add $0x8,%rsp:如果上一条没有跳转,即字符串匹配成功,则回复栈指针的位置,答案通过。
ret:返回,退出函数。
add $0x8,%rsp:同上。
综上,我们可以得到phase_1的基本逻辑:输入一个字符串并存储在%rdi中,并与(%rip + 0x2a45)处存储的值对比。两者匹配则不会爆炸,炸弹解除。
那么,解决的办法也就很清晰了:找到(%rip + 0x2a45)处存储的值。
我们在answer.txt中随便写一行:
pku is better than thu
然后在终端输入:
gdb bomb
之后终端会有如下内容:
Reading symbols from bomb...
Breakpoint 1 at 0x1784
Breakpoint 2 at 0x17a8
Breakpoint 3 at 0x181a
Breakpoint 4 at 0x19da
Breakpoint 5 at 0x1a59
Breakpoint 6 at 0x1aec
Breakpoint 7 at 0x217c
...
Welcome to Mr.Gin's little bomb. You have 6 phases with
which to blow yourself up. Have a nice day! Mua ha ha ha!
Breakpoint 1, 0x0000555555555784 in phase_1 ()
(gdb)
此时,程序停止在了我们设置的第一个断点处,也就是phase_1的第一行。而我们需要查询的值在第三行,所以我们继续输入:
ni
ni
ni
x/s $rip + 0x2a45
# 以C风格字符串打印(%rip + 0x2a45)处存储的值
然后我们可以看到:
(gdb) x/s $rip + 0x2a45
0x5555555581d8: "A secret makes a woman a woman"
所以我们就可以确定我们的答案就是A secret makes a woman a woman。
先输入q退出调试,然后在answer.txt中写入这个字符串,再次打开gdb,运行:
c
Breakpoint 1, 0x0000555555555784 in phase_1 ()
(gdb) c
Continuing.
Phase 1 defused. How about the next one?
Breakpoint 2, 0x00005555555557a8 in phase_2 ()
(gdb)
我们就完成了phase_1。(可喜可贺,可喜可贺~)
00000000000017a8 <phase_2>:
17a8: f3 0f 1e fa endbr64
17ac: 53 push %rbx
17ad: 48 83 ec 20 sub $0x20,%rsp
17b1: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
17b8: 00 00
17ba: 48 89 44 24 18 mov %rax,0x18(%rsp)
17bf: 31 c0 xor %eax,%eax
17c1: 48 89 e6 mov %rsp,%rsi
17c4: e8 f5 09 00 00 call 21be <read_six_numbers>
17c9: 83 3c 24 00 cmpl $0x0,(%rsp)
17cd: 78 07 js 17d6 <phase_2+0x2e>
17cf: bb 01 00 00 00 mov $0x1,%ebx
17d4: eb 0a jmp 17e0 <phase_2+0x38>
17d6: e8 5d 09 00 00 call 2138 <explode_bomb>
17db: eb f2 jmp 17cf <phase_2+0x27>
17dd: 83 c3 01 add $0x1,%ebx
17e0: 83 fb 05 cmp $0x5,%ebx
17e3: 7f 1a jg 17ff <phase_2+0x57>
17e5: 48 63 c3 movslq %ebx,%rax
17e8: 8d 53 ff lea -0x1(%rbx),%edx
17eb: 48 63 d2 movslq %edx,%rdx
17ee: 89 d9 mov %ebx,%ecx
17f0: 03 0c 94 add (%rsp,%rdx,4),%ecx
17f3: 39 0c 84 cmp %ecx,(%rsp,%rax,4)
17f6: 74 e5 je 17dd <phase_2+0x35>
17f8: e8 3b 09 00 00 call 2138 <explode_bomb>
17fd: eb de jmp 17dd <phase_2+0x35>
17ff: 48 8b 44 24 18 mov 0x18(%rsp),%rax
1804: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax
180b: 00 00
180d: 75 06 jne 1815 <phase_2+0x6d>
180f: 48 83 c4 20 add $0x20,%rsp
1813: 5b pop %rbx
1814: c3 ret
1815: e8 86 fa ff ff call 12a0 <__stack_chk_fail@plt>
000000000001aec <phase_6>:
1aec: f3 0f 1e fa endbr64
1af0: 41 54 push %r12
1af2: 55 push %rbp
1af3: 53 push %rbx
1af4: 48 83 ec 60 sub $0x60,%rsp
1af8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
1aff: 00 00
1b01: 48 89 44 24 58 mov %rax,0x58(%rsp)
1b06: 31 c0 xor %eax,%eax
1b08: 48 89 e6 mov %rsp,%rsi
1b0b: e8 ae 06 00 00 call 21be <read_six_numbers>
1b10: bd 00 00 00 00 mov $0x0,%ebp
1b15: eb 27 jmp 1b3e <phase_6+0x52>
1b17: e8 1c 06 00 00 call 2138 <explode_bomb>
1b1c: eb 33 jmp 1b51 <phase_6+0x65>
1b1e: 83 c3 01 add $0x1,%ebx
1b21: 83 fb 05 cmp $0n'hx5,%ebx
1b24: 7f 15 jg 1b3b <phase_6+0x4f>
1b26: 48 63 c5 movslq %ebp,%rax
1b29: 48 63 d3 movslq %ebx,%rdx
1b2c: 8b 3c 94 mov (%rsp,%rdx,4),%edi
1b2f: 39 3c 84 cmp %edi,(%rsp,%rax,4)
1b32: 75 ea jne 1b1e <phase_6+0x32>
1b34: e8 ff 05 00 00 call 2138 <explode_bomb>
1b39: eb e3 jmp 1b1e <phase_6+0x32>
1b3b: 44 89 e5 mov %r12d,%ebp
1b3e: 83 fd 05 cmp $0x5,%ebp
1b41: 7f 17 jg 1b5a <phase_6+0x6e>
1b43: 48 63 c5 movslq %ebp,%rax
1b46: 8b 04 84 mov (%rsp,%rax,4),%eax
1b49: 83 e8 01 sub $0x1,%eax
1b4c: 83 f8 05 cmp $0x5,%eax
1b4f: 77 c6 ja 1b17 <phase_6+0x2b>
1b51: 44 8d 65 01 lea 0x1(%rbp),%r12d
1b55: 44 89 e3 mov %r12d,%ebx
1b58: eb c7 jmp 1b21 <phase_6+0x35>
1b5a: b8 00 00 00 00 mov $0x0,%eax
1b5f: eb 11 jmp 1b72 <phase_6+0x86>
1b61: 48 63 c8 movslq %eax,%rcx
1b64: ba 07 00 00 00 mov $0x7,%edx
1b69: 2b 14 8c sub (%rsp,%rcx,4),%edx
1b6c: 89 14 8c mov %edx,(%rsp,%rcx,4)
1b6f: 83 c0 01 add $0x1,%eax
1b72: 83 f8 05 cmp $0x5,%eax
1b75: 7e ea jle 1b61 <phase_6+0x75>
1b77: be 00 00 00 00 mov $0x0,%esi
1b7c: eb 17 jmp 1b95 <phase_6+0xa9>
1b7e: 48 8b 52 08 mov 0x8(%rdx),%rdx
1b82: 83 c0 01 add $0x1,%eax
1b85: 48 63 ce movslq %esi,%rcx
1b88: 39 04 8c cmp %eax,(%rsp,%rcx,4)
1b8b: 7f f1 jg 1b7e <phase_6+0x92>
1b8d: 48 89 54 cc 20 mov %rdx,0x20(%rsp,%rcx,8)
1b92: 83 c6 01 add $0x1,%esi
1b95: 83 fe 05 cmp $0x5,%esi
1b98: 7f 0e jg 1ba8 <phase_6+0xbc>
1b9a: b8 01 00 00 00 mov $0x1,%eax
1b9f: 48 8d 15 aa 64 00 00 lea 0x64aa(%rip),%rdx # 8050 <node1>
1ba6: eb dd jmp 1b85 <phase_6+0x99>
1ba8: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
1bad: 48 89 d9 mov %rbx,%rcx
1bb0: b8 01 00 00 00 mov $0x1,%eax
1bb5: eb 12 jmp 1bc9 <phase_6+0xdd>
1bb7: 48 63 d0 movslq %eax,%rdx
1bba: 48 8b 54 d4 20 mov 0x20(%rsp,%rdx,8),%rdx
1bbf: 48 89 51 08 mov %rdx,0x8(%rcx)
1bc3: 83 c0 01 add $0x1,%eax
1bc6: 48 89 d1 mov %rdx,%rcx
1bc9: 83 f8 05 cmp $0x5,%eax
1bcc: 7e e9 jle 1bb7 <phase_6+0xcb>
1bce: 48 c7 41 08 00 00 00 movq $0x0,0x8(%rcx)
1bd5: 00
1bd6: bd 00 00 00 00 mov $0x0,%ebp
1bdb: eb 07 jmp 1be4 <phase_6+0xf8>
1bdd: 48 8b 5b 08 mov 0x8(%rbx),%rbx
1be1: 83 c5 01 add $0x1,%ebp
1be4: 83 fd 04 cmp $0x4,%ebp
1be7: 7f 11 jg 1bfa <phase_6+0x10e>
1be9: 48 8b 43 08 mov 0x8(%rbx),%rax
1bed: 8b 00 mov (%rax),%eax
1bef: 39 03 cmp %eax,(%rbx)
1bf1: 7d ea jge 1bdd <phase_6+0xf1>
1bf3: e8 40 05 00 00 call 2138 <explode_bomb>
1bf8: eb e3 jmp 1bdd <phase_6+0xf1>
1bfa: 48 8b 44 24 58 mov 0x58(%rsp),%rax
1bff: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax
1c06: 00 00
1c08: 75 09 jne 1c13 <phase_6+0x127>
1c0a: 48 83 c4 60 add $0x60,%rsp
1c0e: 5b pop %rbx
1c0f: 5d pop %rbp
1c10: 41 5c pop %r12
1c12: c3 ret
1c13: e8 88 f6 ff ff call 12a0 <__stack_chk_fail@plt>