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. |