# Bomb Lab 笔记 这个 lab 给了一个名为 [bomb](../labs/bomb/bomb) 的程序文件,还有一个名为 [bomb.c](../labs/bomb/bomb.c) 的文件是题目要求和 bomb 实现的代码框架,无法编译。题目要求是运行 bomb 后输入六个 phase ,输入正确 bomb 程序才能继续运行,输入错误就会 bomb! 这里将结果保存在了 [result.txt](../labs/bomb/result.txt) 中,看源码可知可用 `unix> ./bomb result.txt` 来运行程序; 题目做法就是运用反编译得到该程序的汇编代码,然后通过分析汇编代码,看程序的实现,同时结合 `gdb` 调试,通过打断点、查看内存结果推测应该输入的 phase 。 反编译使用 `unix> objdump -d > obj.txt` ,这里 `obj.txt` 保存了汇编代码; `gdb` 调试常用的指令有: - `unix> gdb bomb` 运行 gdb 调试 bomb - `(gdb) run result.txt` 以参数 result.txt 调试 bomb - `break *0x40133f` 在 0x40133f 处设置断点 - `print /d $rsi` 以十进制输出寄存器 rsi 的值 - `print (char *) 0xbfff890` 输出以 0xbfff890 为首地址的字符串 更多指令可以参考 [gdb 指令](http://csapp.cs.cmu.edu/2e/docs/gdbnotes-x86-64.pdf) 参考资料: [马天猫的CS学习之旅](https://zhuanlan.zhihu.com/deeplearningcat) 分析汇编代码可以看出: `0000000000400ee0 :`在346行,phase_2 等函数紧跟其后 `0000000000400da0
:`在264行 `000000000040131b :`在688行 `0000000000401338 :`在701行 `000000000040145c :`在804行 ## phase1 phase1 函数将一个地址存入了 `%rsi`,然后调用了 `string_not_equal` 函数,而在该调用函数中又调用了 `string_length` 函数比较两端字符串的长度; ``` 401338: 41 54 push %r12 40133a: 55 push %rbp 40133b: 53 push %rbx 40133c: 48 89 fb mov %rdi,%rbx 40133f: 48 89 f5 mov %rsi,%rbp i01342: e8 d4 ff ff ff callq 40131b 401347: 41 89 c4 mov %eax,%r12d 40134a: 48 89 ef mov %rbp,%rdi 40134d: e8 c9 ff ff ff callq 40131b 401352: ba 01 00 00 00 mov $0x1,%edx 401357: 41 39 c4 cmp %eax,%r12d ``` 上段代码将存储了地址的 `%rdi` 和 `%rsi` 的值先后传到了 `string_length` 中, `%rdi` 即为原函数的第一个参数,即 `input` ,是输入的 phase 的首地址, `%rsi` 为设定的地址,是设定的 phase 的地址;上面的函数判断两个 phase 是否相同,不相同就 bomb! 因而将这个设定的 phase 打印出来就是结果了,设置一个断点,用 `print (char *) 0x402400` 打印出结果就得到了长度为 52 的答案了; 关于 `string_length` 函数: ``` 000000000040131b : 40131b: 80 3f 00 cmpb $0x0,(%rdi) 40131e: 74 12 je 401332 401320: 48 89 fa mov %rdi,%rdx 401323: 48 83 c2 01 add $0x1,%rdx 401327: 89 d0 mov %edx,%eax 401329: 29 f8 sub %edi,%eax 40132b: 80 3a 00 cmpb $0x0,(%rdx) 40132e: 75 f3 jne 401323 401330: f3 c3 repz retq 401332: b8 00 00 00 00 mov $0x0,%eax 401337: c3 retq ``` 该函数以 `%rdi` 为初始位置,通过循环不断比较 0 与该位置的值得到长度,返回到 `%eax`,打印出一个 `%eax` 得到 52 ,即为设定的长度; ## phase2 ``` 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 400f20: e8 15 05 00 00 callq 40143a 400f25: 48 83 c3 04 add $0x4,%rbx 400f29: 48 39 eb cmp %rbp,%rbx 400f2c: 75 e9 jne 400f17 ``` `phase_2` 调用了 `` 函数,来读取六个数字,将读取的数字存储在栈中,利用了上面的循环来判断数字是否符合设定; 上面的循环判断输入的后一项是否等于前一项的两倍,不相等就 bomb ,因而这打印出六个数的一个初始值就可以得到所有结果; 注意这里 ``` 400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) 400f0e: 74 20 je 400f30 ``` 要求了 `(%rsp)` 的值必须为 1 ,而它就是栈顶,也就是第一个输入的数,所以 `1 2 4 8 16 32` 就是本题的答案了。 ## phase3 这题输入两个数,判断是否符合; 这题运用了一个间接跳转: ``` 400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) ``` 以输入第一个数为索引,要求第一个数不超过 7 ,然后计算跳转地址,跳转后会设定第二个数的值,输入的第二个数与设定值不同就 bomb ;这里我没有看懂跳转后的地址,但通过不断打断点尝试,发现当第一个数为 0 时,跳转到了: ``` 400f7c: b8 cf 00 00 00 mov $0xcf,%eax 400f81: eb 3b jmp 400fbe ``` 这里将第二个数设置为 0xcf ,即 `0 207` 是符合的答案,这题答案不唯一。 ## phase4 这题输入两个数字,存入栈中; ``` 401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) 401056: 74 05 je 40105d ``` 从这里看出第二个数为 0 ,第一个数由调用的 `func4` 函数判断,返回值不为 0 时 bomb! ``` 0000000000400fce : 400fce: 48 83 ec 08 sub $0x8,%rsp 400fd2: 89 d0 mov %edx,%eax 400fd4: 29 f0 sub %esi,%eax 400fd6: 89 c1 mov %eax,%ecx 400fd8: c1 e9 1f shr $0x1f,%ecx 400fdb: 01 c8 add %ecx,%eax 400fdd: d1 f8 sar %eax 400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx 400fe2: 39 f9 cmp %edi,%ecx 400fe4: 7e 0c jle 400ff2 400fe6: 8d 51 ff lea -0x1(%rcx),%edx 400fe9: e8 e0 ff ff ff callq 400fce ``` `func4` 函数在 `%ecx` 中利用了一个递归分别存入 7 , 3 , 1 ,并使用跳转使得,第一个数小于7时不断递归且 `%ecx`小于等于第一个数,而跳转后即在下面的代码中会要求 `%ecx` 大于等于第一个数,否则递归,递归过程会设置eax使得其不为 0 ,所以只有当第一数等于 `%ecx` 时即 1 , 3 , 7 时才能使最后返回值为 0 ; ``` 400ff7: 39 f9 cmp %edi,%ecx 400ff9: 7d 0c jge 401007 400ff2: b8 00 00 00 00 mov $0x0,%eax 40104d: 85 c0 test %eax,%eax 40104f: 75 07 jne 401058 ``` 本题答案不唯一,可以为 `1 0` 或 `3 0` 或 `7 0` 。 ## phase5 这题中 `mov %fs:0x28,%rax` 为设定的 canary 值,可以忽视;这题通过对输入的 phase 进行一些运算操作,得到一个新的 phase ,判断完长度后将它的地址和设定的 phase 的地址传入 `strings_not_equal` 比较; ``` 4010b3: be 5e 24 40 00 mov $0x40245e,%esi 4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 4010bd: e8 76 02 00 00 callq 401338 ``` 这里看出设定的 phase 的地址是 0x40245e ,直接打印出句子 `flyers` ; 然后根据这个设定的 phase 得出操作前的 phase ; ``` 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx 40108f: 88 0c 24 mov %cl,(%rsp) 401092: 48 8b 14 24 mov (%rsp),%rdx 401096: 83 e2 0f and $0xf,%edx 401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx 4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) 4010a4: 48 83 c0 01 add $0x1,%rax 4010a8: 48 83 f8 06 cmp $0x6,%rax 4010ac: 75 dd jne 40108b ``` 上面的循环将输入的 phase,即一个数组 `input[n]` 的每个值做 `&0xf` 运算存到了 `%edx` 中,即提取出一个四位,然后以这个四位为偏移量访问内存中一段,将 `0x4024b0(%rdx)` 的值的 `%dl ` 存到栈上的数组 `0x10(%rsp,%rax,1)` 里,这个数组就是了新的 phase ; 打印访问的内存,有: ``` (gdb) p (char *) 0x4024b0 $1 = 0x4024b0 "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?" ``` 设定的 phase 就对应着地址偏移 `9,15,14,5,6,7` 位,而注意到 9 的 ASCII 码就是 0x39 ,提取出的四位正好是 9 ,经过上面的操作就可以得到一个字母 `f`,将其他偏移量对应于 ASCII 码就能得到结果 9?>567 ,就是答案了。 ## phase6 这个函数代码比较长,分几段理解: #### 1.读入六个数的数组; #### 2.一个双重循环; ``` 401114: 4c 89 ed mov %r13,%rbp 401117: 41 8b 45 00 mov 0x0(%r13),%eax 40111b: 83 e8 01 sub $0x1,%eax 40111e: 83 f8 05 cmp $0x5,%eax 401121: 76 05 jbe 401128 401123: e8 12 03 00 00 callq 40143a 401128: 41 83 c4 01 add $0x1,%r12d 40112c: 41 83 fc 06 cmp $0x6,%r12d 401130: 74 21 je 401153 401132: 44 89 e3 mov %r12d,%ebx 401135: 48 63 c3 movslq %ebx,%rax 401138: 8b 04 84 mov (%rsp,%rax,4),%eax 40113b: 39 45 00 cmp %eax,0x0(%rbp) 40113e: 75 05 jne 401145 401140: e8 f5 02 00 00 callq 40143a 401145: 83 c3 01 add $0x1,%ebx 401148: 83 fb 05 cmp $0x5,%ebx 40114b: 7e e8 jle 401135 40114d: 49 83 c5 04 add $0x4,%r13 401151: eb c1 jmp 401114 ``` 这里外层的循环要求六个数都小于等于 6 ,由于这里 `jbe` 是对无符号数操作,另外数为 0 时在循环判断条件处会溢出,所以这六个数都应为正数,即 1-6 范围,嵌套的内循环要求六个数两两不等,否则 bomb! #### 3.对数组操作的循环 ``` 401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi 401158: 4c 89 f0 mov %r14,%rax 40115b: b9 07 00 00 00 mov $0x7,%ecx 401160: 89 ca mov %ecx,%edx 401162: 2b 10 sub (%rax),%edx 401164: 89 10 mov %edx,(%rax) 401166: 48 83 c0 04 add $0x4,%rax 40116a: 48 39 f0 cmp %rsi,%rax 40116d: 75 f1 jne 401160 ``` 这个循环使得读入的数组的每一项 `a[i]` 都变为 `7-a[i]` ;注意这是 16 进制, 0x18 就是输入的数组的边界; #### 4.利用一个链表提取出操作后数组的顺序 ``` 40116f: be 00 00 00 00 mov $0x0,%esi 401174: eb 21 jmp 401197 401176: 48 8b 52 08 mov 0x8(%rdx),%rdx 40117a: 83 c0 01 add $0x1,%eax 40117d: 39 c8 cmp %ecx,%eax 40117f: 75 f5 jne 401176 401181: eb 05 jmp 401188 401183: ba d0 32 60 00 mov $0x6032d0,%edx 401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) 40118d: 48 83 c6 04 add $0x4,%rsi 401191: 48 83 fe 18 cmp $0x18,%rsi 401195: 74 14 je 4011ab 401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx 40119a: 83 f9 01 cmp $0x1,%ecx 40119d: 7e e4 jle 401183 40119f: b8 01 00 00 00 mov $0x1,%eax 4011a4: ba d0 32 60 00 mov $0x6032d0,%edx 4011a9: eb cb jmp 401176 ``` 这里的操作,有点类似于桶排序,而这里的桶是内存中的一个链表,这个链表有六个地址,`0x6032d0,0x6032e0, 0x6032f0,0x603300,0x603310,0x603320`,并且相邻的两地址有关系 `*(0x6032e0+8)=0x6032f0` 成立,以此类推,画个图就是个链表了,头结点是 `0x6032d0`;上面这个循环按六个数的顺序将这六个地址依次存入了栈的不同位置,使得 `0x6032d0` 对应于 1 (同时若是对应于操作后数组的 a[i] ,该地址就存入栈上初地址偏移的 i 位),`0x6032e0` 就对应于 2 以此类推,这样栈上各个地址所存入的偏移量就对应着中操作后数组的数,链表在栈上的顺序就对应着操作后数组中数的顺序。 #### 5.根据栈上链表的顺序修改链表 ``` 4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx 4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax 4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi 4011ba: 48 89 d9 mov %rbx,%rcx 4011bd: 48 8b 10 mov (%rax),%rdx 4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) 4011c4: 48 83 c0 08 add $0x8,%rax 4011c8: 48 39 f0 cmp %rsi,%rax 4011cb: 74 05 je 4011d2 4011cd: 48 89 d1 mov %rdx,%rcx 4011d0: eb eb jmp 4011bd ``` 这个循环把前一个栈中的地址加 0x8 取值后设为后一个栈中的地址,`movq $0x0,0x8(%rdx)` 把最后一个地址加0x8取值后设为 0 ,就是将栈上存入的第一个地址设为了链表的头结点,栈上下一个地址就设为了下一个结点,最后一个结点设为空,这样链表的结构就跟据栈上的存入情况改变了,链表节点的顺序就是操作后数组的顺序。 #### 6.遍历链表 ``` 011da: bd 05 00 00 00 mov $0x5,%ebp 4011df: 48 8b 43 08 mov 0x8(%rbx),%rax 4011e3: 8b 00 mov (%rax),%eax 4011e5: 39 03 cmp %eax,(%rbx) 4011e7: 7d 05 jge 4011ee 4011e9: e8 4c 02 00 00 callq 40143a 4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx 4011f2: 83 ed 01 sub $0x1,%ebp 4011f5: 75 e8 jne 4011df ``` 这里遍历了一遍链表,判断下一结点取得的值是否会小于等于这一结点取得的值时,不满足的话就 bomb ,因此输入的数组的顺序经过一些操作后应使得修改后的链表的值满足从大到小的顺序,打印下这几个地址取得的值,有: ``` (gdb) p /d *0x6032d0 $26 = 332 (gdb) p /d *0x6032e0 $27 = 168 (gdb) p /d *0x6032f0 $28 = 924 (gdb) p /d *0x603300 $29 = 691 (gdb) p /d *0x603310 $30 = 477 (gdb) p /d *0x603320 $31 = 443 ``` 从大到小排序是: 0x6032f0,对应操作后的数组的 3 对应输入数组的 4 ; 0x603300,对应操作后的数组的 4 对应输入数组的 3 ; 0x603310,对应操作后的数组的 5 对应输入数组的 2 ; 0x603320,对应操作后的数组的 6 对应输入数组的 1 ; 0x6032d0,对应操作后的数组的 1 对应输入数组的 6 ; 0x6032e0,对应操作后的数组的 2 对应输入数组的 5 ; 所以原输入数组为 `4 3 2 1 6 5` ## 小结 通过这个 lab ,我学到了用 `gdb` 调试的方法,同时也对汇编知识更加熟悉了,做这个 lab 最重要的是要耐心和细心。