使用GDB+pwndbg轻松解决BinaryBomb!
前言
最近终于把鸽了近一年多的BinaryBomb给做完了,花了一天时间,内力修炼的还不够啊。这里记录一下解题思路,方便以后回顾,同时也疏理一下解题用到的技术点。注意:本文主要讲解题的过程和思路,可在Binary-Bomb’s Readme中查看相关的基础知识。
BinaryBomb简介
BinaryBomb是Carnegie Mellon University(卡内基·梅隆大学) CS课上发布的一道作业,在“Lab Assignments” page of Bryant and O’Hallaron’s website中可以找到以下对BinaryBomb的介绍:
A "binary bomb" is a program provided to students as an object code file. When run, it prompts the user to type in 6 different strings. If any of these is incorrect, the bomb ``explodes,'' printing an error message and logging the event on a grading server. Students must ``defuse'' their own unique bomb by disassembling and reverse engineering the program to determine what the 6 strings should be. The lab teaches students to understand assembly language, and also forces them to learn how to use a debugger. It's also great fun. A legendary lab among the CMU undergrads.
译文:
“二进制炸弹”是作为目标代码文件提供给学生的程序。 运行时,它会提示用户输入 6 个不同的字符串。 如果其中任何一个不正确,炸弹就会“爆炸”,打印错误消息并将事件记录在评分服务器上。 学生必须通过对程序进行分解和逆向工程来确定 6 个字符串应该是什么,从而“化解”他们自己独特的炸弹。 实验室教学生理解汇编语言,并迫使他们学习如何使用调试器。 这也很有趣。 CMU 本科生中的传奇实验室。
正如介绍所说,BinaryBomb非常用趣,虽然这作业是2013年发布的,但时至今日,仍能从中学到许多有用的东西。毕竟我们现在用的计算机除了从32位变成了64位外,寄存器种类,栈结构,函数调用机制等基本没有变化。
GDB+pwndbg简介
GDB即GNU Debugger,是GNU软件系统中的标准调试器,此外GDB也是个具有移携性的调试器,经过移携需求的调修与重新编译,如今许多的类UNIX操作系统上都可以使用GDB,而现有GDB所能支持调试的编程语言有C、C++、Pascal以及FORTRAN。
pwndbg是一个GDB插件,它可以减少使用 GDB 进行调试,重点关注低级软件开发人员、硬件黑客、逆向工程师和漏洞利用开发人员所需的功能。
简单来说,就是用linux自带的古老而又强大的调试器配上一个现代的好用插件,在鸽了一年后用一天时间解决一个差不多十年前发布的CS课作业。也许单用GDB才是这个作业的初衷,但裸GDB用起来非常的麻烦,所有想看的信息都得自己一个个敲命令,完了还运行一步敲一次,等到做完作业,手都起茧喽。人生苦短,用GDB前记得上pwndbg。
本人运行环境
- OS: Arch Linux x86_64
- Shell: zsh 5.8.1
- Debuger: GNU gdb (GDB) 11.2 + pwndbg 1.0.3
关于pwndbg的安装,在github-pwndbg上有详细描述;所有linux都有自带gdb,建议用Ubuntu,Windows上可以用WSL或者MinGW,又或者直接上虚拟机,具体操作网上教程很多,这里不再赘述。
BinaryBomb二进制文件可以在github-qspidy-BinaryBomb上下载,里面有许多不同的BinaryBomb,虽然答案不同,但解答的过程基本一样。懒人命令:git clone https://github.com/qspidy/BinaryBomb
同时附上官方bomb下载链接。下载完在下载文件的目录执行tar xvf bomb.tar
即可解压。
用到的gdb命令一览表
命令 | 说明 |
---|---|
r | 即run,开始运行调试的程序 |
x | 检查内存(Examine memory),将指定地址的内容按指定格式和长度输出 |
b | 即break,在指定位置设置断点 |
c | 即continue,fg,从当前开始继续运行 |
h | 即help,显示命令的帮助说明,善用help命令是变强的第一步 |
si | 即stepi,执行一条汇编指令(遇到函数跳入) |
ni | 即nexti,执行一条汇编指令(遇到函数跳过) |
bt | 即backtrace,输出所有回溯栈帧,又称调用堆栈 |
fin | 即finish,执行完当前函数并break |
disas | 即disassemble,反汇编指定内存区域 |
stack | 输出当前活动记录的栈信息 |
info r | 即info registers,列出寄存器和其中的内容 |
开始调试BinaryBomb
- 在BinaryBomb二进制文件目录下执行
gdb bomb
开始调试: - 个人习惯先把所有的phases给反汇编出来复制到记事本,方便之后查看和相关地址的复制。执行
disas phase_1
显示phase_1的反汇编:
phase_1
- 直接进入正题,一切准备就绪后,在phase_1函数入口处下断点:
b phase_1
,执行程序r
: - 因为还不知道正确答案,这里随便输入一串字符
helloworld
,继续执行c
,这时pwndbg的作用就开始体现了,每次运行到断点停下时,都默认会显示下列信息:
- Registers,即寄存器信息,与gdb中运行
info r
输出类似: - Disasm,即反汇编信息,与gdb中运行
disas
输出类似: - Stack,即当前活动记录的栈信息,与gdb中运行
stack
输出类似: - Backtrack,即堆栈回溯,显示线程的函数调用堆栈,与gdb中运行
bt
输出类似
- 注意这句:
0x400e96 <phase_1+9> call strings_not_equal
,从函数名能看出是调用了一个比较字符串是否相等的函数,那比较的是哪两个字符串呢?上一句指令是0x400e91 <phase_1+4> mov esi, 0x402490
,可以合理怀疑地址0x402490
是其中一个字符串首地址,且作为参数传入该函数,执行x/s 0x402490
尝试以字符串格式输出该地址信息:好家伙,看来这个就是答案了。
- 但假如没有经验,不知道应该看地址
0x402490
的信息呢?那只能一步步运行看看了,连续执行si
直到strings_not_equal
函数,可以注意到下面这一段:很明显,参与比较的两个字符串分别是
rdi
指向的helloworld
和rsi
指向的Why make trillions when we could make... billions?
,前者是之前随便输的,后者是程序里写好的。 - 后面的几句汇编是判断eax即strings_not_equal函数的返回值是否为0,为0则说明两字符串相等,phase_1退出,否则执行到
explode_bomb
函数,BOOM!!!
phase_2
phase_1顶多算是热身,接下来的才是真正的puzzle。先看一眼phase_2的反汇编:
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
33Dump of assembler code for function phase_2:
0x0000000000400ea9 <+0>: push rbp
0x0000000000400eaa <+1>: push rbx
0x0000000000400eab <+2>: sub rsp,0x28
0x0000000000400eaf <+6>: mov rax,QWORD PTR fs:0x28
0x0000000000400eb8 <+15>: mov QWORD PTR [rsp+0x18],rax
0x0000000000400ebd <+20>: xor eax,eax
0x0000000000400ebf <+22>: mov rsi,rsp
0x0000000000400ec2 <+25>: call 0x40152b <read_six_numbers>
0x0000000000400ec7 <+30>: cmp DWORD PTR [rsp],0x0
0x0000000000400ecb <+34>: jne 0x400ed4 <phase_2+43>
0x0000000000400ecd <+36>: cmp DWORD PTR [rsp+0x4],0x1
0x0000000000400ed2 <+41>: je 0x400ed9 <phase_2+48>
0x0000000000400ed4 <+43>: call 0x401509 <explode_bomb>
0x0000000000400ed9 <+48>: mov rbx,rsp
0x0000000000400edc <+51>: lea rbp,[rsp+0x10]
0x0000000000400ee1 <+56>: mov eax,DWORD PTR [rbx+0x4]
0x0000000000400ee4 <+59>: add eax,DWORD PTR [rbx]
0x0000000000400ee6 <+61>: cmp DWORD PTR [rbx+0x8],eax
0x0000000000400ee9 <+64>: je 0x400ef0 <phase_2+71>
0x0000000000400eeb <+66>: call 0x401509 <explode_bomb>
0x0000000000400ef0 <+71>: add rbx,0x4
0x0000000000400ef4 <+75>: cmp rbx,rbp
0x0000000000400ef7 <+78>: jne 0x400ee1 <phase_2+56>
0x0000000000400ef9 <+80>: mov rax,QWORD PTR [rsp+0x18]
0x0000000000400efe <+85>: xor rax,QWORD PTR fs:0x28
0x0000000000400f07 <+94>: je 0x400f0e <phase_2+101>
0x0000000000400f09 <+96>: call 0x400b00 <__stack_chk_fail@plt>
0x0000000000400f0e <+101>: add rsp,0x28
0x0000000000400f12 <+105>: pop rbx
0x0000000000400f13 <+106>: pop rbp
0x0000000000400f14 <+107>: ret
End of assembler dump.同样,在phase_2函数入口下断点
b phase_2
,继续执行c
,因为注意到phase_2的反汇编中有调用一个叫read_six_numbers
的函数,所以输入的应该是六个数字,这里输入了1 2 3 4 5 6
,因为计算机内通常不会有这种简单的数字,所以这样输可以帮助我们观察哪些指令对输入的哪些参数进行了处理,更方便观察程序逻辑。多次执行命令
si
或直接ni 8
(执行8次ni
)到read_six_numbers
函数结束:cmp dword ptr [rsp], 0
一句表示将rsp
寄存器指向的内容以4字节(dword)格式解释,相当于int型,再与右边的0相减(sub),并改变相应的标志位。看一下当前的栈信息:可以看到当前的栈里存的是之前输入的
1 2 3 4 5 6
,这样看可能不是很直观,输入x/10wx $rsp
显示10个、地址从rsp
开始连续的长度为4字节的内存信息:这里就能直观的看到栈里存储的数据格式了,其中
dword ptr [rsp]
是第一个参数即0x00000001
,dword ptr [rsp+4]
是第二个参数0x00000002
,dword ptr [rsp+8]
是第三个参数0x00000003
,以此类推。而dword ptr [rsp]
指的正是栈顶的4字节,也即之前输入的第一个参数。它和0一比较,不相等,便跳到了explode_bomb
函数。所以推出第一个参数应该为0。知道了第一个参数,可以选择重新运行,把输入参数的第一个改为0,也可以先不管它,手动跳过
explode_bomb
函数继续运行。找到jne phase_2+43
下一条指令地址为0x400ecd
,然后执行set $rip=0x400ecd
设置rip
寄存器值为该地址,因为rip
(指令指针寄存器)始终存的是下一条即将执行的指令地址:发现这一段与上一段代码类似,可知第二个参数应该为1。
同样的方法跳过
explode_bomb
函数,从这里开始就有对参数进行计算的操作了,之前全是将参数和某个字符或数字比较,很容易很获得答案。继续一步步执行,发现这样一段:既然有条件跳转,就存在分支,这里pwndbg的输出默认只显示将会运行的指令,也就是说它预先执行了一下,帮你预测好了接下来几步的指令。为查看pwndbg省略掉的反汇编代码,执行
disas
输出当前函数反汇编,当前指令位置会有箭头标注:很明显
cmp DWORD PTR [rbx+0x8],eax
的结果应该为相等,其中前者DWORD PTR [rbx+0x8]
是第三个参数,eax
存的是前几行指令的结果,再看前几行指令:对
rbp
和rbx
进行赋值,然后计算第一参数与第二参数的和并赋于eax
,所以eax
的值应该为1,即第三个参数应该为1。
这里有一个小细节,因为rbp
和rbx
的旧值在这里被覆盖了,所以在函数开头有这样一句将rbp
和rbx
的旧值保存在栈里:并在函数返回前进行恢复:
经典的保存现场和恢复现场操作,这在之后也会经常看到。
继续向下执行,发现有这样三行指令:
rbp
和rbx
分别是之前赋的rsp+0x10
和rsp
,即第五个参数和第一个参数的地址,对rbx
加4即让rbx
指向第下一个参数,然后比较一下看rbx
是不是已经到了第五个参数,不是则跳到之前的0x400ee1 <phase_2+56> mov eax, dword ptr [rbx + 4]
位置执行,否则退出循环。循环里执行的即是上一步的内容:检查第x个参数与第x+1个参数的和是否等于第x+2个参数,x值为1到5。至此,phase_2已解决。
phase_3
到这里基本上已经上手了,不同于phase_2的数学计算,phase_3的重点在于理清程序执行逻辑,找出关键代码。同样附上phase_3的反汇编:
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77Dump of assembler code for function phase_3:
0x0000000000400f15 <+0>: sub rsp,0x28
0x0000000000400f19 <+4>: mov rax,QWORD PTR fs:0x28
0x0000000000400f22 <+13>: mov QWORD PTR [rsp+0x18],rax
0x0000000000400f27 <+18>: xor eax,eax
0x0000000000400f29 <+20>: lea r8,[rsp+0x14]
0x0000000000400f2e <+25>: lea rcx,[rsp+0xf]
0x0000000000400f33 <+30>: lea rdx,[rsp+0x10]
0x0000000000400f38 <+35>: mov esi,0x4024ee
0x0000000000400f3d <+40>: call 0x400bb0 <__isoc99_sscanf@plt>
0x0000000000400f42 <+45>: cmp eax,0x2
0x0000000000400f45 <+48>: jg 0x400f4c <phase_3+55>
0x0000000000400f47 <+50>: call 0x401509 <explode_bomb>
0x0000000000400f4c <+55>: cmp DWORD PTR [rsp+0x10],0x7
0x0000000000400f51 <+60>: ja 0x401053 <phase_3+318>
0x0000000000400f57 <+66>: mov eax,DWORD PTR [rsp+0x10]
=> 0x0000000000400f5b <+70>: jmp QWORD PTR [rax*8+0x402500]
0x0000000000400f62 <+77>: mov eax,0x69
0x0000000000400f67 <+82>: cmp DWORD PTR [rsp+0x14],0x33c
0x0000000000400f6f <+90>: je 0x40105d <phase_3+328>
0x0000000000400f75 <+96>: call 0x401509 <explode_bomb>
0x0000000000400f7a <+101>: mov eax,0x69
0x0000000000400f7f <+106>: jmp 0x40105d <phase_3+328>
0x0000000000400f84 <+111>: mov eax,0x66
0x0000000000400f89 <+116>: cmp DWORD PTR [rsp+0x14],0x30b
0x0000000000400f91 <+124>: je 0x40105d <phase_3+328>
0x0000000000400f97 <+130>: call 0x401509 <explode_bomb>
0x0000000000400f9c <+135>: mov eax,0x66
0x0000000000400fa1 <+140>: jmp 0x40105d <phase_3+328>
0x0000000000400fa6 <+145>: mov eax,0x71
0x0000000000400fab <+150>: cmp DWORD PTR [rsp+0x14],0x2d5
0x0000000000400fb3 <+158>: je 0x40105d <phase_3+328>
0x0000000000400fb9 <+164>: call 0x401509 <explode_bomb>
0x0000000000400fbe <+169>: mov eax,0x71
0x0000000000400fc3 <+174>: jmp 0x40105d <phase_3+328>
0x0000000000400fc8 <+179>: mov eax,0x7a
0x0000000000400fcd <+184>: cmp DWORD PTR [rsp+0x14],0x235
0x0000000000400fd5 <+192>: je 0x40105d <phase_3+328>
0x0000000000400fdb <+198>: call 0x401509 <explode_bomb>
0x0000000000400fe0 <+203>: mov eax,0x7a
0x0000000000400fe5 <+208>: jmp 0x40105d <phase_3+328>
0x0000000000400fe7 <+210>: mov eax,0x6c
0x0000000000400fec <+215>: cmp DWORD PTR [rsp+0x14],0x192
0x0000000000400ff4 <+223>: je 0x40105d <phase_3+328>
0x0000000000400ff6 <+225>: call 0x401509 <explode_bomb>
0x0000000000400ffb <+230>: mov eax,0x6c
0x0000000000401000 <+235>: jmp 0x40105d <phase_3+328>
0x0000000000401002 <+237>: mov eax,0x70
0x0000000000401007 <+242>: cmp DWORD PTR [rsp+0x14],0x1a6
0x000000000040100f <+250>: je 0x40105d <phase_3+328>
0x0000000000401011 <+252>: call 0x401509 <explode_bomb>
0x0000000000401016 <+257>: mov eax,0x70
0x000000000040101b <+262>: jmp 0x40105d <phase_3+328>
0x000000000040101d <+264>: mov eax,0x73
0x0000000000401022 <+269>: cmp DWORD PTR [rsp+0x14],0x325
0x000000000040102a <+277>: je 0x40105d <phase_3+328>
0x000000000040102c <+279>: call 0x401509 <explode_bomb>
0x0000000000401031 <+284>: mov eax,0x73
0x0000000000401036 <+289>: jmp 0x40105d <phase_3+328>
0x0000000000401038 <+291>: mov eax,0x6d
0x000000000040103d <+296>: cmp DWORD PTR [rsp+0x14],0x217
0x0000000000401045 <+304>: je 0x40105d <phase_3+328>
0x0000000000401047 <+306>: call 0x401509 <explode_bomb>
0x000000000040104c <+311>: mov eax,0x6d
0x0000000000401051 <+316>: jmp 0x40105d <phase_3+328>
0x0000000000401053 <+318>: call 0x401509 <explode_bomb>
0x0000000000401058 <+323>: mov eax,0x79
0x000000000040105d <+328>: cmp al,BYTE PTR [rsp+0xf]
0x0000000000401061 <+332>: je 0x401068 <phase_3+339>
0x0000000000401063 <+334>: call 0x401509 <explode_bomb>
0x0000000000401068 <+339>: mov rax,QWORD PTR [rsp+0x18]
0x000000000040106d <+344>: xor rax,QWORD PTR fs:0x28
0x0000000000401076 <+353>: je 0x40107d <phase_3+360>
0x0000000000401078 <+355>: call 0x400b00 <__stack_chk_fail@plt>
0x000000000040107d <+360>: add rsp,0x28
0x0000000000401081 <+364>: ret
End of assembler dump.不像phase_2中有
read_six_numbers
,phase_3没法直接从反汇编中窥见输入的参数格式,那就暂时输mrorange
好了,然后一步步执行,并观察pwndbg输出的registers信息,直到scanf
函数:pwndbg直接将传入
scanf
的参数展示出来,哪怕是新手小白也可以注意到‘%d %c %d‘
就是phase_3正解的输入格式。有经验的话就可以直接x/s 0x4024ee
查看输入格式了,因为call scanf
的上一句指令即是用格式字符串地址赋值rsi
寄存器,再作为参数传给scanf
函数。确定了输入格式,重新运行输入
1 o 1
开始进一步分析:代码在
scanf
之后判断了输入参数个数是否大于2个,又判断了第一个参数是否小于等于7,都通过则跳转到第一个参数*8 + 0x402500
处,先不管这是哪,继续往下执行。注意到这句cmp dword ptr [rsp + 0x14], 0x30b
,看似是在判断一个参数是否为0x30b
,但是哪一个参数呢?之前输入的1 o 1
不是很合适,因为第一个参数和第三个参数一样,在栈里就看不出哪个位置对应哪个参数,我这里又将输入改成了1 o 2
,再看一下栈区:dword ptr [rsp + 0x14]
的值是2,所以dword ptr [rsp + 0x14]
对应的是第三个参数。因此,第三个参数应该为0x30b
,但注意这个数是十六进制,而输入格式是%d
即十进制,所以真正的第三参数是779
。继续向下,跳转到了
phase_3+3280x40105d <phase_3+328> cmp al, byte ptr [rsp + 0xf]
,byte ptr [rsp + 0xf]
的值可以在栈里看到是0x6f
,也即第二个参数o
的ASCII码,可以输入x/bc $rsp+0xf
验证。将第二个参数和al
即rax
的最后一字节比较,那现在rax
里存的又是什么呢?可以注意到是在比较第三个参数前有不显眼的一行mov eax, 0x66
,0x66
对应的字母是f
。至此,可以确定一组正确的答案为1 f 779
。
- Tip: 在Python命令行中,输入十六进制数会输出它的十进制;使用
print('{:c}'.format(0x66))
可以输出0x66
对应的ASCII字符。
- 之所以说得到的只是其中一组正确答案,是因为第一个参数的值会决定跳转到的
第一个参数*8 + 0x402500
位置,而第一个参数值又小于等于7,不能为负,所以对应不同的第一参数,一共会有八个不同的答案。得出答案的步骤完全一样,就不再赘述。
phase_4
先附上phase_4和内部函数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
25
26
27
28
29
30Dump of assembler code for function phase_4:
0x00000000004010b5 <+0>: sub rsp,0x18
0x00000000004010b9 <+4>: mov rax,QWORD PTR fs:0x28
0x00000000004010c2 <+13>: mov QWORD PTR [rsp+0x8],rax
0x00000000004010c7 <+18>: xor eax,eax
0x00000000004010c9 <+20>: lea rcx,[rsp+0x4]
0x00000000004010ce <+25>: mov rdx,rsp
0x00000000004010d1 <+28>: mov esi,0x40268f
0x00000000004010d6 <+33>: call 0x400bb0 <__isoc99_sscanf@plt>
0x00000000004010db <+38>: cmp eax,0x2
0x00000000004010de <+41>: jne 0x4010e6 <phase_4+49>
0x00000000004010e0 <+43>: cmp DWORD PTR [rsp],0xe
0x00000000004010e4 <+47>: jbe 0x4010eb <phase_4+54>
0x00000000004010e6 <+49>: call 0x401509 <explode_bomb>
0x00000000004010eb <+54>: mov edx,0xe
0x00000000004010f0 <+59>: mov esi,0x0
0x00000000004010f5 <+64>: mov edi,DWORD PTR [rsp]
0x00000000004010f8 <+67>: call 0x401082 <func4>
0x00000000004010fd <+72>: cmp eax,0x13
0x0000000000401100 <+75>: jne 0x401109 <phase_4+84>
0x0000000000401102 <+77>: cmp DWORD PTR [rsp+0x4],0x13
0x0000000000401107 <+82>: je 0x40110e <phase_4+89>
0x0000000000401109 <+84>: call 0x401509 <explode_bomb>
0x000000000040110e <+89>: mov rax,QWORD PTR [rsp+0x8]
0x0000000000401113 <+94>: xor rax,QWORD PTR fs:0x28
0x000000000040111c <+103>: je 0x401123 <phase_4+110>
0x000000000040111e <+105>: call 0x400b00 <__stack_chk_fail@plt>
0x0000000000401123 <+110>: add rsp,0x18
0x0000000000401127 <+114>: ret
End of assembler dump.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24Dump of assembler code for function func4:
0x0000000000401082 <+0>: push rbx
0x0000000000401083 <+1>: mov eax,edx
0x0000000000401085 <+3>: sub eax,esi
0x0000000000401087 <+5>: mov ebx,eax
0x0000000000401089 <+7>: shr ebx,0x1f
0x000000000040108c <+10>: add eax,ebx
0x000000000040108e <+12>: sar eax,1
0x0000000000401090 <+14>: lea ebx,[rax+rsi*1]
0x0000000000401093 <+17>: cmp ebx,edi
0x0000000000401095 <+19>: jle 0x4010a3 <func4+33>
0x0000000000401097 <+21>: lea edx,[rbx-0x1]
0x000000000040109a <+24>: call 0x401082 <func4>
0x000000000040109f <+29>: add eax,ebx
0x00000000004010a1 <+31>: jmp 0x4010b3 <func4+49>
0x00000000004010a3 <+33>: mov eax,ebx
0x00000000004010a5 <+35>: cmp ebx,edi
0x00000000004010a7 <+37>: jge 0x4010b3 <func4+49>
0x00000000004010a9 <+39>: lea esi,[rbx+0x1]
0x00000000004010ac <+42>: call 0x401082 <func4>
0x00000000004010b1 <+47>: add eax,ebx
0x00000000004010b3 <+49>: pop rbx
0x00000000004010b4 <+50>: ret
End of assembler dump.首先仍然是确定输入的格式。找到
scanf
函数,从其上一行中得到格式字符串地址,执行x/s 0x40268f
,得到格式字符串为"%d %d"
。phase_4的主函数部分的逻辑很简单,有了前三个phase的热身,应该可以轻松从反汇编中看出其逻辑:先判断输入参数个数是否为2,再判断第一个参数是否小于14,通过则调用函数
func4
,并判断返回值是否为19,最后判断初始输入的第二个参数是否为19,均为是则顺利过关。光从第二步就能得知第一个参数是小于等于14的自然数,第二个参数是19,通过暴力枚举15个可能的输入就能得到答案,甚至不用分析
func4
函数。但是前提是func4
函数里没有改动最初输入的参数,也没有调用explode_bomb
函数。前者在这种级别的题应该不会有,后者通过反汇编发现也满足。赶时间的话就可以开始试答案了,这里不再分析func4
函数,附上伪代码(仅供参考,源自IDA,其中参数a1=输入的第一参数, a2=0, a3=14):1
2
3
4
5
6
7
8
9
10
11
12
13__int64 func4(__int64 a1, __int64 a2, int a3)
{
int v3; // ebx
__int64 result; // rax
v3 = (a3 - (int)a2) / 2 + a2;
if ( v3 > (int)a1 )
return v3 + (unsigned int)func4(a1, a2);
result = (unsigned int)v3;
if ( v3 < (int)a1 )
result = v3 + (unsigned int)func4(a1, (unsigned int)(v3 + 1));
return result;
}
phase_5
先附上phase_5的反汇编:
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
35
36
37
38
39Dump of assembler code for function phase_5:
=> 0x0000000000401128 <+0>: sub rsp,0x18
0x000000000040112c <+4>: mov rax,QWORD PTR fs:0x28
0x0000000000401135 <+13>: mov QWORD PTR [rsp+0x8],rax
0x000000000040113a <+18>: xor eax,eax
0x000000000040113c <+20>: lea rcx,[rsp+0x4]
0x0000000000401141 <+25>: mov rdx,rsp
0x0000000000401144 <+28>: mov esi,0x40268f
0x0000000000401149 <+33>: call 0x400bb0 <__isoc99_sscanf@plt>
0x000000000040114e <+38>: cmp eax,0x1
0x0000000000401151 <+41>: jg 0x401158 <phase_5+48>
0x0000000000401153 <+43>: call 0x401509 <explode_bomb>
0x0000000000401158 <+48>: mov eax,DWORD PTR [rsp]
0x000000000040115b <+51>: and eax,0xf
0x000000000040115e <+54>: mov DWORD PTR [rsp],eax
0x0000000000401161 <+57>: cmp eax,0xf
0x0000000000401164 <+60>: je 0x401195 <phase_5+109>
0x0000000000401166 <+62>: mov ecx,0x0
0x000000000040116b <+67>: mov edx,0x0
0x0000000000401170 <+72>: add edx,0x1
0x0000000000401173 <+75>: cdqe
0x0000000000401175 <+77>: mov eax,DWORD PTR [rax*4+0x402540]
0x000000000040117c <+84>: add ecx,eax
0x000000000040117e <+86>: cmp eax,0xf
0x0000000000401181 <+89>: jne 0x401170 <phase_5+72>
0x0000000000401183 <+91>: mov DWORD PTR [rsp],0xf
0x000000000040118a <+98>: cmp edx,0xf
0x000000000040118d <+101>: jne 0x401195 <phase_5+109>
0x000000000040118f <+103>: cmp ecx,DWORD PTR [rsp+0x4]
0x0000000000401193 <+107>: je 0x40119a <phase_5+114>
0x0000000000401195 <+109>: call 0x401509 <explode_bomb>
0x000000000040119a <+114>: mov rax,QWORD PTR [rsp+0x8]
0x000000000040119f <+119>: xor rax,QWORD PTR fs:0x28
0x00000000004011a8 <+128>: je 0x4011af <phase_5+135>
0x00000000004011aa <+130>: call 0x400b00 <__stack_chk_fail@plt>
0x00000000004011af <+135>: add rsp,0x18
0x00000000004011b3 <+139>: ret
End of assembler dump.同样,通过观察反汇编得到格式字符串地址为
0x40268f
,执行x/s 0x40268f
得到格式字符串为"%d %d"
,这里输入5 10
后继续分析。从
scanf
函数下一行开始一步步执行:判断输入参数个数是否大于1,是则继续;取第一参数的后四位,并覆盖原来的参数,且第一参数的后四位不全为1;这时第一参数的值转化成为0~14之间一个数;从0x0000000000401170 <+72>: add edx,0x1
开始进入循环:
- 其中
cdqe
是使用eax
的最高位拓展rax
高32位的所有位。
注意到这一行
mov eax, dword ptr [rax*4 + 0x402540]
,不禁好奇地址0x402540
里存了什么,执行x/20wx 0x402540
查看内存:果然是一个数组,每个元素长度为4字节,所以
dword ptr [rax*4 + 0x402540]
值为该数组的第rax
个元素。再来看这段循环,它不断的将数组的第rax
个元素再次赋予rax
,同时将rax
的值累加到ecx
寄存器。edx
记录循环的次数,直到rax
值为0xf
时退出循环:退出循环后,判断
edx
的值是否为0xf
,然后判断ecx
和第二参数是否相等,均为是则过关。edx
值为0xf
即循环次数为15次,数组长度为16,所以很有可能ecx
最后就是数组所有元素的和(除下标为0xf
的元素)。再观察数组,发现通过不断的将数组下标对应元素作为新的数组下标,可以遍历完整个数组,因此在15次循环中必然完成了数组的一次遍历,ecx
值即所有元素和(除下标为0xf
的元素)为115
。因此第二个参数应该为115
。那么第一个参数呢?第一个参数是
eax
的初值,也就是第一个下标,然后需要让eax
遍历数组的时候正好在最后才被赋值0xf
,并退出循环。可以暴力枚举,也可以用逆推的方法:最后eax
值为0xf
,那么eax
前一个值为元素0xf
的下标0x6
,再前一个值为元素0x6
的下标0xe
,以此类推,得到最初下标应该为0x5
。或者直接正推,下标为0xf
的元素0x5
即为所求值。故第一个参数应该为0x5
。
phase_6
先附上phase_6的反汇编:
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91Dump of assembler code for function phase_6:
0x00000000004011b4 <+0>: push r14
0x00000000004011b6 <+2>: push r13
0x00000000004011b8 <+4>: push r12
0x00000000004011ba <+6>: push rbp
0x00000000004011bb <+7>: push rbx
0x00000000004011bc <+8>: sub rsp,0x60
0x00000000004011c0 <+12>: mov rax,QWORD PTR fs:0x28
0x00000000004011c9 <+21>: mov QWORD PTR [rsp+0x58],rax
0x00000000004011ce <+26>: xor eax,eax
0x00000000004011d0 <+28>: mov rsi,rsp
0x00000000004011d3 <+31>: call 0x40152b <read_six_numbers>
0x00000000004011d8 <+36>: mov r12,rsp
0x00000000004011db <+39>: mov r13,rsp
0x00000000004011de <+42>: mov r14d,0x0
0x00000000004011e4 <+48>: mov rbp,r13
0x00000000004011e7 <+51>: mov eax,DWORD PTR [r13+0x0]
0x00000000004011eb <+55>: sub eax,0x1
0x00000000004011ee <+58>: cmp eax,0x5
0x00000000004011f1 <+61>: jbe 0x4011f8 <phase_6+68>
0x00000000004011f3 <+63>: call 0x401509 <explode_bomb>
0x00000000004011f8 <+68>: add r14d,0x1
0x00000000004011fc <+72>: cmp r14d,0x6
0x0000000000401200 <+76>: je 0x401223 <phase_6+111>
0x0000000000401202 <+78>: mov ebx,r14d
0x0000000000401205 <+81>: movsxd rax,ebx
0x0000000000401208 <+84>: mov eax,DWORD PTR [rsp+rax*4]
0x000000000040120b <+87>: cmp DWORD PTR [rbp+0x0],eax
0x000000000040120e <+90>: jne 0x401215 <phase_6+97>
0x0000000000401210 <+92>: call 0x401509 <explode_bomb>
0x0000000000401215 <+97>: add ebx,0x1
0x0000000000401218 <+100>: cmp ebx,0x5
0x000000000040121b <+103>: jle 0x401205 <phase_6+81>
0x000000000040121d <+105>: add r13,0x4
0x0000000000401221 <+109>: jmp 0x4011e4 <phase_6+48>
0x0000000000401223 <+111>: lea rcx,[rsp+0x18]
0x0000000000401228 <+116>: mov edx,0x7
0x000000000040122d <+121>: mov eax,edx
0x000000000040122f <+123>: sub eax,DWORD PTR [r12]
0x0000000000401233 <+127>: mov DWORD PTR [r12],eax
0x0000000000401237 <+131>: add r12,0x4
0x000000000040123b <+135>: cmp rcx,r12
0x000000000040123e <+138>: jne 0x40122d <phase_6+121>
0x0000000000401240 <+140>: mov esi,0x0
0x0000000000401245 <+145>: jmp 0x401261 <phase_6+173>
0x0000000000401247 <+147>: mov rdx,QWORD PTR [rdx+0x8]
0x000000000040124b <+151>: add eax,0x1
0x000000000040124e <+154>: cmp eax,ecx
0x0000000000401250 <+156>: jne 0x401247 <phase_6+147>
0x0000000000401252 <+158>: mov QWORD PTR [rsp+rsi*2+0x20],rdx
0x0000000000401257 <+163>: add rsi,0x4
0x000000000040125b <+167>: cmp rsi,0x18
0x000000000040125f <+171>: je 0x401275 <phase_6+193>
0x0000000000401261 <+173>: mov ecx,DWORD PTR [rsp+rsi*1]
0x0000000000401264 <+176>: mov eax,0x1
0x0000000000401269 <+181>: mov edx,0x6042f0
0x000000000040126e <+186>: cmp ecx,0x1
0x0000000000401271 <+189>: jg 0x401247 <phase_6+147>
0x0000000000401273 <+191>: jmp 0x401252 <phase_6+158>
0x0000000000401275 <+193>: mov rbx,QWORD PTR [rsp+0x20]
0x000000000040127a <+198>: lea rax,[rsp+0x20]
0x000000000040127f <+203>: lea rsi,[rsp+0x48]
0x0000000000401284 <+208>: mov rcx,rbx
0x0000000000401287 <+211>: mov rdx,QWORD PTR [rax+0x8]
0x000000000040128b <+215>: mov QWORD PTR [rcx+0x8],rdx
0x000000000040128f <+219>: add rax,0x8
0x0000000000401293 <+223>: mov rcx,rdx
0x0000000000401296 <+226>: cmp rsi,rax
0x0000000000401299 <+229>: jne 0x401287 <phase_6+211>
0x000000000040129b <+231>: mov QWORD PTR [rdx+0x8],0x0
0x00000000004012a3 <+239>: mov ebp,0x5
0x00000000004012a8 <+244>: mov rax,QWORD PTR [rbx+0x8]
0x00000000004012ac <+248>: mov eax,DWORD PTR [rax]
0x00000000004012ae <+250>: cmp DWORD PTR [rbx],eax
0x00000000004012b0 <+252>: jge 0x4012b7 <phase_6+259>
0x00000000004012b2 <+254>: call 0x401509 <explode_bomb>
0x00000000004012b7 <+259>: mov rbx,QWORD PTR [rbx+0x8]
0x00000000004012bb <+263>: sub ebp,0x1
0x00000000004012be <+266>: jne 0x4012a8 <phase_6+244>
0x00000000004012c0 <+268>: mov rax,QWORD PTR [rsp+0x58]
0x00000000004012c5 <+273>: xor rax,QWORD PTR fs:0x28
0x00000000004012ce <+282>: je 0x4012d5 <phase_6+289>
0x00000000004012d0 <+284>: call 0x400b00 <__stack_chk_fail@plt>
0x00000000004012d5 <+289>: add rsp,0x60
0x00000000004012d9 <+293>: pop rbx
0x00000000004012da <+294>: pop rbp
0x00000000004012db <+295>: pop r12
0x00000000004012dd <+297>: pop r13
0x00000000004012df <+299>: pop r14
0x00000000004012e1 <+301>: ret
End of assembler dump.作为最后一个phase,上来就给了个下马威,这反汇编在6个phase中最长,逻辑也是最复杂的。看一下反汇编,注意到phase_6调用了
read_six_numbers
函数,所以输入格式是和phase_2相同的六个数字。这里输入1 2 3 4 5 6
继续分析。直接看不容易看出程序的规律,那就从
read_six_numbers
函数下一行开始一步步运行:上面这第一段代码主要判断第一个参数是否小于6,再往下执行:
从这段可基本判断现在位于一个循环中,
r14d
中存循环变量,且这个循环要执行6次,继续向下执行:这段将当前第
ebx
个参数即第二个参数赋予eax
,并判断和第一个参数是否不相等,是则继续,接下来似乎是这个循环的最后一段:出现了另一个循环变量
ebx
,且其控制的循环要执行5次。再往下,程序跳转到了
0x401205 <phase_6+81> movsxd rax, ebx
,从之前的分析看,这一段是判断第ebx
个参数是否和第一个参数不相等,所以ebx
控制的循环功能是判断6个参数中其它5个参数是否和第一个参数相等。继续运行到
ebx
控制的循环结束:这里对
r13
加4后直接一个大跳转,继续跟进,发现继续执行的是第二步分析的判断参数是否小于6,只不过这次判断的是第二个参数。分析到这,基本可以确定,以上这一整段是个双重循环,用于判断输入的六个数是不是为1~6,且互不相等。检验完输入后就快进入关键逻辑了,循环结束继续向下执行:
这又是一个小循环,但比起之前那个要简单得多。其功能是对每个参数x进行处理,使每个参数x都变为7-x,比如第一个参数为1,处理后变成6。
继续向下执行,程序跳转到了
0x401261 <phase_6+173> mov ecx, dword ptr [rsp + rsi]
:这里开始又是一个循环,注意到出现了
node1 <0x6042f0>
,不防看一眼里面都有什么,执行x/25wx 0x6042f0
:很明显这是一个单链表,
node1
指向node2
,node2
指向node3
,依次类推。继续向下执行:令当前第x个参数为a(x),x为1~6。结合上一个片段看,可以判断这一段是遍历到第a(1)个节点,将其地址存入
rsp + rsi*2 + 0x20
位置,最后更改循环变量rsi
,判断是否要退出循环。上一步在
rsi
控制下一共执行了六次,功能是将第a(x)个节点放入rsp + x*4*2 + 0x20
位置,结果如下图:继续向下执行,又是一个小循环:
令节点n的下一节点指针为flink(n)。这个小循环的功能为将节点a(x)的flink(a(x))置为节点a(x+1)的地址。比如当前参数为
6 5 4 3 2 1
,则循环处理后六个节点就会变成node6->node5->node4->node3->node2->node1
。退出循环后继续向下执行:
这段先将最末节点的flink置0,然后比较链表中第一个节点的值和第二个节点的值,如果前者大于后者则跳转:
这里让
rbx
指向下一个节点,然后循环变量ebp
减1,继续上一步,即比较rbx
指向的当前节点和下一节点的值,前者大于后者则跳转,否则BOOM!上一步循环顺利退出即可过关。前提是当前链表中前一个节点中的数据始终大于后一个节点的数据,根据第六步中的节点信息,可以知道节点最后的顺序应该为
node2->node5->node4->node3->node6->node1
。所以经第五步处理后的参数应该为2 5 4 3 6 1
。即最初输入的参数应该为5 2 3 4 1 6
。
总结
总的来说,这个作业并不算难,程序用到的都是最基本的逻辑和计算。但是想要顺利的解出答案,除了要对GDB的操作和命令熟练外,还要对程序的基本运行原理和相关汇编指令有一定程度的掌握。
我认为这六个Phase中唯一棘手的就是最后一个了,几乎全是循环逻辑,甚至是双重循环,对这种逻辑的处理没太多经验的话很容易望而却步,哪怕是硬着头皮上,也会身心俱疲。但总之,程序的运行逻辑无非就这几种:顺序、分支、循环。基本逻辑搞定了,分析解决再大的程序也不过是时间问题。
当然,每个问题通常都不会只有一种解法,比如phase_4就可以配合暴力枚举找出答案,节省大量时间。灵活的运用工具,善于思考,尝试用不同的角度去审视和解决难题,能够让我们思维更加开阔。
这篇博客算是我第一篇技术长篇,或许还有许多表述不周的地方。欢迎在下方评论,我们一起学习进步,努力变强!