之前的 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 查看它。