CSAPP-Lab-2 实验报告: bomblab
摘要
本文介绍了笔者在做 bomblab 一节的实验报告。该 lab 要求通过 gdb 等工具,调试二进制执行文件,阅读理解汇编指令,并找到避免炸弹的正确口令,阻止炸弹爆炸。
phase 1
首先,通过阅读代码,可以发现 phase_1 这个函数内对读取的口令进行检查,如果函数正确退出,则拆弹完成。使用 gdb 对该函数反汇编可以得到如下
1 | Dump of assembler code for function phase_1: |
可以发现,核心是通过 strings_not_equal 比较口令与 0x402400 地址的字符串是否相同,若不同则引发爆炸。虽然直觉上可以这么理解,但验证正确需要更谨慎一点,需要确认:
strings_not_equal是不是如其名,判断字符串相同strings_not_equal的返回值,是否 0 代表相同
详细的确认需要逐句读汇编,也可以简单确认一下,直接在 0x0000000000400eee 处打断点。输入时输入 0x402400 地址对应的字符串,观察 \(eax\) 的值即可。测试发现即可直接通过,可以节省阅读汇编的时间。
phase 2
难度略微加大。同样地,首先反汇编 phase_2,结果如下:
1 | Dump of assembler code for function phase_2: |
可以看到,核心是调用了 read_six_numbers 函数,这个函数需要两个参数,第一个是 read_line 得到的字符串,存储在 rdi 中没有被修改,第二个是 rsi,在栈 rsp 上开辟了 0x28 的空间,猜测应该是一个大小为 10 的 int 数组。同时观察到后续没有使用 eax 的逻辑,这个函数应该是无返回值的。接着反汇编 read_six_numbers,结果如下:
1 | Dump of assembler code for function read_six_numbers: |
可以看到核心逻辑在 + 54 附近,对__isoc99_sscanf@plt 的返回值进行校验,如果不满足 >5,则引爆炸弹。sscanf 的函数签名如下:
1 | int sscanf(const char *str, const char *format, ...) |
即从 str 读取格式化输入,返回成功匹配和赋值的个数。打印 0x4025c3 处的模板字符串可知,格式为 "% d % d % d % d % d % d",符合这个函数的命名。因此第一步,口令需要按这个格式以六个整数开头。
回到 phase_2,发现还有两个导致炸弹引爆的因素:
- +18:第一个元素值不为 1
- +34:两个值不相等
阅读 + 52 到 + 62,再到 + 34 可知,第二个约束时要求数组的下一个元素需要是这一个的两倍大,进而可以得到正确的口令。
phase 3
与 Phase 2 类似,也是考察 sscanf。反汇编 phase_3 得到:
1 | Dump of assembler code for function phase_3: |
可以发现,只需要根据第一个数确定 eax 取值,也就是第二个数,就可以通过这个 phase。根据 jmpq 猜测源码中有 switch,jmpq 下面的部分就是跳转表。
phase 4
首先反汇编 phase4:
1 | Dump of assembler code for function phase_4: |
调用 func4 之前的代码很好理解,依然是读数 + 校验。func4 接收三个参数,分别是 num1、0、0xe,返回值若不为 0 则会爆炸。func4 反汇编如下:
1 | Dump of assembler code for function func4: |
可以看出,func4 会递归调用本身。首先要思考,func4 在什么情况下不会调用自己,可以发现:
- +22->+36:ecx<=edi=num1
- +43->+57:ecx>=edi
两个均成立时,ecx=edi,返回值 eax=0,那么只需要根据 ecx 确定 num1 即可。
phase 5
首先反汇编:
1 | Dump of assembler code for function phase_5: |
可以看出,输入的口令需要是一个长度为 6 的字符串,在循环内部对字符串每一位进行了修改,具体是取 ascii%16 的值,作为另一个字符串的下标索引,映射得到新字符串。反过来,就可以得到原始口令。%16 的结果应该为 9,15,14,5,6,7,对应到 ASCII 即可。
phase 6
老规矩,先反汇编:
1 | Dump of assembler code for function phase_6: |
前面的部分很好理解,要求输入数组 int a[6] 内部不能有重复元素,且 a[i] <= 6,然后再做变换 a[i]=7-a[i]。
从 + 123 开始的循环,可以看出是要根据 a[i] 初始化一个位于 0x20 的 void * 数组 B,此时还不知道 B 的每个元素指向的是什么。当 a[i]>1 时,+130 处的循环涉及多次寻址,比较复杂,而且难以理解, 可以通过观察打表看 B 的取值。
首先输入 "6 5 4 3 2 1",转换为 "1 2 3 4 5 6",然后打印 B 的值依次为:
1 | (gdb) print *(int **) ($sp+0x20) |
可以看出,随着 a[i]+=1,b[i]+=0x10。而且联想到 + 130 处的循环,0x8(%rdx),%rdx,可以猜测到 *B[i] 应该是一个结构体(大小超过了 8 Bytes),且内部含有指针(可以自更新),进而可以猜测 0x6032d0 这个地址就是链表头部。猜到这一点,后面理解起来就很容易了。
链表节点指向的值依次为
1 | (gdb) print **(int **) ($sp+0x20) |
注意,上面的 int 替换为 long,结果相同,说明有可能结构体里存储的是 int 并发生了对齐填充。
在 + 204 处的循环里,对链表进行了重排,使得 B[i].next=B[i+1]。+243 处判断重排后的链表需要满足 B[i].val>=B[i].next.val,否则会爆炸。只需根据上面的节点值,调整输入的值顺序,使得重排后的链表降序排列即可。
答案
分别是每个 phase 的答案,供参考,部分答案不唯一。
1 | Border relations with Canada have never been better. |