CSAPP LAB-2 二进制炸弹实验
死去的炸弹又来攻击我!!😅😅😅
Computer Systems. A Programmer’s Perspective,CS: APP——中文译名《深入理解计算机系统》,是本科时候的必选课,研究生阶段选修了这门课,相当于重新再学一遍。本科学这门课时正值疫情,在加里敦~~(家里蹲)~~大学上了一个学期,当时太迷茫了,啥也不会,整个课上得懵懵懂懂。估计那时的炸弹也是看着网上的教程东抄抄西补补勉强弄出来的。
现在研究生了,该补的地方还是得补。不管有多忙,我决定沉下心来重看《CSAPP》,炸弹客实验也打算亲自做做。最终还是做出来了😎,耗时 2 天。难度怎么说——还算适中吧,如果你仔细看过课本就不难。
目前基本的 6 层炸弹已经拆除,还有一层隐藏层有时间再研究。由于每个人收到的炸弹是不同的(你的老师会说根据你的学号选择相应的炸弹),所以文章仅提供基本思路供参考。
CSAPP LAB 实验
- CSAPP LAB-1 位操作
- CSAPP LAB-2 二进制炸弹实验(本文)
- CSAPP LAB-3 缓冲区溢出炸弹
- CSAPP LAB-4 代码优化
- CSAPP LAB-5 手写动态存储分配器
实验介绍
二进制炸弹包含若干个阶段,每个阶段需要输入特定的字符串,所有输入正确则炸弹被排除,否则……
我们的任务是找出这些字符串字符串记录到文件中,可输入命令验证 ./bomb solution.txt
用换行区别不同阶段的字符串。
下载压缩包 bombs,压缩包包含多个 bomb
代码包,根据学号领取自己的代码包。
查看 bomb.c
可知程序利用 phase_*
函数(*
为 1~6) 检查输入字符串是否合法,不合法就引爆炸弹。我们的任务就是逆向出每个 phase 的检查规则,构造出合法字符串。当然,bomb.c
没有给出 phase_*
的源码
逆向方法:
- GDB
- 直接反汇编
课本参考:
- CSAPP 第三章:GDB 的使用
- CSAPP 第三章:过程
链接参考(喜报:全是 English):
- GDB:
- X86 指令手册:CS:APP2e, Bryant and O’Hallaron (cmu.edu)
- GNU 汇编语法参考:
汇编语法看课本就行,GDB 命令看本文后续章节就行。以上链接你可以一个都不用点开。
GDB 工具的使用
1 | apt install gdb # 安装 GDB |
GDB 不需要了解过多,想干什么直接查就行。现用现学。
你可能用到的命令只有这些:
run
:运行 GDBcontinue
:继续运行,可简写为c
。break <addr>
:在指定地址处设置断点。break
可简写为b
。print $eax
:打印某个寄存器的值。print
可简写为p
。p /x ($esp-0x8)
:打印某个寄存器的值的运算结果print (char *) <addr>
:将直接打印该地址addr
下的字符串print /x * <addr>
:以十六进制的形式打印指定地址addr
的值print ((int*) <addr>)[<index>]
:打印指定位置addr
数组下标为index
的元素print ((int*) <addr>)[<index>]@<num>
:打印指定位置数组的元素,起始坐标为index
,输出num
个元素
截一张课本的图供更多参考:
开始拆弹!
拆弹准备
在 Linux 环境下解压 bomb 压缩文件。
1 | tar -xvf bomb7.tar |
得到如下文件:
bomb
:二进制可执行文件,任务目标文件bomb.c
:bomb
的源文件,辅助理解bomb
代码
首先使用 objdump
反汇编工具对 bomb
进行反汇编,输出的汇编文件名为 dump.s
:
1 | objdump -d bomb > dump.s |
bomb.c
是 bomb
的部分源文件,包含主函数 main
。通读 main
函数以及注释,我们知道:
- 运行
bomb
时如果不带参数,我们将在标准输入中输入拆弹的字符串。 - 运行
bomb
时如果携带一个参数,则先从指定的文件中读取字符串 - 运行
bomb
时如果携带多于一个参数,则输出提示:bomb [<input_file>]
- 一共有 6 层炸弹,每一层通过输入一行字符串以进行拆弹
- 炸弹爆炸后不会有任何人受到伤害
- 可能存在隐藏关卡
此外,我们可以在炸弹运行时 Ctrl+C 中止拆弹,邪恶博士😈(Dr. Evil)最终还是会放我们走的:
1 | ^CSo you think you can stop the bomb with ctrl-c, do you? |
新建文件 solution.txt
,之后我们可以直接在里面写字符串答案(每行按序写上各层答案),从而不用手动输入字符串。
bomb.c
看完后,它的作用基本结束了。后面我们的时间将会花在人工解读 dump.s
文件以及使用 gdb 调试工具的过程中。
dump.s
文件阅读要点:
- 不需要看懂所有汇编代码(实际上也不可能),只需看懂关键函数部分的代码。
- 关键函数:各层炸弹函数
<phase_X>
,中间穿插的函数<funcX>
。为了便于自己的理解,有时候你也可以看一些工具类的函数,比如<read_six_numbers>
<strings_not_equal>
等,但这些函数看名字就知道其作用。 - 当运行到
<explode_bomb>
时炸弹爆炸,当该层函数安全结束返回到 main 运行<phase_defused>
时该层炸弹安全破解。 - 多注释和分行便于阅读
在破解某一层炸弹是,我们可以先随便输几个数,通过 GDB 工具推测我们的输入存放在栈中的哪个位置,这样也能方便推理。
Phase 1 - 寻找字符串
asm 代码阅读提示:左侧为程序地址与指令字节码,右侧为反汇编后得到的汇编代码。注释使用
;
以及#
。
1 | 08048b80 <phase_1>: |
使用 GDB 调试前记得打断点,比如:
b *0x8048b94
1 | (gdb) p (char*) 0x80499d8 |
解析及要点:
- 本层通过判断输入的字符串是否与指定字符串相等。
- 此层只需使用 GDB 指令找出地址
$0x80499d8
存储的字符串,该字符串就是本层答案。
点评与提示:
- 拆弹入门。
- 像遇到这种存在「魔数」的指令要多留心,常使用 GDB 打印该位置的内容,有可能存储关键字符串或数组。
- %eax 多用于存储函数返回结果
test %eax,%eax
:test
相当于and
指令,用于测试%eax
是否为 0。je
指令会判断ZF
标志位,当 ZF 为 1 时说明%eax
为 0,程序执行跳转。
Phase 2 - 有序数组
1 | 08048ba4 <phase_2>: |
断点打到 <read_six_numbers>
之后,比如 0x8048bbc
。我们随便输入 5 个数字,比如 1 6 11 16 21 26
,通过 GDB 可以判断其存储位置:
1 | # 下面展示了输入的 6 个数字的存储位置(示例数据) |
为啥会知道查 6 个地址。我们根据这个原则就行:汇编中用到哪里,GDB 就查哪里。
这里的汇编代码中存在一个典型的 for
循环结构,使用了「跳转到中间」策略(具体请看教材)。for 循环的通用形式:
1 | for(init-expr;test-expr;update-expr) |
该部分汇编代码结构翻译为 C 语言的大致结构如下:
1 | init-expr; |
解析及要点:
- 本层需输入 6 个数字,每个数字需构成升序的等差数列(公差为 5)
点评与提示:
- 识别
for
循环 - 知道输入的 6 个数字的存储位置
Phase 3 - 跳转表
相信你通过了前面汇编的洗礼后,汇编代码的阅读能力提高了不少。本层开始就不会啰嗦太多哦,看注释即可。
1 | 08048beb <phase_3>: |
查看魔数信息:
1 | (gdb) p (char*) 0x8049a09 |
这个字符串是不是很眼熟,这可能提示我们输入 3 个数据:1 个数字、1 个字符以及 1 个数字。我们可以像上一层那样随便输入 3 个数据。
1 | 8048c14: 08 |
汇编中用到哪里,GDB 就查哪里。
-0x24(%ebp)
指向我们第一个输入的数字。
像 jmp *%eax
这种语法很眼熟,像是课本中提到的 switch
语句的汇编。我们可以尝试找出其跳转表:
1 | (gdb) print /x ((int*) 0x8049a14)[0]@8 |
我们就随便选一个,比如 6。接下来我们看 6 的代码就行。
1 | # 0 |
汇编中用到哪里,GDB 就查哪里。
-0x10(%ebp)
指向我们第三个输入的数字。
1 | # 返回 |
汇编中用到哪里,GDB 就查哪里。
-0x11(%ebp)
指向我们第二个输入的字符。
解析及要点:
- 本层需输入 3 个数据。根据跳转表内容输入符合要求的数字。比如:
6 w 433
。
点评与提示:
- 识别
switch
- 善用
GDB
Phase 4 - 递归函数
第四层使用到了一个函数,我们先解读这个函数:
1 | 08048d17 <func4>: |
不难看出是个递归求斐波那契数列的函数:
1 | func4(x){ |
第四层的逻辑更加简单:
1 | 08048d57 <phase_4>: |
解析及要点:
- 本层需输入 1 个数据。使得函数的输出结果与给定数字相等。在这里为
13
点评与提示:
- 会读递归函数
Phase 5 - 算数
1 | 08048daa <phase_5>: |
我们看到后文老是出现 -0x8(%ebp)
,-0xc(%ebp)
。又看到熟悉的循环结构,干脆分别定义变量 result
,i
辅助我们进行分析。
1 | # 循环开始 |
1 | (gdb) p (char*) 0x804a9a0 |
0x804a9a0
是一个数组 arr 的地址,我们需要输入包含 6 个字符的字符串:
- 每个字符的低 4 位作为数组 arr 的索引 i。
- 本例中,需要使得
arr[0]
+arr[1]
+…+arr[5]
的总和为 36 即可通关。
例如:
- 36 = 2+2+6+6+10+10
- 各加数对应数组的序号为 0 0 2 2 1 1
- ASCII 中,
@
:0x40B
:0x42A
:0x41,也就是对应序号 0, 2, 1 - 答案即为
@@BBAA
解析及要点:
- 本层需输入合适字符串指示数组的下标,使得下标对应数组数字之和等于指定值即可。
- 知道输入字符串的存储位置
点评与提示:
- 汇编的融会贯通
Phase 6 - 链表排序
1 | 08048e08 <phase_6>: |
本例中出现的魔数 0x804a63c
为链表头结点地址,通过 GDB 我们可以得知链表结点地址以及存储的值:
1 | ; 0x804a63c 0x329 |
输入的数组要求:
- 6 个数字互不相同,范围 1~6
- 第 n 个数字值 v,代表第 n 个结点排序后的位置为 v
- 结点排序后的结果为:结点代表的值降序排序
解析及要点:
- 本层需输入 6 个数字对链表进行排序,使得排序结果满足目标要求
- 必须识别出这是个链表
- 逐个循环阅读
点评与提示:
- 耐心耐心耐心
Secret Phase
拆除炸弹各层的提示语如下:
1 | Welcome to my fiendish little bomb. You have 6 phases with |
但这并不意味着炸弹已经拆除完成,注意到 bomb.c
文件中有邪恶博士😈在最后一层炸弹拆除时留下的注释:
1 | /* Wow, they got it! But isn't something... missing? Perhaps |
反汇编得到的汇编代码中还存在一些隐藏内容:<func7>
以及 <secret_phase>
函数。
这部分就留给读者自己解决啦~~ 反正助教通过我的实验了,为了「完美主义」,我有时间在弄吧。
看起来不多呢。
1 | 08048f38 <fun7>: |
后续任务
- 有时间再完善隐藏层部分的内容
本文参考
- CSAPP | Lab2-Bomb Lab 深入解析 - 知乎 (zhihu.com) 文章提示了我第六层那个数据结构是个链表😅