最近终于把鸽了近一年多的 BinaryBomb 给做完了,花了一天时间,内力修炼的还不够啊。这里记录一下解题思路,方便以后回顾,同时也疏理一下解题用到的技术点。
Note: 本文主要记录解题的过程和思路,相关的基础知识可在 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 个字符串应该是什么,从而“拆除”他们自己独特的“炸弹”。这个实验旨在帮助学生理解汇编语言,并迫使他们学习如何使用调试器。它也充满乐趣,是卡内基梅隆大学本科生中一个传奇的实验。
正如介绍所说,BinaryBomb 非常有趣,虽然这作业是 2013 年发布的,但时至今日,仍能从中学到许多有用的东西。毕竟我们现在用的计算机除了从 32 位变成了 64 位外,寄存器种类,栈结构,函数调用机制等程序执行原理基本没有变化。
gdb + pwndbg 简介
gdb 即 GNU Debugger,是 GNU 软件系统中的标准调试器 ,此外 gdb 也是个具有移携性的调试器,经过移携需求的调修与重新编译,如今许多的类 UNIX 操作系统上都可以使用 gdb,而现有 gdb 所能支持调试的编程语言有 C、C++、Pascal 以及 FORTRAN。
pwndbg 是一个 GDB 和 LLDB 插件,可以减少调试工作,重点关注低级软件开发人员、硬件黑客、逆向工程和漏洞开发人员所需的功能。
简单来说,本文使用 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 运行 apt install gdb 进行安装。如果是 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,从当前开始继续运行
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 尝试以字符串格式输出该地址信息:
好家伙,看来这个就是答案了。 4. 但假如没有经验,不知道应该看地址 0x402490 的信息呢?那只能一步步运行看看了,连续执行 si 直到 strings_not_equal 函数,可以注意到下面这一段:
很明显,参与比较的两个字符串分别是 rdi 指向的 helloworld 和 rsi 指向的 Why make trillions when we could make... billions?,前者是之前随便输的,后者是程序里写好的。 5. 后面的几句汇编是判断 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 33 Dump 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 77 Dump 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 30 Dump 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 24 Dump 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; __int64 result; 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 39 Dump 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 91 Dump 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 个参数是否和第 1 个参数相等。
继续运行到 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 的操作和命令熟练外,还要对程序的基本运行原理和相关汇编指令有一定程度的掌握。
我认为这 6 个 Phase 中唯一棘手的是最后一个,几乎全是循环逻辑,甚至是双重循环,对这种逻辑的处理没太多经验的话很容易望而却步,哪怕是硬着头皮上,也会身心俱疲。但总之,程序的运行逻辑无非几种:顺序、分支、循环。基本逻辑搞定了,分析解决再复杂的逻辑也不过是时间问题。
当然,问题通常都不会只有一种解法,比如 phase_4 可以还能配合暴力枚举找出答案,节省大量时间。灵活运用工具,善于思考,尝试用不同的角度去审视难题,可以让思维更加开阔。
该篇是本博客第一个技术长篇,或许还有许多表述不周的地方,欢迎在下方评论,我们一起,变得更强!