之前的 Bomb Lab 没有做,现在刚好在上计算机系统基础这门课,学校使用了 CSAPP 的 Bomb Lab ,可以补交一下 CSAPP 的作业,也可以学习一下 GDB 的使用。
实验简述
遗憾地,CSAPP 的 Bomb Lab 早已提供的是 Linux 的 64 位可执行程序,而我校的 Lab 还是 32 位的 Windows PE32 。不过我还是找老师要到了它的 Linux 版本,不过也是 ELF32 ,可执行文件的名字叫bomb_32
。
该 Lab 是 CSAPP Bomb Lab 的老版 32 位程序,所以之后我还是看一下新版的 Bomb Lab 较好,不能完全不了解 64 位汇编嘛 :(
实验环境
- GNU gdb (GDB) Fedora 12.1-2.fc36
- GNU objdump version 2.37-36.fc36
实验过程
首先反汇编生成汇编代码:
$ objdump -S -C bomb_32 > bomb.disas
由于我不是很会看 AT&T 的汇编代码,所以我的 objdump 是 aliased 的,默认使用 Intel 的语法。
$ which objdump
objdump='objdump --disassembler-options=intel'
/usr/bin/objdump
gdb 也默认使用 Intel 的语法:
$ cat ~/.gdbinit
set disassembly-flavor intel
再使用 gdb 开始调试程序:
$ gdb --args ./bomb_32 input
phase_1
由于老师给了这个程序运行的流程图,我也看过 Bomb Lab 中提供的 .c 文件,所以我知道每个字符串都在phase_n
的函数中输入或者检验。
可以直接查看phase_1
函数的反汇编代码,并注意到这个函数中的一个函数调用:
(gdb) disas phase_1
Dump of assembler code for function phase_1:
......
0x08048b2c <+12>: push 0x80497c0
0x08048b31 <+17>: push eax
0x08048b32 <+18>: call 0x8049030 <strings_not_equal>
......
可以看到它在压入两个参数之后调用了一个函数strings_not_equal
,我们直接查看0x80497c0
里面是什么内容,我首先猜测它是一个字符串:
(gdb) p (char *)0x80497c0
$1 = 0x80497c0 "Public speaking is very easy."
在之后的测试中,这个字符串也让我过了第一关。
phase_2
直接查看phase_2
的汇编代码:
; phase_2 的参数为字符串 input ,在 [ebp+0x8]
08048b48 <phase_2>:
8048b48: 55 push ebp
8048b49: 89 e5 mov ebp,esp
; 为局部变量腾出空间
8048b4b: 83 ec 20 sub esp,0x20
; 可能是内部调用的函数需要使用这两个寄存器
; 所以根据 caller save 在这里保存
8048b4e: 56 push esi
8048b4f: 53 push ebx
; 将 input 放入 edx 中
8048b50: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
; 等于 sub esp, 0x8
8048b53: 83 c4 f8 add esp,0xfffffff8
; 将局部变量的地址 [ebp-0x18] 放入 eax 中
; 不妨给它起名为变量 A
; 由于给局部变量预留的位置很大,猜测 A 是一个数组
8048b56: 8d 45 e8 lea eax,[ebp-0x18]
; 压栈传参,传入的是 input, A
8048b59: 50 push eax
8048b5a: 52 push edx
; 调用函数 read_six_numbers 根据名字合理怀疑这里是读入六个数字,A 可能有 6 个元素
8048b5b: e8 78 04 00 00 call 8048fd8 <read_six_numbers>
; 这里 esp 加了 0x10 相当于把之前的参数退栈了,还把之前 add esp,0xfffffff8 的加回去了。我猜测之前的 add 可能和内存对齐有关
8048b60: 83 c4 10 add esp,0x10
; 将 A[0] 与 1 比较
; 由于之前传递的是地址,这个变量应该在 read_six_numbers 中被修改了
8048b63: 83 7d e8 01 cmp DWORD PTR [ebp-0x18],0x1
; 如果是 1 则成功,不是 1 则引爆炸弹
8048b67: 74 05 je 8048b6e <phase_2+0x26>
8048b69: e8 8e 09 00 00 call 80494fc <explode_bomb>
; ebx <- 0x1
8048b6e: bb 01 00 00 00 mov ebx,0x1
; 将 A 放入 esi 中
8048b73: 8d 75 e8 lea esi,[ebp-0x18]
; eax <- 0x2
8048b76: 8d 43 01 lea eax,[ebx+0x1]
; 这步骤操作之后 eax 的值为 (ebx + 1) * A[ebx - 1]
; ebx 初始为 1
8048b79: 0f af 44 9e fc imul eax,DWORD PTR [esi+ebx*4-0x4]
; 相当于判断 A[ebx] 是不是 A[ebx - 1] 两倍
8048b7e: 39 04 9e cmp DWORD PTR [esi+ebx*4],eax
8048b81: 74 05 je 8048b88 <phase_2+0x40>
8048b83: e8 74 09 00 00 call 80494fc <explode_bomb>
8048b88: 43 inc ebx
8048b89: 83 fb 05 cmp ebx,0x5
; 我读到这里才发现是一个循环,终止条件为 ebx > 5
8048b8c: 7e e8 jle 8048b76 <phase_2+0x2e>
; 所以根据递推公式可知字符串为 1 2 6 24 120 720
8048b8e: 8d 65 d8 lea esp,[ebp-0x28]
8048b91: 5b pop ebx
8048b92: 5e pop esi
8048b93: 89 ec mov esp,ebp
8048b95: 5d pop ebp
8048b96: c3 ret
8048b97: 90 nop
它调用了函数read_six_numbers
,所以我们也分析一下它:
; 参数为字符串 input 和数组 A
08048fd8 <read_six_numbers>:
8048fd8: 55 push ebp
8048fd9: 89 e5 mov ebp,esp
8048fdb: 83 ec 08 sub esp,0x8
; 传入的输入字符串
8048fde: 8b 4d 08 mov ecx,DWORD PTR [ebp+0x8]
; 传入的数组 A
8048fe1: 8b 55 0c mov edx,DWORD PTR [ebp+0xc]
; A[5]
8048fe4: 8d 42 14 lea eax,[edx+0x14]
8048fe7: 50 push eax
; A[4]
8048fe8: 8d 42 10 lea eax,[edx+0x10]
8048feb: 50 push eax
; A[3]
8048fec: 8d 42 0c lea eax,[edx+0xc]
8048fef: 50 push eax
; A[2]
8048ff0: 8d 42 08 lea eax,[edx+0x8]
8048ff3: 50 push eax
; A[1]
8048ff4: 8d 42 04 lea eax,[edx+0x4]
8048ff7: 50 push eax
; A[0]
8048ff8: 52 push edx
; format 字符串 "%d %d %d %d %d %d"
8048ff9: 68 1b 9b 04 08 push 0x8049b1b
; input 输入字符串
8048ffe: 51 push ecx
8048fff: e8 5c f8 ff ff call 8048860 <sscanf@plt>
8049004: 83 c4 20 add esp,0x20
8049007: 83 f8 05 cmp eax,0x5
804900a: 7f 05 jg 8049011 <read_six_numbers+0x39>
804900c: e8 eb 04 00 00 call 80494fc <explode_bomb>
8049011: 89 ec mov esp,ebp
8049013: 5d pop ebp
8049014: c3 ret
8049015: 8d 76 00 lea esi,[esi+0x0]
使用字符串1 2 6 24 120 720
成功通过第二关。
phase_3
; phase_3 的参数为字符串 input ,在 [ebp+0x8]
08048b98 <phase_3>:
8048b98: 55 push ebp
8048b99: 89 e5 mov ebp,esp
; 预留 20 个字节
8048b9b: 83 ec 14 sub esp,0x14
8048b9e: 53 push ebx
; 将 input 放入 edx
8048b9f: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
; sub esp, 0x4
8048ba2: 83 c4 f4 add esp,0xfffffff4
;
; int_b
8048ba5: 8d 45 fc lea eax,[ebp-0x4]
8048ba8: 50 push eax
; char_c
8048ba9: 8d 45 fb lea eax,[ebp-0x5]
8048bac: 50 push eax
; int_a
8048bad: 8d 45 f4 lea eax,[ebp-0xc]
8048bb0: 50 push eax
; 通过 GBD 调试可以知道这个地址内容为 "%d %c %d"
8048bb1: 68 de 97 04 08 push 0x80497de
; input
8048bb6: 52 push edx
8048bb7: e8 a4 fc ff ff call 8048860 <sscanf@plt>
8048bbc: 83 c4 20 add esp,0x20
; 如果 sscanf 匹配并赋值的个数不多于 2 ,则爆炸
8048bbf: 83 f8 02 cmp eax,0x2
8048bc2: 7f 05 jg 8048bc9 <phase_3+0x31>
8048bc4: e8 33 09 00 00 call 80494fc <explode_bomb>
; 将 int_a 与 0x7 比较
; 怀疑是一个防止数组越界的操作
8048bc9: 83 7d f4 07 cmp DWORD PTR [ebp-0xc],0x7
; 如果大于则爆炸
8048bcd: 0f 87 b5 00 00 00 ja 8048c88 <phase_3+0xf0>
; 将 int_a 赋值给 eax
8048bd3: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc]
; 这里存在一个跳转表,对应到 C 语言中应该是一个 switch-case 结构
; 可以在 gdb 里面查看一下对应的表 `(gdb) p/x *(int *)0x80497e8`
; [0x80497e8] 对应 8048be0
; [0x80497e8+1*4] 对应 8048c00
; [0x80497e8+2*4] 对应 8048c16
; [0x80497e8+3*4] 对应 8048c28
; [0x80497e8+4*4] 对应 8048c40
; [0x80497e8+5*4] 对应 8048c52
; [0x80497e8+6*4] 对应 8048c64
; [0x80497e8+7*4] 对应 8048c76
; switch-case(int_a)
8048bd6: ff 24 85 e8 97 04 08 jmp DWORD PTR [eax*4+0x80497e8]
8048bdd: 8d 76 00 lea esi,[esi+0x0]
; case 0:
; bl <- 0x71
8048be0: b3 71 mov bl,0x71
; 将 int_b 与 0x309 比较
8048be2: 81 7d fc 09 03 00 00 cmp DWORD PTR [ebp-0x4],0x309
; 如果等于就 break
8048be9: 0f 84 a0 00 00 00 je 8048c8f <phase_3+0xf7>
8048bef: e8 08 09 00 00 call 80494fc <explode_bomb>
8048bf4: e9 96 00 00 00 jmp 8048c8f <phase_3+0xf7>
; eiz 是一个伪寄存器,它始终为 0
8048bf9: 8d b4 26 00 00 00 00 lea esi,[esi+eiz*1+0x0]
; 后面几个 case 都是相同的道理
......
; bl 与 char_c 比较
8048c8f: 3a 5d fb cmp bl,BYTE PTR [ebp-0x5]
; 所以基本流程就是根据 int_a 的值确定进去哪一个 case ,
; 然后在该分支中将 int_b 和某一值比较,并确定 char_c 应该等于的值
; 所以答案应该不唯一,通过 int_a 的值确定后面两个答案
......
我使用0 q 777
通过了这一关。
phase_4
这一关包含了递归函数,但是挑战性不是很大。
; phase_4 的参数为字符串 input ,在 [ebp+0x8]
08048ce0 <phase_4>:
8048ce0: 55 push ebp
8048ce1: 89 e5 mov ebp,esp
8048ce3: 83 ec 18 sub esp,0x18
; input -> edx
8048ce6: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
; sub esp, 0xc
8048ce9: 83 c4 fc add esp,0xfffffffc
; 不妨令该局部变量为 A
8048cec: 8d 45 fc lea eax,[ebp-0x4]
8048cef: 50 push eax
; 根据 GDB `p (char *)0x8049808` 可得该字符串为 `%d`
8048cf0: 68 08 98 04 08 push 0x8049808
8048cf5: 52 push edx
8048cf6: e8 65 fb ff ff call 8048860 <sscanf@plt>
8048cfb: 83 c4 10 add esp,0x10
; 判断 sscanf 是否成功
8048cfe: 83 f8 01 cmp eax,0x1
8048d01: 75 06 jne 8048d09 <phase_4+0x29>
; 判断 A 和 0x0 ,如果不大于 0 则爆炸
8048d03: 83 7d fc 00 cmp DWORD PTR [ebp-0x4],0x0
8048d07: 7f 05 jg 8048d0e <phase_4+0x2e>
8048d09: e8 ee 07 00 00 call 80494fc <explode_bomb>
; sub esp, 0x4
8048d0e: 83 c4 f4 add esp,0xfffffff4
; A -> eax
8048d11: 8b 45 fc mov eax,DWORD PTR [ebp-0x4]
; 参数入栈并调用 func4
8048d14: 50 push eax
8048d15: e8 86 ff ff ff call 8048ca0 <func4>
8048d1a: 83 c4 10 add esp,0x10
; 返回值如果不是 55 则爆炸
8048d1d: 83 f8 37 cmp eax,0x37
8048d20: 74 05 je 8048d27 <phase_4+0x47>
8048d22: e8 d5 07 00 00 call 80494fc <explode_bomb>
8048d27: 89 ec mov esp,ebp
8048d29: 5d pop ebp
8048d2a: c3 ret
8048d2b: 90 nop
现在我们来看看func4
; phase_4 的参数为整型 A ,在 [ebp+0x8]
08048ca0 <func4>:
8048ca0: 55 push ebp
8048ca1: 89 e5 mov ebp,esp
8048ca3: 83 ec 10 sub esp,0x10
8048ca6: 56 push esi
8048ca7: 53 push ebx
; ebx <- A
8048ca8: 8b 5d 08 mov ebx,DWORD PTR [ebp+0x8]
; 如果 A 不大于 1 则跳转到 退出,这里应该是递归出口,返回值为 1
8048cab: 83 fb 01 cmp ebx,0x1
8048cae: 7e 20 jle 8048cd0 <func4+0x30>
8048cb0: 83 c4 f4 add esp,0xfffffff4
8048cb3: 8d 43 ff lea eax,[ebx-0x1]
8048cb6: 50 push eax
; func(A - 1)
8048cb7: e8 e4 ff ff ff call 8048ca0 <func4>
8048cbc: 89 c6 mov esi,eax
8048cbe: 83 c4 f4 add esp,0xfffffff4
8048cc1: 8d 43 fe lea eax,[ebx-0x2]
8048cc4: 50 push eax
; func(A - 2)
; 写到这里可以猜到这是斐波那契数列,
; 之前要求返回值是 55 ,所以输入应该是 9
8048cc5: e8 d6 ff ff ff call 8048ca0 <func4>
8048cca: 01 f0 add eax,esi
8048ccc: eb 07 jmp 8048cd5 <func4+0x35>
8048cce: 89 f6 mov esi,esi
8048cd0: b8 01 00 00 00 mov eax,0x1
8048cd5: 8d 65 e8 lea esp,[ebp-0x18]
8048cd8: 5b pop ebx
8048cd9: 5e pop esi
8048cda: 89 ec mov esp,ebp
8048cdc: 5d pop ebp
8048cdd: c3 ret
8048cde: 89 f6 mov esi,esi
输入9
成功通过了这一关。
phase_5
; phase_5 的参数为字符串 input ,在 [ebp+0x8]
08048d2c <phase_5>:
8048d2c: 55 push ebp
8048d2d: 89 e5 mov ebp,esp
8048d2f: 83 ec 10 sub esp,0x10
8048d32: 56 push esi
8048d33: 53 push ebx
; input -> ebx
8048d34: 8b 5d 08 mov ebx,DWORD PTR [ebp+0x8]
8048d37: 83 c4 f4 add esp,0xfffffff4
8048d3a: 53 push ebx
; 计算 input 字符串长度,如果长度不为 6 则爆炸
8048d3b: e8 d8 02 00 00 call 8049018 <string_length>
8048d40: 83 c4 10 add esp,0x10
8048d43: 83 f8 06 cmp eax,0x6
8048d46: 74 05 je 8048d4d <phase_5+0x21>
8048d48: e8 af 07 00 00 call 80494fc <explode_bomb>
; 小知识:由于两数相同,所以使用 xor 来清零快一些
8048d4d: 31 d2 xor edx,edx
; 不妨令这个变量为 str_a
8048d4f: 8d 4d f8 lea ecx,[ebp-0x8]
; 地址 0x804b220 "isrveawhobpnutfg\260\001"
; 怀疑是通过一些操作从上述字符串中取出字符重新拼接
; 不妨令这个字符串为 dict
8048d52: be 20 b2 04 08 mov esi,0x804b220
; 从这里开始是循环六次,后面的字符串常量长度也刚好为 6
; al <- input[edx]
8048d57: 8a 04 1a mov al,BYTE PTR [edx+ebx*1]
; 取 al 低四位
8048d5a: 24 0f and al,0xf
; 拓展长度到 32 位
8048d5c: 0f be c0 movsx eax,al
; al <- dict[eax]
8048d5f: 8a 04 30 mov al,BYTE PTR [eax+esi*1]
; str_a[edx] <- al
8048d62: 88 04 0a mov BYTE PTR [edx+ecx*1],al
8048d65: 42 inc edx
8048d66: 83 fa 05 cmp edx,0x5
8048d69: 7e ec jle 8048d57 <phase_5+0x2b>
; 循环结束
; 给字符串 str_a 结尾补上 '\0'
8048d6b: c6 45 fe 00 mov BYTE PTR [ebp-0x2],0x0
8048d6f: 83 c4 f8 add esp,0xfffffff8
; 地址 0x804980b "giants"
8048d72: 68 0b 98 04 08 push 0x804980b
8048d77: 8d 45 f8 lea eax,[ebp-0x8]
8048d7a: 50 push eax
; 比较 str_a 和 "giants"
; 所以输入字符串只看最低四位应该是 15 0 5 11 13 1 ,十六进制是 f 0 5 b d 1
; 查看 ASCII 表,找到方便输入的内容,a-z 及其附近的字符很符合要求
; 所以使用 o`ekma
8048d7b: e8 b0 02 00 00 call 8049030 <strings_not_equal>
......
输入o`ekma
成功通过了这一关。
phase_6
这一关比较长,我将它分为几个部分。
一开始就能看到局部变量被赋值到一个神秘的地址,这应该解这题的关键。之后使用了read_six_numbers
这个函数读入六个数字。
; phase_6 的参数为字符串 input ,在 [ebp+0x8]
08048d98 <phase_6>:
......
; input <- edx
8048da1: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
; *(int *)0x804b26c 153
; 不妨令该局部变量为指针 C
; C <- 0x804b26c
; 它直接指向了一个神秘的结构
8048da4: c7 45 cc 6c b2 04 08 mov DWORD PTR [ebp-0x34],0x804b26c
8048dab: 83 c4 f8 add esp,0xfffffff8
; 不妨令该局部变量为数组 A
8048dae: 8d 45 e8 lea eax,[ebp-0x18]
8048db1: 50 push eax
8048db2: 52 push edx
8048db3: e8 20 02 00 00 call 8048fd8 <read_six_numbers>
碰到的第一个循环:
; for (int i = 0; i <= 5; ++i) {
; if (A[i] > 6)
; explode_bomb();
; for (int j = i + 1; j <= 5; ++j) {
; if (A[i] == A[j])
; explode_bomb();
; }
; }
; edi <- 0 不妨令其为 i
8048db8: 31 ff xor edi,edi
8048dba: 83 c4 10 add esp,0x10
8048dbd: 8d 76 00 lea esi,[esi+0x0]
; eax <- A
; 循环 6 次
8048dc0: 8d 45 e8 lea eax,[ebp-0x18]
; eax <- A[i]
8048dc3: 8b 04 b8 mov eax,DWORD PTR [eax+edi*4]
; eax <- A[i] - 1
8048dc6: 48 dec eax
; 如果 eax 大于 5 就爆炸,不能大于 6
8048dc7: 83 f8 05 cmp eax,0x5
8048dca: 76 05 jbe 8048dd1 <phase_6+0x39>
8048dcc: e8 2b 07 00 00 call 80494fc <explode_bomb>
; ebx <- i + 1 不妨令其为 j
8048dd1: 8d 5f 01 lea ebx,[edi+0x1]
; if (j <= 5)
8048dd4: 83 fb 05 cmp ebx,0x5
8048dd7: 7f 23 jg 8048dfc <phase_6+0x64>
; eax <- 4 * edi
8048dd9: 8d 04 bd 00 00 00 00 lea eax,[edi*4+0x0]
; 不妨令这个局部变量为 B
; B <- 4 * edi
8048de0: 89 45 c8 mov DWORD PTR [ebp-0x38],eax
; esi <- A
8048de3: 8d 75 e8 lea esi,[ebp-0x18]
; 内层循环
8048de6: 8b 55 c8 mov edx,DWORD PTR [ebp-0x38]
; eax <- A[i]
8048de9: 8b 04 32 mov eax,DWORD PTR [edx+esi*1]
; 比较 A[i] 和 A[j]
8048dec: 3b 04 9e cmp eax,DWORD PTR [esi+ebx*4]
; 如果相等则引爆炸弹
8048def: 75 05 jne 8048df6 <phase_6+0x5e>
8048df1: e8 06 07 00 00 call 80494fc <explode_bomb>
8048df6: 43 inc ebx
8048df7: 83 fb 05 cmp ebx,0x5
8048dfa: 7e ea jle 8048de6 <phase_6+0x4e>
; 结束内层循环
8048dfc: 47 inc edi
8048dfd: 83 ff 05 cmp edi,0x5
8048e00: 7e be jle 8048dc0 <phase_6+0x28>
; 结束循环
至此可以得出初步结论,第一个数字一定不大于 6 ,而且任意两个数字不相等
碰到第二个循环:
; E = D;
; for (int i = 0, p = C; i <= 5; ++i) {
; j = 1;
; while (j < A[i]) {
; p = p->next;
; j++;
; }
; E[i] = p;
; }
; edi <- 0 不妨令其为 i
8048e02: 31 ff xor edi,edi
; ecx <- A
8048e04: 8d 4d e8 lea ecx,[ebp-0x18]
; 不妨令该局部变量为数组 D
8048e07: 8d 45 d0 lea eax,[ebp-0x30]
; 不妨令该局部变量为指针 E
; E <- D
8048e0a: 89 45 c4 mov DWORD PTR [ebp-0x3c],eax
8048e0d: 8d 76 00 lea esi,[esi+0x0]
; 循环 6 次
; esi <- C 不妨令其为指针 p
; C 初值为 0x804b26c
8048e10: 8b 75 cc mov esi,DWORD PTR [ebp-0x34]
; ebx <- 1 不妨令其为 j
8048e13: bb 01 00 00 00 mov ebx,0x1
8048e18: 8d 04 bd 00 00 00 00 lea eax,[edi*4+0x0]
8048e1f: 89 c2 mov edx,eax
; 将 j 与 A[i] 比较
8048e21: 3b 1c 08 cmp ebx,DWORD PTR [eax+ecx*1]
8048e24: 7d 12 jge 8048e38 <phase_6+0xa0>
; 如果小于则执行下面这段
; eax <- A[i]
8048e26: 8b 04 0a mov eax,DWORD PTR [edx+ecx*1]
8048e29: 8d b4 26 00 00 00 00 lea esi,[esi+eiz*1+0x0]
; 内层循环
; 感觉是一个链表结构
; 猜测偏移为 0x8 的位置是 next 字段
; p <- p->next
8048e30: 8b 76 08 mov esi,DWORD PTR [esi+0x8]
; j++
8048e33: 43 inc ebx
; while (j < A[i])
8048e34: 39 c3 cmp ebx,eax
8048e36: 7c f8 jl 8048e30 <phase_6+0x98>
; 结束内层循环
; edx <- E
8048e38: 8b 55 c4 mov edx,DWORD PTR [ebp-0x3c]
; E[i] <- p
8048e3b: 89 34 ba mov DWORD PTR [edx+edi*4],esi
; i++
8048e3e: 47 inc edi
; while (i <= 5)
8048e3f: 83 ff 05 cmp edi,0x5
8048e42: 7e cc jle 8048e10 <phase_6+0x78>
; 结束循环
使用 GDB 查看一下那个神秘地址果然有了收获:
(gdb) x/3x 0x804b26c
0x804b26c <node1>: 0x000000fd 0x00000001 0x0804b260
(gdb) x/3x 0x804b260
0x804b260 <node2>: 0x000002d5 0x00000002 0x0804b254
(gdb) x/3x 0x804b254
0x804b254 <node3>: 0x0000012d 0x00000003 0x0804b248
(gdb) x/3x 0x804b248
0x804b248 <node4>: 0x000003e5 0x00000004 0x0804b23c
(gdb) x/3x 0x804b23c
0x804b23c <node5>: 0x000000d4 0x00000005 0x0804b230
(gdb) x/3x 0x804b230
0x804b230 <node6>: 0x000001b0 0x00000006 0x00000000
一共有六个节点
猜测三个字段分别为:value
, id
, next
碰到第三个循环:
; p = D;
; C = D;
; for (int i = 1; i <= 5; ++i) {
; p->next = D[i];
; p = p->next;
; }
; esi <- D 不妨令其为指针 p
8048e44: 8b 75 d0 mov esi,DWORD PTR [ebp-0x30]
; C <- D
8048e47: 89 75 cc mov DWORD PTR [ebp-0x34],esi
; edi <- 1 令其为 i
8048e4a: bf 01 00 00 00 mov edi,0x1
; edx <- D
8048e4f: 8d 55 d0 lea edx,[ebp-0x30]
; 循环 6 次
; eax <- D[i]
8048e52: 8b 04 ba mov eax,DWORD PTR [edx+edi*4]
; p->next <- D[i]
8048e55: 89 46 08 mov DWORD PTR [esi+0x8],eax
; p <- p->next
8048e58: 89 c6 mov esi,eax
; i++
8048e5a: 47 inc edi
; while (i <= 5)
8048e5b: 83 ff 05 cmp edi,0x5
8048e5e: 7e f2 jle 8048e52 <phase_6+0xba>
; 结束循环
; p->next = NULL;
8048e60: c7 46 08 00 00 00 00 mov DWORD PTR [esi+0x8],0x0
上面这部分代码相当于把数组D
串起来变为一个链表。
碰到第四个循环:
; p = C;
; for (int i = 0; i <= 4; ++i) {
; if (p->value < p->next->value)
; explode_bomb();
; p = p->next;
; i++;
; }
; 数组 D 组成的数列应该递减
; 所以我们可以将 6 个 node 排序
; 从大到小:node4 node2 node 6 node3 node1 node5
8048e67: 8b 75 cc mov esi,DWORD PTR [ebp-0x34]
8048e6a: 31 ff xor edi,edi
8048e6c: 8d 74 26 00 lea esi,[esi+eiz*1+0x0]
; 循环 5 次
; edx <- p->next
8048e70: 8b 56 08 mov edx,DWORD PTR [esi+0x8]
; eax <- p->value
8048e73: 8b 06 mov eax,DWORD PTR [esi]
; 将 p->value 和 p->next->value 比较
; 即如果小于,则爆炸
8048e75: 3b 02 cmp eax,DWORD PTR [edx]
8048e77: 7d 05 jge 8048e7e <phase_6+0xe6>
8048e79: e8 7e 06 00 00 call 80494fc <explode_bomb>
; p <- p->next
8048e7e: 8b 76 08 mov esi,DWORD PTR [esi+0x8]
; i++
8048e81: 47 inc edi
; while (i <= 4)
8048e82: 83 ff 04 cmp edi,0x4
8048e85: 7e e9 jle 8048e70 <phase_6+0xd8>
; 结束循环
由于从大到小是node4
, node2
, node6
, node3
, node1
, node5
。
输入字符串4 2 6 3 1 5
便能通过这关。
secret_phase
其实在 6 关之后还有一个隐藏关卡,需要一定的条件触发。
可以直接在bomb.disas
里面查找函数secret_phase
,然后可以发现它在函数phase_defused
中被调用。
首先在phase_6
打上断点,发现在phase_defused
中发现有怪异之处:
8049543: 50 push eax
; 格式化字符串
; (gdb) x/s 0x8049d03
; 0x8049d03: "%d %s"
8049544: 68 03 9d 04 08 push 0x8049d03
; 没有解锁隐藏关卡时,输入字符串只有一个 9 ,这与两个占位符的不相符
; 所以我们应该在 9 后面补上一个字符串
; 在 phase_6 打上断点
; (gdb) break phase_6
; (gdb) run
; (gdb) x/s 0x804b770
; 0x804b770 <input_strings+240>: "9"
8049549: 68 70 b7 04 08 push 0x804b770
804954e: e8 0d f3 ff ff call 8048860 <sscanf@plt>
8049553: 83 c4 10 add esp,0x10
发现这里有个用于调试的标签input_strings
,我们可以看看input_strings
+0 +80 +160 都是什么:
(gdb) x/s 0x804b770-240
0x804b680 <input_strings>: "Public speaking is very easy."
(gdb) x/s 0x804b770-240+80
0x804b6d0 <input_strings+80>: "1 2 6 24 120 720"
(gdb) x/s 0x804b770-240+160
0x804b720 <input_strings+160>: "0 q 777"
发现是前三关输入的字符,所以"9"对应的是我们第四关的输入
; 如果成功给两个参数赋值
8049556: 83 f8 02 cmp eax,0x2
8049559: 75 37 jne 8049592 <phase_defused+0x66>
804955b: 83 c4 f8 add esp,0xfffffff8
; 字符串比对
; (gdb) x/s 0x8049d09
; 0x8049d09: "austinpowers"
; 可以得知第四关时后面应该添加一个"austinpowers"
; 由此可以进入隐藏关卡
......
804958d: e8 56 f9 ff ff call 8048ee8 <secret_phase>
现在我们开始看函数secret_phase
:
08048ee8 <secret_phase>:
8048ee8: 55 push ebp
8048ee9: 89 e5 mov ebp,esp
8048eeb: 83 ec 14 sub esp,0x14
8048eee: 53 push ebx
; 将输入的字符串转换为数字
; 返回值在 eax
8048eef: e8 08 03 00 00 call 80491fc <read_line>
8048ef4: 6a 00 push 0x0
8048ef6: 6a 0a push 0xa
8048ef8: 6a 00 push 0x0
8048efa: 50 push eax
8048efb: e8 f0 f8 ff ff call 80487f0 <__strtol_internal@plt>
8048f00: 83 c4 10 add esp,0x10
8048f03: 89 c3 mov ebx,eax
8048f05: 8d 43 ff lea eax,[ebx-0x1]
; 如果 input-1 大于 0x3e8 则爆炸
8048f08: 3d e8 03 00 00 cmp eax,0x3e8
8048f0d: 76 05 jbe 8048f14 <secret_phase+0x2c>
8048f0f: e8 e8 05 00 00 call 80494fc <explode_bomb>
8048f14: 83 c4 f8 add esp,0xfffffff8
8048f17: 53 push ebx
; 该地址可能指向一个数组或者结构
8048f18: 68 20 b3 04 08 push 0x804b320
8048f1d: e8 72 ff ff ff call 8048e94 <fun7>
8048f22: 83 c4 10 add esp,0x10
; 如果返回值为 7 则成功
8048f25: 83 f8 07 cmp eax,0x7
8048f28: 74 05 je 8048f2f <secret_phase+0x47>
......
由于调用了函数fun7
,我们直接来看它。func7
是一个比较简单的递归函数,主要是结构体较为复杂:
08048e94 <fun7>:
8048e94: 55 push ebp
8048e95: 89 e5 mov ebp,esp
8048e97: 83 ec 08 sub esp,0x8
8048e9a: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
8048e9d: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
8048ea0: 85 d2 test edx,edx
8048ea2: 75 0c jne 8048eb0 <fun7+0x1c>
8048ea4: b8 ff ff ff ff mov eax,0xffffffff
8048ea9: eb 37 jmp 8048ee2 <fun7+0x4e>
8048eab: 90 nop
8048eac: 8d 74 26 00 lea esi,[esi+eiz*1+0x0]
8048eb0: 3b 02 cmp eax,DWORD PTR [edx]
8048eb2: 7d 11 jge 8048ec5 <fun7+0x31>
8048eb4: 83 c4 f8 add esp,0xfffffff8
8048eb7: 50 push eax
; 类似链表的结构
8048eb8: 8b 42 04 mov eax,DWORD PTR [edx+0x4]
8048ebb: 50 push eax
8048ebc: e8 d3 ff ff ff call 8048e94 <fun7>
8048ec1: 01 c0 add eax,eax
8048ec3: eb 1d jmp 8048ee2 <fun7+0x4e>
8048ec5: 3b 02 cmp eax,DWORD PTR [edx]
8048ec7: 74 17 je 8048ee0 <fun7+0x4c>
8048ec9: 83 c4 f8 add esp,0xfffffff8
8048ecc: 50 push eax
; 类似链表的结构
8048ecd: 8b 42 08 mov eax,DWORD PTR [edx+0x8]
8048ed0: 50 push eax
8048ed1: e8 be ff ff ff call 8048e94 <fun7>
8048ed6: 01 c0 add eax,eax
8048ed8: 40 inc eax
8048ed9: eb 07 jmp 8048ee2 <fun7+0x4e>
8048edb: 90 nop
8048edc: 8d 74 26 00 lea esi,[esi+eiz*1+0x0]
8048ee0: 31 c0 xor eax,eax
8048ee2: 89 ec mov esp,ebp
8048ee4: 5d pop ebp
8048ee5: c3 ret
8048ee6: 89 f6 mov esi,esi
可以看到有类似链表的结构,但是它拥有两个指针域,我们可以把它想象为一棵树:
// fun7 输入参数:指针 p 和 整型 n
// p 可以理解为二叉树的节点
typedef struct TreeNode {
int value;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
这样一来fun7
马上可以写成 C 语言:
int fun7(TreeNode *p, int n) {
if (p == 0) {
return -1;
} else {
if (n->value < p[0]) {
return 2 * fun7(p->left, n);
} else if (n == p[0]) {
return 0;
} else {
return 2 * fun7(p->right, n) + 1;
}
}
}
由于需要递归函数返回 7 (参数(p, n)
),所以内一层只能返回 3 (参数(p->right, n)
), 再内一层只能返回 1 (p->right->right, n)
,最里面一层返回 0 (p->right->right->right, n)
。 所以第一次调用时p->righ->right->right->value
应该与n
相等
可以通过 GDB 很快找到:
# value left right
(gdb) x/3x 0x804b320
0x804b320 <n1>: 0x00000024 0x0804b314 0x0804b308
(gdb) x/3x 0x0804b308
0x804b308 <n22>: 0x00000032 0x0804b2f0 0x0804b2d8
(gdb) x/3x 0x0804b2d8
0x804b2d8 <n34>: 0x0000006b 0x0804b2b4 0x0804b278
(gdb) x/3x 0x0804b278
0x804b278 <n48>: 0x000003e9 0x00000000 0x00000000
所以应该输入3e9
也就是十进制1001
。
输入文件
如下是最后通过测试的输入文件:
$ cat input
Public speaking is very easy.
1 2 6 24 120 720
0 q 777
9 austinpowers
o`ekma
4 2 6 3 1 5
1001
实验感受
这六关分别涉及了字符串常量、循环、switch-case(跳转表)、递归、数组、链表等知识,难度没有想象中那么大,耐心看汇编代码其实是很容易看懂的。而隐藏关卡需要细心或者运气发现,但是解决它并不是问题。
做 Lab 时没有动态调试这个程序,感觉也没有动态调试的必要,只要看到了“魔法”地址就可以直接 GDB 查看它。