⚠️ 注意,本文只提供个人的解法与思路,不能代替自己亲手用调试器做实验。如果你希望更好地锻炼自己的能力,仅仅在你尝试过,并且被卡住时候,观看对应章节的内容。或者,在你独立做完实验,可以与本指南中的内容核对。也欢迎各位对本文中的拼写错误、细节遗漏等方面进行补充!
本文受到了 更适合北大宝宝体质的 Bomb Lab 踩坑记 很大的启发,因此你可能发现一些雷同的内容,这都只是我的拙劣模仿。
实验介绍 本实验是 CSAPP 第三章的实验内容,设计了汇编的许多方面,包括整数运算、字符串比较、循环条件分支、递归调用、栈、指针、链表、结构等等。本文读者应当学习过以上内容,进而更好地理解该实验。
Bomblab 的实验背景相当经典,可以在 bomb.c 中看到。简单来说,你需要根据二进制程序来拆弹💣。
实验准备 实验暴露出来的源码 bomb.c 只涵盖了一小部分,没有涉及每个函数具体是如何实现的。不过,我们可以从中看出大概流程——获取输入,然后执行下面的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
initialize_bomb ();
printf ( "Welcome to my fiendish little bomb. You have 6 phases with \n " );
printf ( "which to blow yourself up. Have a nice day! \n " );
/* Hmm... Six phases must be more secure than one phase! */
input = read_line (); /* Get input */
phase_1 ( input ); /* Run the phase */
phase_defused (); /* Drat! They figured it out!
* Let me know how they did it. */
printf ( "Phase 1 defused. How about the next one? \n " );
// 更多内容
每次都读取一行作为输入,给 phase_n ,然后运行 phase_defused 函数。
为了调试二进制程序,需要使用 gdb。我已经在之前的文章中,介绍了 GDB Dashboard 以及 pwndbg 。为了更好的体验,建议先配置 pwndbg ,但这不是必需的。本文前四个阶段使用 GDB Dashboard,后两个阶段使用 pwndbg。
参考北大Bomblab踩坑记 ,该实验用到的常用调试指令如下:
指令 全称 描述 r run 开始执行程序,直到下一个断点或程序结束 q quit 退出 GDB 调试器 ni nexti 执行下一条汇编指令,但不进入函数内部 si stepi 执行当前汇编指令,如果是函数调用则进入函数 b break 在指定位置设置断点 c cont 从当前位置继续执行程序,直到下一个断点或程序结束 p print 打印变量的值 x 打印内存中的值
注意 n 或者 q 指令是直接进行一步源代码指令,因此该实验中不会用到。p 是打印变量的值,通常是寄存器,不过在 pwndbg 的存在下没什么必要。x 是打印某个地址对应的值,也可以看做进行一次解引用操作,相当常用。
一些常用指令:
1
2
3
4
5
6
7
8
9
x/2x $rsp # 以十六进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。
x/2d $rsp # 以十进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。
x/2c $rsp # 以字符格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。
x/s $rsp # 把栈指针指向的内存位置 M[%rsp] 当作 C 风格字符串来查看。
x/b $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 1 字节。
x/h $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 2 字节(半字)。
x/w $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 4 字节(字)。
x/g $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 8 字节(双字)。
注意 / 后面的后缀(如 2x、2d、s、g、20c)指定了查看内存的方式和数量。具体来说:
第一个数字(如 2、20)指定要查看的单位数量。 第二个字母(如 x、d、s、c)指定单位类型和显示格式,其中:c / d / x 分别代表以字符 / 十进制 / 十六进制格式显示内存内容。 s 代表以字符串格式显示内存内容。 第三个字母(如 b / h / w / g)分别代表以 1 / 2 / 4 / 8 字节为单位(unit)显示内存内容。 为了方便每次运行 gdb bomb 都能进入我们想要的部分,可以编辑当前文件夹下的 .gdbinit 文件(注意还需要更改系统的~/.config/gdb/gdbinit,参考GDB Dashboard 教程 ):
1
2
3
4
5
6
7
8
9
10
11
12
set args psol.txt
b phase_1
#b phase_2
#b phase_3
#b phase_4
#b phase_5
#b phase_6
#b phase_defused
setupwin /dev/pts/5
这当中 set args 可以指定参数。当前目录下的 psol.txt 文件的每一行,就是对每一个阶段的解答。这样每次完成一个阶段,都可以写入 psol.txt 文件,不用重复输入密码。
断点可以根据当前阶段设置。# 开头的是注释。例如当前完成了前两个阶段,就可以只为第三阶段开启。
最后一行是我为 pwndbg 自定义的函数,为了打印调试信息到指定内容,参考 pwndbg 教程与自定义配置 。
同时,因为 GDB 或者 pwndbg 能看到的反汇编代码的范围有限,为了方便查找,可以一次性生成反汇编代码并存储:
1
objdump -d bomb > bomb.asm
这样就可以在 VSCode 中查找。
如果你发现 CSAPP Labs 只有远程桌面,其实你可以简单地把 gitlab 链接复制,在本地或其他指定的地方克隆下来,运行即可。例如我使用的是一个远程 Ubuntu 电脑,本地使用 VSCode SSH,以及直接利用 Tabby SSH 连接。
本文中不区分 eax 与 rax 等 32 位与 64 位寄存器,认为是等价的。本文中十六进制尽量使用 0x 前缀开头,但可能有遗漏。
phase_1 运行程序 gdb bomb,然后输入 run:
1
2
3
4
(gdb) run
Starting program: /home/ywr/dsc/learn/csapp/bomblab/bomb psol.txt
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
此时可以随意输入一些尝试,例如我输入 123456 并回车,就进入了 phase_1 的断点:
1
2
Breakpoint 1, 0x0000000000400e6d in phase_1 ()
( gdb)
搜索 phase_1,可以看到如下代码:
1
2
3
4
5
6
7
8
9
10
0000000000400 e6d < phase_1 > :
400 e6d: 48 83 ec 08 sub $0x8 , %rsp
400 e71: be d0 23 40 00 mov $0x4023d0 , %esi
400 e76: e8 cf 04 00 00 callq 40134a < strings_not_equal >
400 e7b: 85 c0 test %eax , %eax
400 e7d: 75 05 jne 400e84 < phase_1 + 0x17 >
400 e7f: 48 83 c4 08 add $0x8 , %rsp
400 e83: c3 retq
400 e84 : e8 be 05 00 00 callq 401447 < explode_bomb >
400 e89: eb f4 jmp 400e7f < phase_1 + 0x12 >
可以看到,第三步调用了 strings_not_equal 函数。让我们输入两次 ni,到达 0x400e76 处。
1
2
3
4
( gdb) ni
0x0000000000400e71 in phase_1 ()
( gdb) ni
0x0000000000400e76 in phase_1 ()
汇编中第一个与第二个参数分别是 %rdi 与%rsi,让我们尝试打印:
1
2
3
4
( gdb) p/x $rdi
$1 = 0x603780
( gdb) p/x $rsi
$2 = 0x4023d0
我们将这两个看成字符串指针,尝试打印:
1
2
3
4
( gdb) x/s $rdi
0x603780 <input_strings>: "123456"
( gdb) x/s $rsi
0x4023d0: "Slave, thou hast slain me. Villain, take my purse."
可以看出,其中 123456 就是我们输入的字符,也是 input_strings。剩下的就应该是第一阶段的密码了。
阅读原先汇编的逻辑,也可以知道在 strings_not_equal 调用后,会利用 test %eax,%eax 来判断返回值是否为 0(test 指令会对两个操作数进行按位与运算),之后根据 jne 进行跳转。如果不为 0,则跳转到 0x400e84,然后调用 explode_bomb 函数,引发爆炸;如果为 0,说明字符串匹配,则恢复栈指针位置,然后返回。
所以第一阶段密码为 Slave, thou hast slain me. Villain, take my purse. 。
phase_2 read_six_numbers 分析 调用 read_six_numbers 后爆炸,对该函数打断点。
rcx 写为 0x00007fffffffdb44,rax 写为 0x00007fffffffdb54,推入栈,然后写为 0x00007fffffffdb50,再推入栈。r9 为0x00007fffffffdb4c,r8 为0x00007fffffffdb48 。
调用前,rdi 为 0x00007fffffffd4d0,rsi 为 0x00000000004025c3。
打印 sscanf 输入参数对应位置:
1
2
3
4
>>> x/s $rdi
0x6037d0 <input_strings+80>: "123456"
>>> x/s $rsi
0x4025c3: "%d %d %d %d %d %d"
发现其中第一个参数就是我们输入的字符串,第二个就是指定的格式。参考 sscanf 的使用方法 :
1
int sscanf ( const char * s , const char * format , ...);
函数返回值代表捕捉到的参数个数。第一个参数是输入字符串(如 123456),第二个是指定格式(如 %d %d %d %d %d %d),之后的参数都是指针,指向希望存储数据的位置。
根据定义,第三个参数开始分别是 rdx、rcx、r8、r9,超过六个参数都会压入调用者的栈中。注意,栈顶是从内存大区域往小区域的,并且压入的参数是逆序的,也就是先压入第8个参数,再压入第7个参数。所以地址最大的 %rsi+0x14 是最先压入的,也就是最后的参数。
调用后,返回值 eax 会与 5 比较,如果小于等于 5,则会直接爆炸。这说明我们应该符合格式,输入 6 个整数进行尝试,如 1 2 3 4 5 6。在 0x401486 处停下,打印这些变量,就可以得到此时这些变量。打印的是现在 rsi 指向的地址,也就是0x7fffffffdb40。
1
2
3
>>> x/6dw 0x7fffffffdb40
0x7fffffffdb40: 0 0 4199486 0
0x7fffffffdb50: -9112 32767
目前都为空。经过 sscanf 函数后再次打印:
1
2
3
>>> x/6dw 0x7fffffffdb40
0x7fffffffdb40: 1 2 3 4
0x7fffffffdb50: 5 6
我们发现这就是 sscanf 获得的输入,将我们的输入存到了 6 个 int 类型中,放在栈上,是连续的六个变量。
参数 值/指向的地址 指向的值 rdx %rsi 1 rcx %rsi+0x4 2 r8 %rsi+0x8 3 r9 %rsi+0xc 4 第七个参数 %rsi+0x10 5 第八个参数 %rsi+0x14 6
phase_2 本身分析 之后来分析 phase_2 本身。该部分有详细的汇编注释,但之后的汇编中我会减少相关注释。注意下面的 emoji 代表了跳转的位置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0000000000400 e8b < phase_2 > :
# ...省略...
400 ea3: e8 c1 05 00 00 callq 401469 < read_six_numbers > # 获取六个数字
400 ea8: 83 3 c 24 00 cmpl $0x0 ,( %rsp ) # rsp 第一个数字 与 0 比较
400 eac: 78 07 js 400eb5 < phase_2 + 0x2a > # 有符号比较,当第一个数字为负数的时候跳转到爆炸
400 eae: bb 01 00 00 00 mov $0x1 , %ebx # 将 ebx 变为1
400 eb3: eb 11 jmp 400ec6 < phase_2 + 0x3b > # 无条件跳转到 ec6 一行😭
400 eb5: e8 8 d 05 00 00 callq 401447 < explode_bomb > # 爆炸
400 eba: eb f2 jmp 400eae < phase_2 + 0x23 >
400 ebc: 48 83 c3 01 add $0x1 , %rbx # 😁:rbx += 1
400 ec0: 48 83 fb 06 cmp $0x6 , %rbx # 将 rbx 与 6 比较
400 ec4: 74 12 je 400ed8 < phase_2 + 0x4d > # 如果相等,说明循环结束,跳转到🚩
400 ec6: 89 d8 mov %ebx , %eax # 😭:eax 变成 ebx
400 ec8: 03 44 9 c fc add - 0x4 ( %rsp , %rbx , 4 ), %eax # eax += rsp对应数组中rbx偏移量后减去4,也就是rbx为1 2 3分别对应数组索引 0 1 2,也即 rsp[rbx-1]
400 ecc: 39 04 9 c cmp %eax ,( %rsp , %rbx , 4 ) # 将 rsp[rbx] 与 eax 比较
400 ecf: 74 eb je 400ebc < phase_2 + 0x31 > # 如果相等,跳转到😁
400 ed1: e8 71 05 00 00 callq 401447 < explode_bomb >
400 ed6: eb e4 jmp 400ebc < phase_2 + 0x31 >
400 ed8: 48 8 b 44 24 18 mov 0x18 ( %rsp ), %rax # 🚩:此时循环结束,将 rax 变成 rsp 地址加上 18
400 edd: 64 48 33 04 25 28 00 xor %fs : 0x28 , %rax # 异或比较
400 ee4: 00 00
400 ee6: 75 06 jne 400eee < phase_2 + 0x63 > # 如果不相等,跳转到最后一行
400 ee8: 48 83 c4 20 add $0x20 , %rsp # rsp 增加 20
400 eec: 5 b pop %rbx # 推出元素
400 eed: c3 retq @ 返回
400 eee: e8 0 d fc ff ff callq 400b00 < __stack_chk_fail@plt > # 报错
可以跟着编译器,看出其跳转的逻辑,写成 C 的话类似这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int [] rsp ;
int eax ;
if ( rsp [ 0 ] < 0 ) {
explode ();
}
int ebx = 1 ;
do {
eax = ebx ;
eax += rsp [ ebx - 1 ];
if ( ! ( eax = rsp [ ebx ])) {
explode ();
}
ebx ++ ;
} while ( ebx != 6 )
所以,第一个循环 eax 为 1,并且 eax 加上数组第 0 个元素,判断是否等于数组第 1 个元素;之后继续循环。
可以有多种解,就是一个二阶等差数列(二次函数),相邻两个元素的差是 1 2 3 4 5。同时保证第一个数不小于 0 即可:
1
2
3
4
0 1 3 6 10 15
1 2 4 7 11 16
2 3 5 8 12 17
...
phase_3 在 sscanf 调用前,esi 便是第二个输入参数,代表了匹配的格式,打印得出:
1
2
>>> x / s $ rsi
0x4025cf : "%d %d"
这说明第三阶段希望匹配两个整数。前面将函数第三个参数 rdx 设置为 rsp 地址,第四个参数 rcx 设置为 0x4(%rsp) ,说明 rsp 指向了我们输入的两个数。
1
2
3
4
5
6
7
8
9
10
0000000000400 ef3 < phase_3 > :
400 f0f: be cf 25 40 00 mov $0x4025cf , %esi
400 f14: e8 87 fc ff ff callq 400ba0 < __isoc99_sscanf@plt >
400 f19: 83 f8 01 cmp $0x1 , %eax # 比较 sscanf 返回值与 1
400 f1c: 7 e 10 jle 400f2e < phase_3 + 0x3b > # 如果小于等于1,则爆炸
400 f1e: 83 3 c 24 07 cmpl $0x7 ,( %rsp ) # 比较 rsp 指向的值与 7
400 f22: 77 42 ja 400f66 < phase_3 + 0x73 > # 如果大于7,爆炸
400 f24: 8 b 04 24 mov ( %rsp ), %eax # 否则将 rsp 指向的值放入 eax
400 f27: ff 24 c5 40 24 40 00 jmpq * 0x402440 (, %rax , 8 ) # 将 rax 作为索引,跳转到指定位置
...
尝试输入 1 2。调试发现 0x400f1e 步 rsp 指向的值为 1,也就是我们输入的第一个数。接下来与 7 进行比较,然后有一大堆跳转。不难想到,这是 switch 的跳转表。可知表的位置在 0x402440 ,尝试打印(打印 8 个单位,以十六进制,每个单位有 8 个字节)。
1
2
3
4
5
>>> x/8xg 0x402440
0x402440: 0x0000000000400f72 0x0000000000400f35
0x402450: 0x0000000000400f3c 0x0000000000400f43
0x402460: 0x0000000000400f4a 0x0000000000400f51
0x402470: 0x0000000000400f58 0x0000000000400f5f
这就是输入数从 0 到 7 的时候,对应的跳转位置。让我们根据下面的代码进行分析,加上注释,将第一个输入称为 x:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
400 f1e: 83 3 c 24 07 cmpl $0x7 ,( %rsp )
400 f22: 77 42 ja 400f66 < phase_3 + 0x73 >
400 f24: 8 b 04 24 mov ( %rsp ), %eax
400 f27: ff 24 c5 40 24 40 00 jmpq * 0x402440 (, %rax , 8 )
400 f2e: e8 14 05 00 00 callq 401447 < explode_bomb >
400 f33: eb e9 jmp 400f1e < phase_3 + 0x2b > # 调到rsp的值与7的比较
400 f35: b8 35 02 00 00 mov $0x235 , %eax # x = 1
400 f3a: eb 3 b jmp 400f77 < phase_3 + 0x84 >
400 f3c: b8 a7 01 00 00 mov $0x1a7 , %eax # x = 2
400 f41: eb 34 jmp 400f77 < phase_3 + 0x84 >
400 f43: b8 2 b 02 00 00 mov $0x22b , %eax # x = 3
400 f48: eb 2 d jmp 400f77 < phase_3 + 0x84 >
400 f4a: b8 6 c 00 00 00 mov $0x6c , %eax # x = 4
400 f4f: eb 26 jmp 400f77 < phase_3 + 0x84 >
400 f51: b8 f1 02 00 00 mov $0x2f1 , %eax # x = 5
400 f56: eb 1 f jmp 400f77 < phase_3 + 0x84 >
400 f58: b8 3 e 00 00 00 mov $0x3e , %eax # x = 6
400 f5d: eb 18 jmp 400f77 < phase_3 + 0x84 >
400 f5f: b8 48 02 00 00 mov $0x248 , %eax # x = 7
400 f64: eb 11 jmp 400f77 < phase_3 + 0x84 >
400 f66 : e8 dc 04 00 00 callq 401447 < explode_bomb >
400 f6b: b8 00 00 00 00 mov $0x0 , %eax
400 f70: eb 05 jmp 400f77 < phase_3 + 0x84 >
400 f72: b8 21 01 00 00 mov $0x121 , %eax # x = 0
400 f77: 39 44 24 04 cmp %eax , 0x4 ( %rsp ) # 继续运行,比较eax 与第二个输入数字的值
400 f7b: 74 05 je 400f82 < phase_3 + 0x8f > # 如果相等,跳转到下方
400 f7d: e8 c5 04 00 00 callq 401447 < explode_bomb > # 不然爆炸
400 f82: 48 8 b 44 24 08 mov 0x8 ( %rsp ), %rax # 开始各种栈相关的操作
400 f87: 64 48 33 04 25 28 00 xor %fs : 0x28 , %rax
400 f8e: 00 00
400 f90: 75 05 jne 400f97 < phase_3 + 0xa4 >
400 f92: 48 83 c4 18 add $0x18 , %rsp
400 f96: c3 retq
400 f97 : e8 64 fb ff ff callq 400b00 < __stack_chk_fail@plt >
通过注释,我们不难发现,当 x 从 0 遍历到 7 的时候,都会通过一行 mov 命令设置 eax 的值,然后跳转到 0x400f77,将 eax 与第二个输入进行比较。如果不相等,就爆炸。
因此可以得出多种解法,只要让第二个参数,与对应的 mov 中的硬编码的值一样即可。注意之前 sscanf 的 %d 只能匹配十进制输入,所以需要将十六进制进行转换。
原先十六进制:
1
2
3
4
5
6
7
8
0 0x121
1 0x235
2 0x1a7
3 0x22b
4 0x6c
5 0x2f1
6 0x3e
7 0x248
转换为十进制:
1
2
3
4
5
6
7
8
0 289
1 565
2 423
3 555
4 108
5 753
6 62
7 584
任意一行都是有效的解。
phase_4 phase_4 本身分析 照样有 sscanf 函数,打印出 rsi,发现还是两个整数:
1
2
>>> x/s $rsi
0x4025cf: "%d %d"
第三个参数 rdx 设置为 rsp 地址,第四个参数 rcx 设置为 0x4 (%rsp) ,与第三阶段一样,还是将两个输入存放在 rsp 指向的位置。
调用 sscanf 后先检验返回值是否为 2,如果不是,直接爆炸。接着比较 rsp 指向的值,也就是第一个输入与 0xe(14) 的关系,如果小于等于 14,正常跳转;否则爆炸。
1
2
3
4
5
6
7
400 ff7: be cf 25 40 00 mov $0x4025cf , %esi
400 ffc: e8 9 f fb ff ff callq 400ba0 < __isoc99_sscanf@plt >
401001: 83 f8 02 cmp $0x2 , %eax # 检验返回值是否为2
401004: 75 06 jne 40100c < phase_4 + 0x31 >
401006: 83 3 c 24 0 e cmpl $0xe ,( %rsp )
40100 a: 76 05 jbe 401011 < phase_4 + 0x36 > # 如果rsp指向的值小于等于14,跳转
40100 c: e8 36 04 00 00 callq 401447 < explode_bomb > # 爆炸
分析接下来的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
401011: ba 0 e 00 00 00 mov $0xe , %edx
401016: be 00 00 00 00 mov $0x0 , %esi
40101 b: 8 b 3 c 24 mov ( %rsp ), %edi
40101 e: e8 79 ff ff ff callq 400f9c < func4 >
401023: 83 f8 03 cmp $0x3 , %eax
401026: 75 07 jne 40102f < phase_4 + 0x54 >
401028: 83 7 c 24 04 03 cmpl $0x3 , 0x4 ( %rsp ) # 将第二个数字与 3 比较
40102 d: 74 05 je 401034 < phase_4 + 0x59 >
40102 f: e8 13 04 00 00 callq 401447 < explode_bomb >
401034: 48 8 b 44 24 08 mov 0x8 ( %rsp ), %rax # 正常操作
401039: 64 48 33 04 25 28 00 xor %fs : 0x28 , %rax
401040: 00 00
401042: 75 05 jne 401049 < phase_4 + 0x6e >
401044: 48 83 c4 18 add $0x18 , %rsp
401048: c3 retq
401049 : e8 b2 fa ff ff callq 400b00 < __stack_chk_fail@plt >
设置 edx 为 14,esi 为 0,edi 为第一个数字的值,然后调用 func4 函数。
根据输入参数顺序,可以 func4 第一个参数 rdi 为第一个数字,第二个参数为 rsi 为 0,第三个参数 rdx 为 14 。
调用后将返回值与 3 比较,如果不一样,直接爆炸;否则将第二个输入与 3 比较,如果不相等,直接爆炸。所以这里可以确定第二个输入为 3 。
所以,剩下来就看 func4 是如何处理 eax 的,让 fun4 返回值为 3。
func4 分析 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0000000000400 f9c < func4 > :
400 f9c: 48 83 ec 08 sub $0x8 , %rsp
400 fa0: 89 d0 mov %edx , %eax
400 fa2: 29 f0 sub %esi , %eax
400 fa4: 89 c1 mov %eax , %ecx
400 fa6: c1 e9 1 f shr $0x1f , %ecx
400 fa9: 01 c1 add %eax , %ecx
400 fab: d1 f9 sar %ecx
400 fad: 01 f1 add %esi , %ecx
400 faf: 39 f9 cmp %edi , %ecx
400 fb1: 7 f 0 e jg 400fc1 < func4 + 0x25 >
400 fb3: b8 00 00 00 00 mov $0x0 , %eax
400 fb8: 39 f9 cmp %edi , %ecx
400 fba: 7 c 11 jl 400fcd < func4 + 0x31 >
400 fbc: 48 83 c4 08 add $0x8 , %rsp
400 fc0: c3 retq
400 fc1 : 8 d 51 ff lea - 0x1 ( %rcx ), %edx
400 fc4: e8 d3 ff ff ff callq 400f9c < func4 >
400 fc9: 01 c0 add %eax , %eax
400 fcb: eb ef jmp 400fbc < func4 + 0x20 >
400 fcd: 8 d 71 01 lea 0x1 ( %rcx ), %esi
400 fd0: e8 c7 ff ff ff callq 400f9c < func4 >
400 fd5: 8 d 44 00 01 lea 0x1 ( %rax , %rax , 1 ), %eax
400 fd9: eb e1 jmp 400fbc < func4 + 0x20 >
首先将栈指针 rsp 减小,edx(14)放入 eax,减去 esi(0),再放到 ecx,向右移位 0x1f,也就是 31 位,ecx 变成 0 。ecx 加上 eax 变成 14。单独的 sar 就是向右算数移位 1 位,ecx 变成 7。加上 esi 不变。将 ecx(7)与 edi(第一个输入)比较,如果大于,跳转。
第一个输入小于 7,跳转到400fc1,将 edx 设置为 rcx 减去 1(6),递归调用自身 第一个输入大于等于 7,没有跳转,eax 变成 0,比较 ecx(7)与 edi,此时一定小于等于小于时跳转400fcd,将 esi 设置为 rcx+1,即 8,递归调用 输入等于 7,不跳转,将栈指针增大,返回 为了方便看,将所有变量前面的 e 或者 r 隐藏:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func4 ( int dx = 14 , int si = 0 , int di = 第一个输入 ) {
int ax = dx - si ;
int cx = ax >> 31 + ax ;
cx = cx >> 1 ;
cx += si ; // cx = (dx-si的符号位 + dx-si) / 2 + si
if ( cx > di ) {
dx = cx - 1 ;
func4 ( dx , si , di ); // 这里的 esi 与 edi 与输入的值一样,edx不同
ax *= 2 ;
return ;
} else {
ax = 0 ;
if ( cx < di ) {
si = cx + 1 ;
func4 ( dx , si , di );
ax = 2 * ax + 1 ;
return ;
} else {
return ;
}
}
}
我们希望运行这个函数后,ax 的值为 3 。
直接分析很难,尝试倒推。ax 有几个地方会被修改:
进入函数,ax 设置为 dx - si 当修改后的 cx <= di 时,ax 设置为 0 之后如果 cx < di,调用函数后,ax 设置为 2 * ax + 1 也可以发现,函数中 di 仅仅会在判断的时候用到,永远不会被修改。并且判断分为三种可能,也就是 cx 与 di 的大小关系,其中 cx 为 (dx-si -(dx-si) 的符号位 ) / 2 + si,看起来是取 dx 与 si 的平均值。
例如,一开始输入 dx 为 14,si 为 0,则 cx 为 7。ax 在函数开始初始化为两端之差。
若均值大于 di,则 dx 变成 cx-1,也就是大端变成均值-1;然后递归调用,其中会改变 ax,最后返回 ax * 2 若均值小于 di,则 si 变成 cx+1,也就是小端变成均值+1;然后递归调用,其中会改变 ax,最后返回 ax * 2 + 1 如果均值等于 di,则直接返回,ax 为 0 这种形式很像二分法。我们希望最后的 ax 返回值为 3,是奇数,其倒推的过程应该是 0 -> (0 * 2 + 1 = 1) -> (1 * 2 + 1 = 3) 因此均值应该小于 di,也就是 di 大于 7 。
调用函数过程中,ax 应该为 1 。在这个函数中,1 为奇数,因此其均值也应该小于 di,第二层递归。在第二层递归中,应该均值等于 di,直接返回 ax 为 0 。这样就可以构造出结果。
[0, 14] 的均值为 7,di 大于 7 。[8, 14] 的均值为 11,di 应该大于 11。[12, 14] 的均值为 13。所以应该为 13。
总之,第四阶段输入为:
phase_5 开始时的 sscanf 前面还是 0x4025cf,调试发现还是将输入的两个数字,存到 rsp 所指的栈上,若输入个数小于 2 则爆炸。
1
2
3
4
5
6
7
8
9
10
11
12
13
40106 a: be cf 25 40 00 mov $0x4025cf , %esi
40106 f: e8 2 c fb ff ff callq 400ba0 < __isoc99_sscanf@plt >
401074 : 83 f8 01 cmp $0x1 , %eax
401077: 7 e 57 jle 4010d0 < phase_5 + 0x82 > #输入个数小于2,爆炸
401079: 8 b 04 24 mov ( %rsp ), %eax # eax变成第一个输入
40107 c: 83 e0 0 f and $0xf , %eax
40107 f: 89 04 24 mov %eax ,( %rsp )
401082: 83 f8 0 f cmp $0xf , %eax
401085: 74 2 f je 4010b6 < phase_5 + 0x68 > #爆炸
401087: b9 00 00 00 00 mov $0x0 , %ecx
40108 c: ba 00 00 00 00 mov $0x0 , %edx
401091: 83 c2 01 add $0x1 , %edx
401094: 48 98 cltq
接着 eax 变成第一个输入,与 0xf 按位与运算,写入 rsp 第一个输入。如果 eax 等于 0xf,爆炸。然后 ecx 为 0,edx 为 1,并将 eax 拓展为 64 位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
401091: 83 c2 01 add $0x1 , %edx # 😎
401094: 48 98 cltq
401096 : 8 b 04 85 80 24 40 00 mov 0x402480 (, %rax , 4 ), %eax
40109 d: 01 c1 add %eax , %ecx
40109 f: 83 f8 0 f cmp $0xf , %eax
4010 a2: 75 ed jne 401091 < phase_5 + 0x43 > # 跳转到 😎,循环计数
4010 a4: c7 04 24 0 f 00 00 00 movl $0xf ,( %rsp )
4010 ab: 83 fa 03 cmp $0x3 , %edx
4010 ae: 75 06 jne 4010b6 < phase_5 + 0x68 >
4010 b0: 39 4 c 24 04 cmp %ecx , 0x4 ( %rsp )
4010 b4: 74 05 je 4010bb < phase_5 + 0x6d >
4010 b6: e8 8 c 03 00 00 callq 401447 < explode_bomb >
4010 bb: 48 8 b 44 24 08 mov 0x8 ( %rsp ), %rax
4010 c0: 64 48 33 04 25 28 00 xor %fs : 0x28 , %rax
4010 c7: 00 00
4010 c9: 75 0 c jne 4010d7 < phase_5 + 0x89 >
4010 cb: 48 83 c4 18 add $0x18 , %rsp
4010 cf: c3 retq
4010 d0 : e8 72 03 00 00 callq 401447 < explode_bomb >
4010 d5: eb a2 jmp 401079 < phase_5 + 0x2b >
4010 d7: e8 24 fa ff ff callq 400b00 < __stack_chk_fail@plt >
接下来会取 0x402480 开始的第 rax 的元素,尝试打印:
1
2
3
4
5
6
pwndbg > x / 20 dw 0x402480
0x402480 < array .3415 >: 10 2 14 7
0x402490 < array .3415 + 16 >: 8 12 15 11
0x4024a0 < array .3415 + 32 >: 0 4 1 13
0x4024b0 < array .3415 + 48 >: 3 9 6 5
0x4024c0 : 2032168787 1948284271 1802398056 1970239776
发现这是一个大小为 16 的数组,这也说明了之前为什么要限制 eax 为 0 到 15 之间的数:
1
2
3
4
10 2 14 7
8 12 15 11
0 4 1 13
3 9 6 5
其索引与数组元素的对应关系如下:
取其中第 rax 的元素放入 eax,将 ecx 加上 eax。判断 eax 与 0xf(15) 的关系,如果不相等,跳转回 0x401091。期间对 edx 不断加 1,ecx 不断加上 eax。
画出其跳转关系:
0 -> 10 -> 1 -> 2 -> 14 -> 6 -> 15 -> 5 -> 12 -> 3 -> 7 -> 11 -> 13 -> 9 -> 4 -> 8 -> 0,然后循环。可以知道当变成 15 的时候终止,所以可以改变终点:
5 -> 12 -> 3 -> 7 -> 11 -> 13 -> 9 -> 4 -> 8 -> 0 -> 10 -> 1 -> 2 -> 14 -> 6 -> 15
循环结束后,将 15 写入 rsp 指向的元素,也就是第一个输入改为 15 。然后将 edx 与 3 进行比较,如果不为 3,跳转并爆炸。
这说明循环需要进行 3 次。例如,我的尝试中输入为 13 14,在 0x4010a4 打上断点,发现此时 edx 为 0xa(10),刚好是上面的链条中 13 到最后 15 的距离。
因此,第一个输入应该是 2,或者 2 + 16n,其中 n 为自然数。
之后,将 ecx 与第二个输入比较,如果不相等,爆炸。已知 ecx 是累计的数字的和,可知其为 14 + 6 + 15 = 35。
总之,输入可以是如下的数:
1
2
3
4
2 35
18 35
34 35
...
phase_6 首先将多个 caller-saved 的寄存器推入栈:
1
2
3
4
5
6
0000000004010 dc < phase_6 > :
4010 dc: 41 56 push %r14
4010 de: 41 55 push %r13
4010 e0: 41 54 push %r12
4010 e2: 55 push %rbp
4010 e3: 53 push %rbx
接着是金丝雀数的一些操作(参考 ),此处略。
然后将 eax 清零,rsp 放入第二个参数 rsi,调用 read_six_numbers。
1
2
3
4
5
6
7
4010 f6: 31 c0 xor %eax , %eax
4010 f8: 48 89 e6 mov %rsp , %rsi
4010 fb: e8 69 03 00 00 callq 401469 < read_six_numbers >
401100: 49 89 e4 mov %rsp , %r12
401103: 49 89 e5 mov %rsp , %r13
401106: 41 be 00 00 00 00 mov $0x0 , %r14d
40110 c: eb 25 jmp 401133 < phase_6 + 0x57 >
回顾之前的 read_six_numbers 函数,可知第一个参数 dsi 是我们的输入字符串的地址,例如此处我的输入为 2 3 4 5 6 7,第二个参数 rsi 是当前 rsp 的地址。
调用 read_six_numbers 后,打印当前 rsp 对应的量:
1
2
3
pwndbg> x/6dw $rsp
0x7fffffffd9f0: 2 3 4 5
0x7fffffffda00: 6 7
可知我们输入的六个数都存在了栈上。然后函数更改一些寄存器,跳到 0x401133。
1
2
3
4
5
6
7
8
9
10
401133: 4 c 89 ed mov %r13 , %rbp
401136: 41 8 b 45 00 mov 0x0 ( %r13 ), %eax
40113 a: 83 e8 01 sub $0x1 , %eax
40113 d: 83 f8 05 cmp $0x5 , %eax
401140: 77 cc ja 40110e < phase_6 + 0x32 > # 爆炸
401142: 41 83 c6 01 add $0x1 , %r14d
401146: 41 83 fe 06 cmp $0x6 , %r14d
40114 a: 74 05 je 401151 < phase_6 + 0x75 >
40114 c: 44 89 f3 mov %r14d , %ebx
40114 f: eb cc jmp 40111d < phase_6 + 0x41 >
将第一个输入放入 eax,与 5 比较,如果大于,爆炸。r14 之前初始化为 0 。现在加上 1,与 6 比较,如果相等,跳到 0x401151 结束循环;否则将 r14 移到 ebx 中,跳到 40111d。
1
2
3
4
5
6
7
8
401115: 83 c3 01 add $0x1 , %ebx
401118: 83 fb 05 cmp $0x5 , %ebx
40111 b: 7 f 12 jg 40112f < phase_6 + 0x53 >
40111 d: 48 63 c3 movslq %ebx , %rax # 刚刚跳转的位置
401120: 8 b 04 84 mov ( %rsp , %rax , 4 ), %eax
401123: 39 45 00 cmp %eax , 0x0 ( %rbp )
401126: 75 ed jne 401115 < phase_6 + 0x39 >
401128: e8 1 a 03 00 00 callq 401447 < explode_bomb >
将 ebx 移入 rax,rsp 中第 rax 个元素放入 eax。第一次来到这里时 ebx 为 1,所以这就是取第二个输入,例如我的是 3。目前 rbp 指向元素就是第一个元素,所以比较第二个元素与第一个是否相等。如果相等,就直接爆炸。否则跳转到 401115,增加 ebx,与 5 比较,如果大于,跳转到 40112f,这会给 rsp 指向的地址加 4,也就是指向下一个元素。
利用调试器,多看几次流程。会发现其本质就是检查输入的 6 个数中第 i 个元素与第 j 个元素是否相等,如果相等就会触发爆炸。然后每次循环后增加 i 索引。同时,也会讲当前第 i 个元素减 1 后与 5 比较,如果大于,也会爆炸。说明当前元素应该小于等于 6 。
因此,我们尝试输入 1 2 3 4 5 6,为 0x401151 打断点,跳转。
1
2
3
4
5
6
7
8
9
10
401151: 49 8 d 4 c 24 18 lea 0x18 ( %r12 ), %rcx
401156: ba 07 00 00 00 mov $0x7 , %edx
40115 b: 89 d0 mov %edx , %eax
40115 d: 41 2 b 04 24 sub ( %r12 ), %eax
401161: 41 89 04 24 mov %eax ,( %r12 )
401165: 49 83 c4 04 add $0x4 , %r12
401169: 4 c 39 e1 cmp %r12 , %rcx
40116 c: 75 ed jne 40115b < phase_6 + 0x7f >
40116 e: be 00 00 00 00 mov $0x0 , %esi
401173: eb 1 a jmp 40118f < phase_6 + 0xb3 >
首先将输入字符串的指针的地址(在栈上)写入 rcx。edx 与 eax 设为 7,将 7 - 第一个输入 写入第一个输入。然后增加 r12 指针指向下一个元素,直到指向尽头(rcx),否则跳转到 0x40115b 重复操作。总之,就是数组每个元素都变成 7-自身,因此现在我的数组变成了 6 5 4 3 2 1。将 esi 变成 0,跳转到 0x40118f 。
1
2
3
4
5
6
40118f: 8b 0c b4 mov (%rsp,%rsi,4),%ecx
401192: b8 01 00 00 00 mov $0x1,%eax
401197: ba d0 32 60 00 mov $0x6032d0,%edx
40119c: 83 f9 01 cmp $0x1,%ecx
40119f: 7f d4 jg 401175 <phase_6+0x99>
4011a1: eb dd jmp 401180 <phase_6+0xa4>
将 rsp[rsi] 放入 ecx,eax 写入 1,edx 是一个地址,然后 rsp[rsi] 与 1 比较。如果大于,跳转到 0x401175,否则跳转到 0x401180。尝试打印 edx:
1
2
3
4
pwndbg> x/12xw 0x6032d0
0x6032d0 <node1>: 0x0000027a 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x00000353 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x00000399 0x00000003 0x00603300 0x00000000
可以看到这是一个类似链表的结构—— node1 中 0x006032e0 指向下一个的地址 0x006032e0,以此类推。我们看看一共有多少:
1
2
3
4
5
6
7
8
pwndbg> x/28xw 0x6032d0
0x6032d0 <node1>: 0x0000027a 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x00000353 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x00000399 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x00000136 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x00000249 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x0000008a 0x00000006 0x00000000 0x00000000
0x603330 <bomb_id>: 0x000007e6 0x00000000 0x00000000 0x00000000
可以看到刚好有 6 个,并且其中数字部分就是 1 2 3 4 5 6 。尝试更换我们的输入,我们发现这些链表的值与我们的输入是无关的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Good work! On to the next...
6 5 4 3 2 1
Breakpoint 1, 0x00000000004010dc in phase_6 ()
pwndbg> c
Continuing.
Breakpoint 2, 0x0000000000401151 in phase_6 ()
pwndbg> c
Continuing.
Breakpoint 3, 0x0000000000401197 in phase_6 ()
pwndbg> x/28xw 0x6032d0
0x6032d0 <node1>: 0x0000027a 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x00000353 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x00000399 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x00000136 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x00000249 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x0000008a 0x00000006 0x00000000 0x00000000
0x603330 <bomb_id>: 0x000007e6 0x00000000 0x00000000 0x00000000
换回 1 2 3 4 5 6 的输入。我这里 rsp[rsi] 是 5,所以大于,跳转到 0x401175。
1
2
3
4
5
6
7
8
401175: 48 8 b 52 08 mov 0x8 ( %rdx ), %rdx
401179: 83 c0 01 add $0x1 , %eax
40117 c: 39 c8 cmp %ecx , %eax
40117 e: 75 f5 jne 401175 < phase_6 + 0x99 >
401180: 48 89 54 f4 20 mov %rdx , 0x20 ( %rsp , %rsi , 8 )
401185: 48 83 c6 01 add $0x1 , %rsi
401189: 48 83 fe 06 cmp $0x6 , %rsi
40118 d: 74 14 je 4011a3 < phase_6 + 0xc7 >
这一段不断增加 rdx,也就是寻找下一个链表元素,并且计数到 eax 中。记住 ecx 是 rsp[rsi] ,所以就相当于是取这个链表的第 rsp[rsi] 个元素,rdx 就指向该元素。
然后将链表的第 rsp[rsi] 个元素的地址放入栈中。结束循环,现在的栈如下:
1
2
3
4
5
6
7
8
9
10
11
00:0000│ rsp 0x7fffffffd9f0 ◂— 0x500000006
01:0008│-00 c 0x7fffffffd9f8 ◂— 0x300000004
02:0010│ r13-4 rbp-4 0x7fffffffda00 ◂— 0x100000002
03:0018│ r12 0x7fffffffda08 —▸ 0x603910 ( input_strings + 400 ) ◂— ' 1 2 3 4 5 6 '
04:0020│+00 c 0x7fffffffda10 —▸ 0x603320 ( node6 ) ◂— 0x60000008a
05:0028│+014 0 x7fffffffda18 —▸ 0x603310 ( node5 ) ◂— 0x500000249
06:0030│+01 c 0x7fffffffda20 —▸ 0x603300 ( node4 ) ◂— 0x400000136
07:0038│+024 0 x7fffffffda28 —▸ 0x6032f0 ( node3 ) ◂— 0x300000399
08:0040│+02 c 0x7fffffffda30 —▸ 0x6032e0 ( node2 ) ◂— 0x200000353
09:0048│+034 0 x7fffffffda38 —▸ 0x6032d0 ( node1 ) ◂— 0x10000027a
0 a: 0050│+03 c 0x7fffffffda40 ◂— 0
可以看到栈中 00 到 02 存储的是输入数字,已经经过被7减处理了,原本是 1 2 3 4 5 6,现在是 6 5 4 3 2 1。03 存储的是输入字符串的指针。04 到 09 存储的是链表节点的指针,顺序是根据处理后的输入决定的。
例如,输入 2 4 5 3 1 6,处理后变成 5 3 2 4 5 1,内存如下:
1
2
3
4
5
6
04:0020│+00c 0x7fffffffda10 —▸ 0x603310 (node5) ◂— 0x500000249
05:0028│+014 0x7fffffffda18 —▸ 0x6032f0 (node3) ◂— 0x300000399
06:0030│+01c 0x7fffffffda20 —▸ 0x6032e0 (node2) ◂— 0x200000353
07:0038│+024 0x7fffffffda28 —▸ 0x603300 (node4) ◂— 0x400000136
08:0040│+02c 0x7fffffffda30 —▸ 0x603320 (node6) ◂— 0x60000008a
09:0048│+034 0x7fffffffda38 —▸ 0x6032d0 (node1) ◂— 0x10000027a
可以看到这里链表指针的顺序就与我们的输入有关。
接下来又是许多代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
4011 a3: 48 8 b 5 c 24 20 mov 0x20 ( %rsp ), %rbx
4011 a8: 48 8 b 44 24 28 mov 0x28 ( %rsp ), %rax
4011 ad: 48 89 43 08 mov %rax , 0x8 ( %rbx )
4011 b1: 48 8 b 54 24 30 mov 0x30 ( %rsp ), %rdx
4011 b6: 48 89 50 08 mov %rdx , 0x8 ( %rax )
4011 ba: 48 8 b 44 24 38 mov 0x38 ( %rsp ), %rax
4011 bf: 48 89 42 08 mov %rax , 0x8 ( %rdx )
4011 c3: 48 8 b 54 24 40 mov 0x40 ( %rsp ), %rdx
4011 c8: 48 89 50 08 mov %rdx , 0x8 ( %rax )
4011 cc: 48 8 b 44 24 48 mov 0x48 ( %rsp ), %rax
4011 d1: 48 89 42 08 mov %rax , 0x8 ( %rdx )
4011 d5: 48 c7 40 08 00 00 00 movq $0x0 , 0x8 ( %rax )
4011 dc: 00
4011 dc: 00
4011 dd: bd 05 00 00 00 mov $0x5 , %ebp
4011 e2: eb 09 jmp 4011ed < phase_6 + 0x111 >
还是先将输入变回 1 2 3 4 5 6,然后会发现这串代码会改变链表的顺序。原始顺序:
1 -> 2 -> 3 -> 4 -> 5 -> 6
在存放顺序是 6 5 4 3 2 1 的情况下,这些操作的结果如下(我省略了其他代码的结果):
1
2
3
4
5
6
0 x4011ad < phase_6 + 209 > movq %rax , 8 ( %rbx ) [ node6 + 8 ] => 0x603310 ( node5 ) ◂— 0x500000249
0 x4011b6 < phase_6 + 218 > movq %rdx , 8 ( %rax ) [ node5 + 8 ] => 0x603300 ( node4 ) ◂— 0x400000136
0 x4011bf < phase_6 + 227 > movq %rax , 8 ( %rdx ) [ node4 + 8 ] => 0x6032f0 ( node3 ) ◂— 0x300000399
0 x4011c8 < phase_6 + 236 > movq %rdx , 8 ( %rax ) [ node3 + 8 ] => 0x6032e0 ( node2 ) ◂— 0x200000353
0 x4011d1 < phase_6 + 245 > movq %rax , 8 ( %rdx ) [ node2 + 8 ] => 0x6032d0 ( node1 ) ◂— 0x10000027a
0 x4011d5 < phase_6 + 249 > movq $0 , 8 ( %rax ) [ node1 + 8 ] => 0
再次打印,发现顺序发生变化:
1
2
3
4
5
6
7
8
pwndbg> x/28xw 0x6032d0
0x6032d0 <node1>: 0x0000027a 0x00000001 0x00000000 0x00000000
0x6032e0 <node2>: 0x00000353 0x00000002 0x006032d0 0x00000000
0x6032f0 <node3>: 0x00000399 0x00000003 0x006032e0 0x00000000
0x603300 <node4>: 0x00000136 0x00000004 0x006032f0 0x00000000
0x603310 <node5>: 0x00000249 0x00000005 0x00603300 0x00000000
0x603320 <node6>: 0x0000008a 0x00000006 0x00603310 0x00000000
0x603330 <bomb_id>: 0x000007e6 0x00000000 0x00000000 0x00000000
6 -> 5 -> 4 -> 3 -> 2 -> 1 -> NaN
所以,该函数的功能就是根据栈中存放的顺序,重新调整链表节点的顺序。例如输入 1 2 3 4 5 6,存放到栈中为 6 5 4 3 2 1 ,于是链表顺序变成 6 5 4 3 2 1。
尝试输入 5 3 1 2 6 4 ,存放到栈中为 2 4 6 5 1 3,链表顺序为 2 4 6 5 1 3。
接着跳转到 0x4011ed,将 rbx 指向的下一个节点地址放入 rax,将该下一个节点的值放入 eax,让 rbx 指向的节点的值与该值比较。如果大于等于,跳转;否则爆炸。
1
2
3
4
5
4011ed: 48 8b 43 08 mov 0x8(%rbx),%rax
4011f1: 8b 00 mov (%rax),%eax
4011f3: 39 03 cmp %eax,(%rbx)
4011f5: 7d ed jge 4011e4 <phase_6+0x108>
4011f7: e8 4b 02 00 00 callq 401447 <explode_bomb>
想要跳出循环,需要在上面满足该条件,也就是 ebp 等于 0。而 ebp 一开始为 5,说明会进行遍历操作。
1
2
3
4011e4: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011e8: 83 ed 01 sub $0x1,%ebp
4011eb: 74 11 je 4011fe <phase_6+0x122>
若此处跳转,就可以结束了。所以,我们的目标明确了起来——找到一组 1-6 的顺序,可以调整链表的连接顺序,进而满足最后的每个节点大于另一个节点的条件 。
观察原始链表中的大小,可以得到正确的排序:
链表 id 值 3 0x399 2 0x353 1 0x27a 5 0x249 4 0x136 6 0x08a
也就是处理后为 3 2 1 5 4 6,处理前为 4 5 6 2 3 1。第六阶段密码为 4 5 6 2 3 1。
结束了? 翻一下汇编,我们会发现一个神秘的函数 fun7,它会在 secret_phase 中被调用。搜索 secret_phase,发现它隐藏在 phase_defused 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
00000000004015 d6 < phase_defused > :
4015 d6: 48 83 ec 78 sub $0x78 , %rsp
4015 da: 64 48 8 b 04 25 28 00 mov %fs : 0x28 , %rax
4015 e1: 00 00
4015 e3: 48 89 44 24 68 mov %rax , 0x68 ( %rsp )
4015 e8: 31 c0 xor %eax , %eax
4015 ea: 83 3 d 7 b 21 20 00 06 cmpl $0x6 , 0x20217b ( %rip ) # 60376c <num_input_strings>
4015 f1: 74 15 je 401608 < phase_defused + 0x32 >
4015 f3: 48 8 b 44 24 68 mov 0x68 ( %rsp ), %rax
4015 f8: 64 48 33 04 25 28 00 xor %fs : 0x28 , %rax
4015 ff: 00 00
401601: 75 67 jne 40166a < phase_defused + 0x94 >
401603: 48 83 c4 78 add $0x78 , %rsp
401607: c3 retq
发现其中要想进入隐藏阶段,必须满足 cmpl $0x6,0x20217b(%rip),这就是 num_input_strings 的地址。在 read_line 中,每次读取一行的输入,都会将该变量加一。所以这一行代表着结束第六阶段后才能解锁。
1
2
3
4
5
6
7
8
401608: 4 c 8 d 44 24 10 lea 0x10 ( %rsp ), %r8
40160 d: 48 8 d 4 c 24 0 c lea 0xc ( %rsp ), %rcx
401612: 48 8 d 54 24 08 lea 0x8 ( %rsp ), %rdx
401617: be 19 26 40 00 mov $0x402619 , %esi
40161 c: bf 70 38 60 00 mov $0x603870 , %edi
401621: e8 7 a f5 ff ff callq 400ba0 < __isoc99_sscanf@plt >
401626: 83 f8 03 cmp $0x3 , %eax
401629: 74 0 c je 401637 < phase_defused + 0x61 >
调试可以发现,该 sscanf 的输入是 input_strings+240,也就是第四阶段输入的字符串,例如我的是 13 3。匹配的格式是 %d %d %s,说明需要额外输入一个字符串。
尝试改变第四阶段输入为 13 3 hello,进入 0x401637 :
1
2
3
4
5
6
7
8
9
10
11
401637: be 22 26 40 00 mov $0x402622 , %esi
40163 c: 48 8 d 7 c 24 10 lea 0x10 ( %rsp ), %rdi
401641: e8 04 fd ff ff callq 40134a < strings_not_equal >
401646: 85 c0 test %eax , %eax
401648: 75 e1 jne 40162b < phase_defused + 0x55 >
40164 a: bf f8 24 40 00 mov $0x4024f8 , %edi
40164 f: e8 8 c f4 ff ff callq 400ae0 < puts@plt >
401654: bf 20 25 40 00 mov $0x402520 , %edi
401659: e8 82 f4 ff ff callq 400ae0 < puts@plt >
40165 e: b8 00 00 00 00 mov $0x0 , %eax
401663: e8 f7 fb ff ff callq 40125f < secret_phase >
发现 esi 为字符串 urxvt,然后调用字符串匹配。因此我们修改输入为 13 3 urxvt,成功进入隐藏阶段。
一开始就要输入,然后调用 strtol 将字符串转换为 long。接着返回值移入 rbx,eax 变成输入的数减一,与 0x3e8 (1000) 比较,如果大于,爆炸。因此我们尝试输入 987。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
00000000040125 f < secret_phase > :
40125 f: 53 push %rbx
401260: e8 43 02 00 00 callq 4014a8 < read_line >
401265: ba 0 a 00 00 00 mov $0xa , %edx
40126 a: be 00 00 00 00 mov $0x0 , %esi
40126 f: 48 89 c7 mov %rax , %rdi
401272: e8 09 f9 ff ff callq 400b80 < strtol@plt >
401277: 48 89 c3 mov %rax , %rbx
40127 a: 8 d 40 ff lea - 0x1 ( %rax ), %eax
40127 d: 3 d e8 03 00 00 cmp $0x3e8 , %eax
401282: 77 27 ja 4012ab < secret_phase + 0x4c >
401284: 89 de mov %ebx , %esi
401286: bf f0 30 60 00 mov $0x6030f0 , %edi
40128 b: e8 90 ff ff ff callq 401220 < fun7 >
接下来调用函数 fun7,第一个参数为 0x6030f0,第二个参数为我们输入的数。可以发现我们希望这个函数的返回值是 4,这就是最终目标。
1
2
3
4
5
6
0000000000401220 < fun7 > :
401220: 48 85 ff test %rdi , %rdi
401223: 74 34 je 401259 < fun7 + 0x39 >
...
401259: b8 ff ff ff ff mov $0xffffffff , %eax
40125 e: c3 retq
首先判断 rdi 不为 0,否则直接返回 -1 。
然后将 rdi 指向的值放入 edx,这里的值是 0x24,判断与我们输入数的关系。
如果输入大于 edx,不跳转,eax 写入 0,跳转到 40124a。将 rdi 后面隔 16 个字节的中数写入 rdi,递归调用 如果输入小于等于 edx,跳转,将 rdi 后面隔 8 个字节中的数写入 rdi,递归调用 打印这些值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pwndbg> x/64xg $rdi
0x6030f0 <n1>: 0x0000000000000024 0x0000000000603110 <n21>
0x603100 <n1+16>: 0x0000000000603130 <n22> 0x0000000000000000
0x603110 <n21>: 0x0000000000000008 0x0000000000603190 <n31>
0x603120 <n21+16>: 0x0000000000603150 <n32> 0x0000000000000000
0x603130 <n22>: 0x0000000000000032 0x0000000000603170 <n33>
0x603140 <n22+16>: 0x00000000006031b0 <n34> 0x0000000000000000
0x603150 <n32>: 0x0000000000000016 0x0000000000603270 <n43>
0x603160 <n32+16>: 0x0000000000603230 <n44> 0x0000000000000000
0x603170 <n33>: 0x000000000000002d 0x00000000006031d0 <n45>
0x603180 <n33+16>: 0x0000000000603290 <n46> 0x0000000000000000
0x603190 <n31>: 0x0000000000000006 0x00000000006031f0 <n41>
0x6031a0 <n31+16>: 0x0000000000603250 <n42> 0x0000000000000000
0x6031b0 <n34>: 0x000000000000006b 0x0000000000603210 <n47>
0x6031c0 <n34+16>: 0x00000000006032b0 <n48> 0x0000000000000000
0x6031d0 <n45>: 0x0000000000000028 0x0000000000000000
0x6031e0 <n45+16>: 0x0000000000000000 0x0000000000000000
0x6031f0 <n41>: 0x0000000000000001 0x0000000000000000
0x603200 <n41+16>: 0x0000000000000000 0x0000000000000000
0x603210 <n47>: 0x0000000000000063 0x0000000000000000
0x603220 <n47+16>: 0x0000000000000000 0x0000000000000000
0x603230 <n44>: 0x0000000000000023 0x0000000000000000
0x603240 <n44+16>: 0x0000000000000000 0x0000000000000000
0x603250 <n42>: 0x0000000000000007 0x0000000000000000
0x603260 <n42+16>: 0x0000000000000000 0x0000000000000000
0x603270 <n43>: 0x0000000000000014 0x0000000000000000
0x603280 <n43+16>: 0x0000000000000000 0x0000000000000000
0x603290 <n46>: 0x000000000000002f 0x0000000000000000
0x6032a0 <n46+16>: 0x0000000000000000 0x0000000000000000
0x6032b0 <n48>: 0x00000000000003e9 0x0000000000000000
0x6032c0 <n48+16>: 0x0000000000000000 0x0000000000000000
0x6032d0 <node1>: 0x000000010000027a 0x0000000000603310
0x6032e0 <node2>: 0x0000000200000353 0x00000000006032d0
这些节点的相互引用关系如图:
树的指针参考 不断调用,可以发现当 rdi 指向 n48 时,n48 中的值为 3e9 (1001),必定大于我们的输入。此时结束,跳转到 0x40123d,此时会取 rdi 指向位置的后 8 个字节 ,放入 rdi(与之前的 16 字节不同! )。不过现在 n48 对应的这个值为 0 。再次递归调用。这时候因为 rdi 参数为 0,所以将 -1 写入 eax,返回。
8 字节情况返回 401246:eax = eax + eax,例如我这里变成了-2 16 字节情况返回 401253:eax = rax + rax + 1,例如我这里有几个状态:-2 + -2 + 1 = -3-3 + -3 + 1 = -5-5 + -5 + 1 = -9 最终在 987 输入的情况下,返回为 -9 于是,当时让我迷惑的事情来了:secret_phase 中希望我们最后得到的返回值是整数 4,这怎么做到呢?
尝试输入 1 调试,发现当输入的数恰好等于叶子结点时,就会停止递归调用。此时会运行 0x40122f ,将 eax 变成 0 。所以,从反向推导,要得到 4 应该是这样的:
0 -> 2 * 0 + 1 = 1 -> 1 * 2 = 2 -> 2 * 2 = 4
反向过程先是一个大于,然后是两个小于等于。正向过程就是先两个小于等于,然后一个大于。
参考图片:
树的指针参考2 答案为 7 。
总结 这个实验确实量大管饱,我差不多前前后后花了好几天时间才写完。
对于此前没有接触过汇编的我来说,确实有些难度。还好借助了 GDB Dashboard、pwndbg 等强大的调试器,以及部分地方参考了 arthals 的北大 bomblab 博客 。自然有些细节上还是有差异的,并且该博客没有写 secret phase。
经过这样的折腾,确实感觉自己对汇编的掌握更加深入。也让我想起了很久之前手挫 cs61b 的 gitlet 的快感()
该指南很多地方都是我做 lab 的时候,边想边写的,所以语句可能不通顺,也不是最适合理解的顺序。该指南只记录了我解题中的一些思考过程。如果能在某个卡点帮到你,有所启发,那么我的目的就达成了。
参考资料