【写在前面】
这是一个仍然需要修改和更新的 CSAPP : BombLab 的解题教程,本篇大多数情况下都在分析汇编代码,操作部分讲解的不多,我会在后期更新简化的操作教程。这是一篇本人在学习 IA-32 汇编指令并完成学校实验过程中一点浅薄的见解,现在将其整理出来与君分享。学识尚浅,高手勿喷。(2023.12.5)
2023.12.31 更新了隐藏关卡的破解过程,并修正了一些文字上的小错误。
一、基本要求
- 基本的 Linux 命令行使用,一些基本指令
- 基本的汇编语言阅读能力
- 基本的 gdb调试指令,本文会把使用到的 gdb 命令列出。
二、题目要求
运行一个二进制文件 bomb,它包括六个"阶段(phase)",每个阶段要求学生通过 stdin 输入一个特定的字符串。如果输入了预期的字符串,那么该阶段被"拆除",进入下一个阶段,直到所有炸弹被成功"拆除"。否则,炸弹就会"爆炸",打印出"BOOM!!!",并通知教师端。
三、拆弹方法
1.使用 objdump 对炸弹文件进行反汇编;
2.理解汇编语言代码的行为或作用;
3.使用 gdb 调试器单步跟踪调试每一阶段的机器代码,找出拆除炸弹所需的字符串;
4.提交结果,撰写实验报告。
四、配置拆弹环境
4.1 安装 VMWare
下面的链接共有 3 个不同版本的 VMWare,根据自己的操作系统选择合适的版本:
链接:https://pan.baidu.com/s/1f6MXhyiMqGYpezjZQhSeAA
提取码:bdo6
PS:V16.0.0 只支持 Win10 64 位;Win7 或配置较低的电脑,建议用 v12.5.9 老版本;XP 或 32 位系统只能用 v10.0.7 经典版。
(注意: Win 11 请安装最新版,到官网下载,激活方法网上找激活码啥的,自己查阅资料)
4.2 安装 Linux 虚拟机
(1)下载 Linux 镜像文件,一定要确保是 32 位 Linux。Linux 推荐使用 Ubuntu 16.04 或 14.04 版本。官方下载地址:
16.04.6:Ubuntu 16.04.7 LTS (Xenial Xerus)
14.04.6:Ubuntu 14.04.6 LTS (Trusty Tahr)
注:Linux iso文件名上带有 i386 字样的为 32 位 Linux,带有 amd64 字样的为 64 位 Linux。
(2)在 VMware 下,点击 文件-新建虚拟机,安装 Linux 虚拟机,各种设置按默认即可。
确保已安装 VMWare Tools,否则无法设置共享文件夹。如果已安装 VMWare Tools,则不需要重新安装。安装方法参照《实验室电脑配置说明》。如果 VMWare 下的 虚拟机-安装 VMWare Tools 按钮是灰色的,导致无法安装,则转第 3 条进行处理;VMWware Tools 安装完成之后,转第5条。
4.3 VMWare Tools 按钮灰显
(1)点击虚拟机-设置-CD/DVD(SATA),选中“使用ISO影像文件”,同时选中“已连接”和“启动时连接”。
(2)点击浏览,选择 VMWare 安装目录下的 linux.iso 文件,如:C:\Program Files (x86)\VMware\ VMware Workstation\linux.iso,点击打开,如下图所示:
(3)此时应自动展开 VMWare Tools 虚拟光驱目录,或出现下面的 DVD 图标,用鼠标点击该图标。就可以安装 VMWare Tools 了,具体的安装步骤请自己查阅资料(限于篇幅)。
(4)此时应自动展开 VMWare Tools 虚拟光驱目录,或出现下面的 DVD 图标,用鼠标点击该图标。就可以安装 VMWare Tools 了。
4.4 设置共享文件夹的方法
在 VMware 下,点击 虚拟机-设置-选项-共享文件夹-总是启用-添加-浏览-选择 D:\share(在 Windows 下提前建好此文件夹)-下一步-完成。
如果共享文件夹设置好之后,在/mnt/hgfs目录下没有 share 目录,则重新安装 VMWare Tools,一般可解决问题。
在做实验二时,要登录 ***,能够访问学校内网,才能连接服务器。如果登录了 ***,仍然不能连接服务器,可以在获得拆弹密码之后,用其他同学的电脑把结果提交到服务器。(我们作业是在线版本的,如果个人完成,则不需要配置网络)
在做实验二时,要确保自己的电脑主机已联网,并将虚拟机设置为“NAT模式”进行联网,否则可能导致无法连接服务器。设置方法:在 VMWare 的 虚拟机-设置-网络适配器 中,选择“NAT模式”,如下图所示。
4.5 独立下载炸弹
如果读者不是通过学校课程作业获取到的炸弹文件,那么请按照以下教程自行下载离线版炸弹文件。
使用 wget 下载炸弹文件,需要先 cd 到你想存放炸弹的文件夹,然后使用下面的指令下载文件:
wget csapp.cs.cmu.edu/3e/bomb.tar
通过下面的指令可以解压文件:
tar xvf bomb.tar
这会生成一个 bomb 目录,包含三个文件:bomb, bomb.c, README
五、了解 GDB 常用指令
5.1 变量
查看变量信息:
(gdb) p
5.2 寄存器
对于调试来说寄存器中的值也很重要,可以查看到当前正在执行的指令的地址等。具体操作如下:
(gdb) info reg:显示所有寄存器。可以简写为:i r。
如果要查看具体的寄存器可以这样:i $ebx
(gdb) p $eax:显示eax寄存器内容
(gdb) p/c $eax:用字符显示 eax 寄存器内容
反斜杠后面的是显示格式,可使用的格式见下表:该表在显示内存内容的 x 命令中也是通用的
格式 说明 x 显示为十六进制数 d 显示为十进制数 u 显示为无符号十六进制数 o 显示为八进制数 t 显示为二进制数 a 显示为地址 c 显示为字符(ASCII) f 显示为浮点数 s 显示为字符串 i 显示为汇编代码(仅在显示内存的x命令中可用) 5.3 栈
查看栈信息:
(gdb) bt:显示所有栈帧
(gdb) bt 10:显示前面10个栈帧
(gdb) bt -10:显示后面10个栈帧
(gdb) bt full:显示栈帧以及局部变量
(gdb) bt full 10:显示前面10个栈帧以及局部变量
(gdb) bt full -10:显示后面10个栈帧以及局部变量
(gdb) frame :进入指定的栈帧中,然后可以查看当前栈帧中的局部变量,以及栈帧内容等信息
(gdb) info frame :可以查看指定栈帧的详细信息
(gdb) up:进入上层栈帧
(gdb) down:进入下层栈帧
5.4 内存
可以查看具体内存地址中的内容,比如:目前执行的汇编指令,以及栈中的内容等。
(gdb) x $pc:显示程序指针指向位置的内容
(gdb) x/i $pc:显示程序当前位置的汇编指令
(gdb) x/10i $pc:显示程序当前位置开始往后的10条汇编指令
5.5 设置断点
(gdb) break :对当前正在执行的文件中的指定函数设置断点。可简写为:(gdb) b
(gdb) break :对当前正在执行的文件中的特定行设置断点。可简写为:(gdb) b
(gdb) break :对指定文件的指定行设置断点。最常用的设置断点方式。可简写为:(gdb) b
(gdb) break :对指定文件的指定函数设置断点。C++类中的方法似乎不好使。可简写为:(gdb) b
(gdb) break :当前指令行+/-偏移量出设置断点。可简写为:b
(gdb) break :指定地址处设置断点。可简写为:b
5.6 查看/删除断点
(gdb) info break:显示所有断点以及监视点。可简写为:(gdb) i b
(gdb) delete :删除编号指向的断电或者监视点。可简写为:(gdb) d
(gdb) clear :删除该行的断点
(gdb) clear :删除该行的断点
5.7 设置无效、有效断点
(gdb) disble :当前断点设置为无效
(gdb) enable:当前断点设置为有效
5.8 监视点
可以监视某个变量,在变量被访问或者被修改时程序会在当前点进入断点。删除,查看监视点的方式与断点相同。设置监视点方式如下:
(gdb) watch :表达式发生变化时暂停
(gdb) awatch :表达式访问或者改变时暂停
(gdb) rwatch :表达式被访问时暂停
5.9 条件断点
在调试程序过程中,有时候我们只想在某个条件下停止程序,然后进行单步调试,而条件断点就是为此而设计。下面是条件断点的操作方式:
(gdb) b if : 例如:b main.cpp:8 if x=10 && y=10
(gdb) condition :删除该断点的条件。
(gdb) condition :修改断点条件。例如:condition 1 x=10 && y=10
6.0 断点命令
每次断点发生时候,想要查看的变量很多时,如果每个变量都手动 print 需要浪费很多时间。断点命令可以在断点发生时批量执行GDB命令。下面是断点命令的设置方式:
(gdb) commands
(gdb) >print x
(gdb) >print y
(gdb) >end
首先输入 GDB 命令 commands 然后回车,这时候会出现> 提示符。出现> 提示符后可以输入断点发生时需要执行的GDB命令,每行一条,全部输入完成后输入end结束断点命令。
6.1 设置变量值
对变量的值进行控制,可以更快的调试自己的程序。下面就是设置变量值的方法:
(gdb) set variable = :将变量的值设定为指定表达式的值。例如 set variable x=10
六、分析各个关卡
前置:
首先我们注意到我们的 jar 解压后一共有三个文件,一个是 bomb.c 还有一个就是炸弹程序 bomb,以及不是很重要的 README 文件。 bomb.c 源码文件是 main 函数的源代码,这有助于我们调试,但是我想先对里面的一些英文注释做翻译:
/********************************85/2000000000000000000******************************************* * Dr. Evil's Insidious Bomb, Version 1.1 * Copyright 2011, Dr. Evil Incorporated. All rights reserved. * * LICENSE: * * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the * VICTIM) explicit permission to use this bomb (the BOMB). This is a * time limited license, which expires on the death of the VICTIM. * The PERPETRATOR takes no responsibility for damage, frustration, * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other * harm to the VICTIM. Unless the PERPETRATOR wants to take credit, * that is. The VICTIM may not distribute this bomb source code to * any enemies of the PERPETRATOR. No VICTIM may debug, * reverse-engineer, run "strings" on, decompile, decrypt, or use any * other technique to gain knowledge of and defuse the BOMB. BOMB * proof clothing may not be worn when handling this program. The * PERPETRATOR will not apologize for the PERPETRATOR's poor sense of * humor. This license is null and void where the BOMB is prohibited * by law. * // 中文版: * 邪恶博士的阴险炸弹,1.1 版 * 版权所有 2011,邪恶集结者博士。保留所有权利。 * 许可证: * 邪恶博士公司(犯罪者)特此明确允许您(受害者)使用此炸弹。 * 这是一个有时间限制的许可证,在受害者死亡时到期。犯罪者对受害者的伤害 * 、沮丧、精神错乱、虫眼、腕管综合征、睡眠不足或其他伤害不承担任何责任。 * 除非犯罪者想获得荣誉,否则受害者不得将此炸弹源代码分发给犯罪者的任何敌人。 * 任何受害者都不得调试、反向工程、运行“字符串”、反编译、解密或使用 * 任何其他技术来获取和拆除炸弹。处理此程序时可能不穿防炸弹的衣服。 * 犯罪者不会为犯罪者缺乏幽默感而道歉。 * 在法律禁止使用炸弹的情况下,此许可证无效。 ***************************************************************************/ #include #include #include "support.h" #include "phases.h" /* * Note to self: Remember to erase this file so my victims will have no * idea what is going on, and so they will all blow up in a * spectaculary fiendish explosion. -- Dr. Evil */ // 自我提醒:记住要删除这个文件,这样我的受害者就不知道发生了什么, // 所以他们都会在一场可怕的爆炸中爆炸。 —— 邪恶博士 FILE *infile; int main(int argc, char *argv[]) { char *input; /* Note to self: remember to port this bomb to Windows and put a * fantastic GUI on it. */ // 自我提醒:记得把这个炸弹移植到Windows上,并在上面制作一个很棒的GUI。 /* When run with no arguments, the bomb reads its input lines * from standard input. */ // 关于文件读取:当在没有参数的情况下运行时,炸弹会从标准输入中读取其输入行。 // 译者注:这里是指如果程序没有指定文件参数,则通过标准输入来读取一行字符串 if (argc == 1) { infile = stdin; } /* When run with one argument , the bomb reads from * until EOF, and then switches to standard input. Thus, as you * defuse each phase, you can add its defusing string to and * avoid having to retype it. */ // 当使用一个参数<file>运行时,炸弹从<file>读取直到EOF,然后切换到标准输入。 // 因此,在分解每个阶段时,可以将其分解字符串添加到<file>中,从而避免重新键入它。 // 译者注:意思就是前面通过的关卡,可以通过把结果写入到文件,来避免重复输入。 else if (argc == 2) { if (!(infile = fopen(argv[1], "r"))) { printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]); exit(8); } } /* You can't call the bomb with more than 1 command line argument. */ // 你不能用一个以上的命令行参数调用炸弹。只能最多指定一个文件路径作为参数 else { printf("Usage: %s []\n", argv[0]); exit(8); } /* Do all sorts of secret stuff that makes the bomb harder to defuse. */ // 做各种秘密的事情,让炸弹更难拆除。 // (作者指混淆,汇编陷阱欺骗反汇编程序,让代码更加难读???) // 译者注:初始化环境,可能是加分点。需分析后决定功能。 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! */ // 六个阶段必须比一个阶段更安全!(暗示至少有6个阶段) 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. */ /* Drat!他们想通了! 让我知道他们是怎么做到的。*/ printf("Phase 1 defused. How about the next one?\n"); /* The second phase is harder. No one will ever figure out * how to defuse this... */ // 第二阶段更难。没有人会想到办法化解这个(难题)。。。 input = read_line(); phase_2(input); phase_defused(); printf("That's number 2. Keep going!\n"); /* I guess this is too easy so far. Some more complex code will * confuse people. */ // 到目前为止,我想这太容易了。一些更复杂的代码会让人感到困惑。 input = read_line(); phase_3(input); phase_defused(); printf("Halfway there!\n");*- /* Oh yeah? Well, how good is your math? Try on this saucy problem! */ // 哦,是吗?你的数学有多好?试试这个恶心的题目! input = read_line(); phase_4(input); phase_defused(); printf("So you got that one. Try this one.\n"); /* Round and 'round in memory we go, where we stop, the bomb blows! */ // 在记忆中,我们一圈又一圈地走,停在哪里,炸弹就爆炸!(循环?) input = read_line(); phase_5(input); phase_defused(); printf("Good work! On to the next...\n"); /* This phase will never be used, since no one will get past the * earlier ones. But just in case, make this one extra hard. */ // 这一阶段将永远不会被使用,因为没有人会通过之前的阶段。 // 但以防万一,我让这件事变得格外艰难。 input = read_line(); phase_6(input); phase_defused(); /* Wow, they got it! But isn't something... missing? Perhaps * something they overlooked? Mua ha ha ha ha! */ // 哇,他们成功了!但是,遗失的。。。? // 也许他们忽略了什么?哈————哈,哈! return 0; }
注释几乎暗含了每个关卡的方向。下面就是破解和分析时间了(以下内容是干货(错误)比较多的,内容这是从我实验报告上几乎原封不动复制过来的,高手勿喷。准备寒假完善隐藏关卡的解释,并写一个单纯的攻略操作版本,有空再研究一下 x64 的炸弹)
6.1 phase_1
6.1.1 相关汇编代码
08048b33 :
8048b33: 83 ec 14 sub $0x14,%esp
8048b36: 68 84 a1 04 08 push $0x804a184
8048b3b: ff 74 24 1c pushl 0x1c(%esp)
8048b3f: e8 ba 04 00 00 call 8048ffe
8048b44: 83 c4 10 add $0x10,%esp
8048b47: 85 c0 test %eax,%eax
8048b49: 74 05 je 8048b50
8048b4b: e8 eb 06 00 00 call 804923b
8048b50: 83 c4 0c add $0xc,%esp
8048b53: c3 ret
6.1.2 破解过程与结果
在 函数中:
1.
sub $0x14,%esp
将栈顶指针减去0x14字节来为局部变量和临时数据腾出空间。
2.
push $0x804a184
将绝对地址 0x804a184 处获数据并压栈,根据上下文猜测是在准备下面 strings_not_equal 函数的参数。
3.
pushl 0x1c(%esp)
从栈上偏移量 0x1c 处获取一个32位值,然后将其压入栈中。
4.
call 8048ffe
调用 strings_not_equal 函数来解析输入数据。
再看 main 函数中调用 phase_1 函数的上下文:
8048a79: e8 35 08 00 00 call 80492b3
8048a7e: 89 04 24 mov %eax,(%esp)
8048a81: e8 ad 00 00 00 call 8048b33
8048a86: e8 21 09 00 00 call 80493ac
这里 ` mov %eax,(%esp)` 将寄存器 %eax 的值移动(存储)到栈顶 (sp)。也就是读入了一行字符串,然后将其首地址从 eax(read_line返回值)存储到 esp 。
于是,我们可以知道 地址 0x804a184 处的字符串用于与输入字符串进行比较,继续分析phase_1的下文:
5.
test %eax,%eax
这条指令测试 %eax 寄存器中的值。它将 %eax 与自身进行按位逻辑与(AND)运算,并根据结果设置标志寄存器(FLAGS)。
6.
je 8048b50
如果上一条指令产生的结果为零(也就是 %eax 的值为零),则跳转到地址 0x8048b50 处执行,否则继续执行下一条指令。而下一条指令是 `call 804923b ` 该函数使得炸弹爆炸,拆弹失败。所以当两条字符串相等时,strings_not_equal 返回值是 0,炸弹拆除成功;否则为非零值,炸弹拆除失败。
于是,只需利用 gdb 的 x/s 指令查看 0x804a184 位置对应内存存的字符串即可:
也可以使用 readelf 的readelf -x .rodata bomb 列出整个 rodata 节的数据:
重新运行一个实例,并输入 I was trying to give Tina Fey more material. 可以发现成功通关:
6.2 phase_2
6.2.1 相关汇编代码
08048b54 :
8048b54: 53 push %ebx
8048b55: 83 ec 30 sub $0x30,%esp
8048b58: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048b5e: 89 44 24 24 mov %eax,0x24(%esp)
8048b62: 31 c0 xor %eax,%eax
8048b64: 8d 44 24 0c lea 0xc(%esp),%eax
8048b68: 50 push %eax
8048b69: ff 74 24 3c pushl 0x3c(%esp)
8048b6d: e8 06 07 00 00 call 8049278
8048b72: 83 c4 10 add $0x10,%esp
8048b75: 83 7c 24 04 00 cmpl $0x0,0x4(%esp)
8048b7a: 79 05 jns 8048b81
8048b7c: e8 ba 06 00 00 call 804923b
8048b81: bb 01 00 00 00 mov $0x1,%ebx
8048b86: 89 d8 mov %ebx,%eax
8048b88: 03 04 9c add (%esp,%ebx,4),%eax
8048b8b: 39 44 9c 04 cmp %eax,0x4(%esp,%ebx,4)
8048b8f: 74 05 je 8048b96
8048b91: e8 a5 06 00 00 call 804923b
8048b96: 83 c3 01 add $0x1,%ebx
8048b99: 83 fb 06 cmp $0x6,%ebx
8048b9c: 75 e8 jne 8048b86
8048b9e: 8b 44 24 1c mov 0x1c(%esp),%eax
8048ba2: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048ba9: 74 05 je 8048bb0
8048bab: e8 e0 fb ff ff call 8048790
8048bb0: 83 c4 28 add $0x28,%esp
8048bb3: 5b pop %ebx
8048bb4: c3 ret
08049278 :
8049278: 83 ec 0c sub $0xc,%esp
804927b: 8b 44 24 14 mov 0x14(%esp),%eax
804927f: 8d 50 14 lea 0x14(%eax),%edx
8049282: 52 push %edx
8049283: 8d 50 10 lea 0x10(%eax),%edx
8049286: 52 push %edx
8049287: 8d 50 0c lea 0xc(%eax),%edx
804928a: 52 push %edx
804928b: 8d 50 08 lea 0x8(%eax),%edx
804928e: 52 push %edx
804928f: 8d 50 04 lea 0x4(%eax),%edx
8049292: 52 push %edx
8049293: 50 push %eax
8049294: 68 05 a4 04 08 push $0x804a405
8049299: ff 74 24 2c pushl 0x2c(%esp)
804929d: e8 6e f5 ff ff call 8048810
80492a2: 83 c4 20 add $0x20,%esp
80492a5: 83 f8 05 cmp $0x5,%eax
80492a8: 7f 05 jg 80492af
80492aa: e8 8c ff ff ff call 804923b
80492af: 83 c4 0c add $0xc,%esp
80492b2: c3 ret
6.2.2 破解过程与结果
【逐行分析】
1.
push %ebx
将 %ebx 寄存器的值推入栈中,保存现场, esp1 = esp0 - 4。
2.
sub $0x30,%esp
在栈上为局部变量和缓冲区开辟空间。 esp2 = esp1 – 48 = esp0 - 52
3.
mov %gs:0x14,%eax
mov %eax,0x24(%esp).
获取操作系统提供的安全机制(通常称为"canary")并放置到栈上以防止缓冲区溢出攻击。根据上下文计算esp0 - 4 - 0x30(48) + 0x24(36) = esp0 – 16,是将gs:0x14对应地址放到了esp0 - 16 处。
4.
xor %eax,%eax
清空 eax 寄存器的值。
5.
lea 0xc(%esp),%eax
push %eax
将 %eax 设置为栈顶地址加上偏移量 12(即 %esp + 0xC 的地址),即esp0 – 40 处作的值压栈,先将将栈顶指针 – 4,即esp3 = esp0 – 56, 然后将值压入到新的栈顶esp0-56处作为后续函数调用的参数。
6.
pushl 0x3c(%esp)
将当前 esp 加上偏移量 0x3c 即 esp0 – 56 + 60 = esp0 + 4 处的值,作为调用 read_six_numbers 函数的参数,由于调用了 push, 这个参数是放在现在的esp4 = esp0 – 60 处。
7.
call 8049278
将前面计算得到的esp0-52处参数2(数组首地址)和esp0 + 4 的栈上数据(源字符串首地址)作为参数1,用这两个参数来调用函数 read_six_numbers。
重难点:观察 mian 函数调用 phase_2 的上下文:
8048a97: e8 17 08 00 00 call 80492b3
8048a9c: 89 04 24 mov %eax,(%esp)
8048a9f: e8 b0 00 00 00 call 8048b54
8048aa4: e8 03 09 00 00 call 80493ac
在调用 phase_2 函数之前,先调用了 read_line 函数,字面理解就是读取了一行输入,返回值是字符串首地址,存放在 eax 寄存器中,然后调用 mov %eax,(%esp) 将 eax 中的地址放到当前的栈顶,即esp 处。可以发现 传递给 phase_2 的参数在传递给 read_six_numbers 时,上面分析的是 esp0 + 4 处的值,为什么两者看似“不等”呢?
【分析】
因为调用 call phase_2指令时,call 指令实际上做了两件事:将PC+偏移量计算得到的地址压入栈顶,并将 esp – 4, 所以我们进入 phase_2 的栈帧时 esp0 = esp – 4。
综上,read_six_numbers 的第一个参数其实就是 main 函数传递给 phase_2 的唯一参数,而不是其他偏移处的值。
8.
add $0x10,%esp
恢复堆栈指针,这里是恢复调用 read_six_numbers 前为其分配参数所占用的空间,将栈帧移动到数组第一个元素地址(esp0-40)的顶部,也就是说,现在的esp5 = esp4 + 16 = esp0 - 44。
9.
cmpl $0x0,0x4(%esp)
jns 8048b81
0x4(%esp),即 esp5 + 4 = esp0 – 44 + 4 = esp0 - 40,根据前面分析,这个值是read_six_numbers参数格式化的第一个数的地址,即6个数的数组的首地址。这两条指令检查第一个输入数值是否大于等于零。如果是负数,则会向下执行,引爆炸弹。
【小结】:需要确保第一个输入数字不为负数。可以通过输入非负整数来绕过这个检查。
10.
call 804923b
此行代码对应一种异常情况,即第一个输入数字为负数,会导致炸弹爆炸。
11.
mov $0x1,%ebx
将寄存器 %ebx 设置为 1,用作循环计数器,即for循环的i 。
11.
mov %ebx,%ea
将 %ebx 的值移动到 %eax 寄存器中,即 %eax = %ebx = 1,用于后续计算。
12.
add (%esp,%ebx,4),%eax
将栈上地址为 (%esp + 4 * %ebx) 处的数值加到 %eax 中。这是一个累加操作, 而eax 用于存放ebx(索引 i)和a[i-1]的和,第一轮是eax = i = 1,经过这个操作后理解为 eax = 1 + a[i - 1]。由于esp5 + 4 = esp0 - 40 是数组的首地址,ebx 作为 i 并初始化为1, (%esp,%ebx,4)就是指 %esp + 4 + (%ebx - 1) * 4,在遍历过程中可以理解为这行指令是依次取数组中元素的意思,即 a[i - 1]且 i=1:n。现在 eax 中的值就是取出的 a[i-1]。
13.
cmp %eax,0x4(%esp,%ebx,4)
将 %eax 和栈上另一个地址 (%esp + 4 * %ebx + 4) 处的数值比较, (%esp + 4 * %ebx + 4)比(%esp,%ebx,4)多加一个4,所以对于每一次循环,这个值代表 a[i],这行指令意思是把eax和a[i]进行比较。
14.
je 8048b96
如果相等,则跳转到 8048b96 处执行下一条指令。而8048b96 处的指令为 add $0x1,%ebx,将 %ebx 加1,增加循环计数器的值。
15.
call 804923b
此行代码对应一种异常情况,即累加结果与所比较的数不相等,会引爆炸弹。
16.
cmp $0x6,%ebx
jne 8048b86
如果 %ebx 不等于 6,则跳转到 8048b86 处,我们再看一下调转的位置:
8048b86: mov %ebx,%eax
8048b88: add (%esp,%ebx,4),%eax
8048b8b: cmp %eax,0x4(%esp,%ebx,4)
可以看出,这里是将之前累加过1的新的ebx 赋值给eax,再看eax 其实每次循环 eax 都会变成当前的索引 i 所以,我们进一步理解了此处的循环到底在做什么,即:每次循环比较 a[i] 和 a[i-1] + i 是否相等,如果不等,就会爆炸。
17.
mov 0x1c(%esp),%eax
将栈上地址为 (%esp + 0x1C) = esp5 + 28 = esp0 – 44 + 28 = esp0 - 16 的值移动到 %eax 中根据上面的分析,这里存储的是 gs:14 生成的校验码,用于下一步的检测缓冲区溢出的操作。(如果数组 a 没有发生栈溢出,则不会覆盖esp0 -16,这个位于它上方的值,如果溢出,则会覆盖掉这个值,导致下一步异或失败)。
18.
xor %gs:0x14,%eax
通过与内存中的gs:0x14值异或操作,检查是否有缓冲区溢出。本行代码是对安全机制(canary)进行验证,略过它即可继续。
19.
je 8048bb0
如果没有缓冲区溢出,则跳转至 8048bb0 进行返回前的堆栈平衡处理。
否则,发生溢出时,会继续向下执行 call 8048790 ,此行代码对应一种异常情况,即发现了堆栈错误,程序崩溃退出。
19.
8048bb0: add $0x28,%esp
恢复堆栈指针到,esp0 – 16即堆栈检测的校验码处。
20.
pop %ebx
从栈中弹出phase_2为调用者(main)维护的 %ebx 值,恢复寄存器状态。
21.
ret
返回 %eax 最后的值到调用者,也就是堆栈检测异或的结果。
根据以上分析可以尝试画出 phase_2 函数返回前的栈帧结构图:
(P.S. 由于当时实验时候时间紧的原因,栈帧理解有一个地方出错的,就是 call 压入的是返回地址,用于回到调用者,图中文本错了)
综上所述,本关卡的破解建议是输入六个非负整数,以满足每次循环 a[i-1] + 1与a[i]相等(i从1开始)。如果在执行期间触发异常条件,则会引爆炸弹,导致游戏失败。
所以,如果以 a[0] = 1,则后面的数为:2 4 7 11 16。
将结果保存至 anx.txt 并作为运行参数,通关结果如下:
6.3 phase_3
(1)相关汇编代码
08048bb5 :
8048bb5: 83 ec 1c sub $0x1c,%esp
8048bb8: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048bbe: 89 44 24 0c mov %eax,0xc(%esp)
8048bc2: 31 c0 xor %eax,%eax
8048bc4: 8d 44 24 08 lea 0x8(%esp),%eax
8048bc8: 50 push %eax
8048bc9: 8d 44 24 08 lea 0x8(%esp),%eax
8048bcd: 50 push %eax
8048bce: 68 11 a4 04 08 push $0x804a411
8048bd3: ff 74 24 2c pushl 0x2c(%esp)
8048bd7: e8 34 fc ff ff call 8048810
8048bdc: 83 c4 10 add $0x10,%esp
8048bdf: 83 f8 01 cmp $0x1,%eax
8048be2: 7f 05 jg 8048be9
8048be4: e8 52 06 00 00 call 804923b
8048be9: 83 7c 24 04 07 cmpl $0x7,0x4(%esp)
8048bee: 77 3c ja 8048c2c
8048bf0: 8b 44 24 04 mov 0x4(%esp),%eax
8048bf4: ff 24 85 e0 a1 04 08 jmp *0x804a1e0(,%eax,4)
8048bfb: b8 c0 03 00 00 mov $0x3c0,%eax
8048c00: eb 3b jmp 8048c3d
8048c02: b8 c6 02 00 00 mov $0x2c6,%eax
8048c07: eb 34 jmp 8048c3d
8048c09: b8 fd 02 00 00 mov $0x2fd,%eax
8048c0e: eb 2d jmp 8048c3d
8048c10: b8 c4 00 00 00 mov $0xc4,%eax
8048c15: eb 26 jmp 8048c3d
8048c17: b8 a6 02 00 00 mov $0x2a6,%eax
8048c1c: eb 1f jmp 8048c3d
8048c1e: b8 f9 02 00 00 mov $0x2f9,%eax
8048c23: eb 18 jmp 8048c3d
8048c25: b8 81 03 00 00 mov $0x381,%eax
8048c2a: eb 11 jmp 8048c3d
8048c2c: e8 0a 06 00 00 call 804923b
8048c31: b8 00 00 00 00 mov $0x0,%eax
8048c36: eb 05 jmp 8048c3d
8048c38: b8 a1 00 00 00 mov $0xa1,%eax
8048c3d: 3b 44 24 08 cmp 0x8(%esp),%eax
8048c41: 74 05 je 8048c48
8048c43: e8 f3 05 00 00 call 804923b
8048c48: 8b 44 24 0c mov 0xc(%esp),%eax
8048c4c: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048c53: 74 05 je 8048c5a
8048c55: e8 36 fb ff ff call 8048790
8048c5a: 83 c4 1c add $0x1c,%esp
8048c5d: c3 ret
(2)破解过程与结果
1.
sub $0x1c,%esp
栈顶指针减去 28 个字节,为局部变量和临时数据腾出空间。
2.
mov %gs:0x14,%eax
调用了内核函数,这是一种栈溢出保护手段。
3.
mov %eax,0xc(%esp)
保存 eax 旧的值,便于调用返回时恢复现场;使用 mov 指令从 %gs:0x14 处加载一个值到 %eax 寄存器中,并通过 mov 指令将其拷贝到 %esp + 0xc 的位置。
4.
xor %eax,%eax
将 %eax 寄存器与自身进行异或操作,相当于将其清零。
5.
准备 sscanf 函数的参数:
lea 0x8(%esp),%eax
将倒数第一个参数传递给 eax 寄存器。(格式化的两个数中的第二个数)
push %eax
将%eax寄存器的值压入栈中。
lea 0x8(%esp),%eax
push %eax
重复两条指令,将倒数第二个参数压入栈中。(第一个数)
push $0x804a411
压入绝对地址的字符串,gdb x/s -> "%d %d"; 进一步印证了sscanf 格式化输入两个整数
pushl 0x2c(%esp)
局部变量(位于 phase_3 入口时的 esp + 0xC 处),这个是 sscanf 的第一个参数,也就是源字符串的首地址,是 main函数调用 phase_3 时传入的参数。
6.
call 8048810
调用 sscanf 格式化输入多参数。
所以,整理一下就是:
int ret = sscanf(p1, "%d %d", x , y);
7.
add $0x10,%esp
这里实现了esp + 16。在函数调用结束时,需要恢复栈的原始状态,以确保函数参数和局部变量在栈上的正确分配与释放。从栈上移除了刚刚传递给__isoc99_sscanf 函数的参数所占据的空间。这些参数可能被压入栈上作为函数调用的参数列表,并且不再需要它们,因此使用这个指令清理栈空间。push 了4次,刚好16字节。
8.
cmp $0x1,%eax
sscanf 函数返回值和立即数 1 进行比较,sscanf 返回值表示实际格式化成功的参数个数。
jg 8048be9 ; %eax 大于 1,跳转到0x8048be9处执行,如果 %eax 小于等于 1,程序向下执行直接爆炸。结合上下文,意思是输入整数个数必须大于等于2个。
整理一下就是:
if ret > 1: goto 0x8048be9; else explode_bomb();
9.
8048be9: cmpl $0x7,0x4(%esp)
8048bee: ja 8048c2c
8048bf0: mov 0x4(%esp),%eax
8048bf4: jmp *0x804a1e0(,%eax,4)
这部分代码比较了位于栈上偏移esp + 4的值与7的大小关系,即判断第一个输入的数和7的大小关系,如果输入的数大于 7或者小于0,则跳转到地址8048c2c,引爆炸弹。注意点:为什么小于 0 的也会引爆?因为 jg是无符号运算大于的时候会跳转,负数采用补码参与运算时,始终比无符号的7要大,这就会导致直接跳转到爆炸处。
8048c2c: call 804923b
否则,将输入的第一个数复制到寄存器 %eax中,并通过间接跳转指令jmp *0x804a1e0(,%eax,4)进行跳转,跳转的位置取决于输入的参数1即现在的 eax 的值,0x804a1e0是一个存放地址值的数组的首地址。
这里我们需要借助工具知道该标签数组存放的是什么数据:
方法一:gdb:x/12w 0x804a1e0
方法二:readelf -x .rodata bomb,并定位0x804a1e0处:
明显,这是一个利用数组存放跳转地址的代码,结合课本已经学过的知识,不难猜测这是一个 switch-case 的“跳转表”,即根据变量(第一个输入的整数)的具体值,到数组中取得地址,然后进行相应的跳转。下面我们理一下跳转的具体过程:
输入的第一个数的数值
跳转后第一个指令
eax最终的值(十进制)
0
mov $0xa1,%eax
161
1
mov $0x3c0,%eax
960
2
mov $0x2c6,%eax
710
3
mov $0x2fd,%eax
765
4
mov $0xc4,%eax
196
5
mov $0x2a6,%eax
678
6
mov $0x2f9,%eax
761
7
mov $0x381,%eax
897
> 7或者
call explode_bomb
爆炸
然后我们再看一下这段代码的上下文:
明显看出这里的跳转表都是执行了 mov 指令将 eax 的值改变掉,然后都跳转到0x8048c3d 处,即下一条指令是 cmp 0x8(%esp),%eax 指令,我们知道 esp + 4 是用户输入的第一个整数,那么esp + 8 则是用户输入的第二个数,这里比较了eax 转换后的值和第二个数是否相等,比较的结果会改变标志寄存器(FLAGS),具体的操作需要看下面的条件跳转指令。
10.
je 8048c48
条件跳转指令,如果相等则跳转到0x8048c48 处,如果不相等,就会继续执行,然后执行 explode_bomb,引发爆炸。
11.
8048c48: mov 0xc(%esp),%eax
8048c4c: xor %gs:0x14,%eax
8048c53: je 8048c5a
8048c55: call 8048790
8048c5a: add $0x1c,%esp
8048c5d: ret
以上代码就是之前条件跳转的地址,这里负责检查缓冲区是否溢出,并进行堆栈平衡,最后 ret 返回函数。
综上,要想通过本关卡,我们需要输入两个整数,第一个数在 0~7之间,然后第二个数必须等于对应的eax 计算得到的值,这一关卡有多组正解,这个在上面的跳转表中已经分析过,这里以“0 161”作为第一种正确的解去尝试,发现成功通关:
6.4 phase_4
(1)相关汇编代码
08048c5e :
8048c5e: 56 push %esi
8048c5f: 53 push %ebx
8048c60: 83 ec 04 sub $0x4,%esp
8048c63: 8b 54 24 10 mov 0x10(%esp),%edx
8048c67: 8b 74 24 14 mov 0x14(%esp),%esi
8048c6b: 8b 4c 24 18 mov 0x18(%esp),%ecx
8048c6f: 89 c8 mov %ecx,%eax
8048c71: 29 f0 sub %esi,%eax
8048c73: 89 c3 mov %eax,%ebx
8048c75: c1 eb 1f shr $0x1f,%ebx
8048c78: 01 d8 add %ebx,%eax
8048c7a: d1 f8 sar %eax
8048c7c: 8d 1c 30 lea (%eax,%esi,1),%ebx
8048c7f: 39 d3 cmp %edx,%ebx
8048c81: 7e 15 jle 8048c98
8048c83: 83 ec 04 sub $0x4,%esp
8048c86: 8d 43 ff lea -0x1(%ebx),%eax
8048c89: 50 push %eax
8048c8a: 56 push %esi
8048c8b: 52 push %edx
8048c8c: e8 cd ff ff ff call 8048c5e
8048c91: 83 c4 10 add $0x10,%esp
8048c94: 01 d8 add %ebx,%eax
8048c96: eb 19 jmp 8048cb1
8048c98: 89 d8 mov %ebx,%eax
8048c9a: 39 d3 cmp %edx,%ebx
8048c9c: 7d 13 jge 8048cb1
8048c9e: 83 ec 04 sub $0x4,%esp
8048ca1: 51 push %ecx
8048ca2: 8d 43 01 lea 0x1(%ebx),%eax
8048ca5: 50 push %eax
8048ca6: 52 push %edx
8048ca7: e8 b2 ff ff ff call 8048c5e
8048cac: 83 c4 10 add $0x10,%esp
8048caf: 01 d8 add %ebx,%eax
8048cb1: 83 c4 04 add $0x4,%esp
8048cb4: 5b pop %ebx
8048cb5: 5e pop %esi
8048cb6: c3 ret
08048cb7 :
8048cb7: 83 ec 1c sub $0x1c,%esp
8048cba: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048cc0: 89 44 24 0c mov %eax,0xc(%esp)
8048cc4: 31 c0 xor %eax,%eax
8048cc6: 8d 44 24 08 lea 0x8(%esp),%eax
8048cca: 50 push %eax
8048ccb: 8d 44 24 08 lea 0x8(%esp),%eax
8048ccf: 50 push %eax
8048cd0: 68 11 a4 04 08 push $0x804a411
8048cd5: ff 74 24 2c pushl 0x2c(%esp)
8048cd9: e8 32 fb ff ff call 8048810
8048cde: 83 c4 10 add $0x10,%esp
8048ce1: 83 f8 02 cmp $0x2,%eax
8048ce4: 75 07 jne 8048ced
8048ce6: 83 7c 24 04 0e cmpl $0xe,0x4(%esp)
8048ceb: 76 05 jbe 8048cf2
8048ced: e8 49 05 00 00 call 804923b
8048cf2: 83 ec 04 sub $0x4,%esp
8048cf5: 6a 0e push $0xe
8048cf7: 6a 00 push $0x0
8048cf9: ff 74 24 10 pushl 0x10(%esp)
8048cfd: e8 5c ff ff ff call 8048c5e
8048d02: 83 c4 10 add $0x10,%esp
8048d05: 83 f8 0a cmp $0xa,%eax
8048d08: 75 07 jne 8048d11
8048d0a: 83 7c 24 08 0a cmpl $0xa,0x8(%esp)
8048d0f: 74 05 je 8048d16
8048d11: e8 25 05 00 00 call 804923b
8048d16: 8b 44 24 0c mov 0xc(%esp),%eax
8048d1a: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048d21: 74 05 je 8048d28
8048d23: e8 68 fa ff ff call 8048790
8048d28: 83 c4 1c add $0x1c,%esp
8048d2b: c3 ret
(2)破解过程与结果
首先,第四关和第三关输入函数的调用代码类似,简单概括如下图:
我们接下来分析不一样的地方:
1.
8048ce6: cmpl $0xe,0x4(%esp)
8048ceb: jbe 8048cf2
8048ced: call 804923b
这三行代码检查了格式化输入后的第一个整数,看第一个整数是否不大于0xe,(jbe是条件跳转,当无符号数整数比较A不大于B 时,发生跳转)。如果大于 0xe(十进制的14)或者为负值,则不跳转,继续执行下一条指令,造成炸弹爆炸。
所以,以上代码可以理解为如下伪代码:
unsigned int v1; int v2; if ( __isoc99_sscanf(p1, "%d %d", &v1, &v2) != 2 || v1 > 14 ) explode_bomb();// 输入的数不是两个,或第一个数比14大,或第一个数是负数,则都会爆炸。
尤其要注意无符号数比较的问题,不能为负值,负值就爆炸。
2.
8048cf5: push $0xe
8048cf7: push $0x0
8048cf9: pushl 0x10(%esp)
8048cfd: call 8048c5e
这段代码首先准备了函数调用的参数,一共有三个参数,第一个数是我们输入的第一个数,第二个为数0,第三个为数14。然后我们调用call func4 指令,进入 func4 函数。
接着,分析 func4 的功能:
1). 首先这段代码实现了将调用者的 esi 和ebx 压栈,保存现场。然后又预分配了4个字节的空间:
8048c5e: push %esi
8048c5f: push %ebx
8048c60: sub $0x4,%esp
2). 这三条指令是计算了三个局部变量的值:
8048c63: mov 0x10(%esp),%edx ; SrcV:指定的目标数字
8048c67: mov 0x14(%esp),%esi ; 0 -> st:表示搜索范围的起始数字
8048c6b: mov 0x18(%esp),%ecx ; 14 -> ed:表示搜索范围的结尾数字
3). 这段指令序列实现了用 ebx 保存当前 ed – st 的结果。
8048c6f: mov %ecx,%eax ; 临时拷贝 ed 到 eax 寄存器
8048c71: sub %esi,%eax ; ed - st,结果保存在 eax
8048c73: mov %eax,%ebx ; 结果转移到 ebx
4). 这段指令序列实现了将 ebx 中的数值除以2,有符号数不能够整除时向零取整。并将结果继续保存到 ebx 中,所以 %ebx = (ed - st) / 2
8048c75: shr $0x1f,%ebx ; 逻辑右移 31 位,相当于取符号位
8048c78: add %ebx,%eax ; 对于被除数为负数且不能被整除的情况做向零取整,需要加上1,正好符号位是1,所以利用直接加符号位,将不影响正数的计算(这是一个技巧)
8048c7a: sar %eax ; eax 的值算数右移 1 位,相当于有符号数除法,除数为 2.
8048c7c: lea (%eax,%esi,1),%ebx ; 用 st 加上 st 和 ed 之间距离的一半,计算结果是中位数,放到 ebx 中
自此,我们可以用伪代码概括一下上面一段指令的功能:
int mid = (ed - st) / 2 + st; // 求中位数
5). 根据上面计算的中位数(mid)和目标数(SrcV)之间的大小关系,对函数自身进行递归调用,通过分析参数的准备过程不难推断出 func4 的算法是二分,即目标数小于中间数的时候,目标数只能在中位数左侧的搜索区间内,相反只能在右侧的搜索区间内,从而缩小搜索范围。
8048c7f: cmp %edx,%ebx ; 要查找的数 SrcV(在 edx 上),和 ebx 也就是中位数比较
8048c81: jle 8048c98 ; 如果 中位数 = 目标数,结合前面的条件中位数 > 1; if (mid > x) return mid + func4(x, st, mid - 1); if (mid 14 >> 2 >> 1 >> 10 >> 0 >> 8 >> 4 >> 4 >> 9 >> 13 >> 11 >> 7 >> 3 >> 12 >> 5。所以第一个数是数字 5 。而第二个数则要等于累加和(不包括第一次输入的数),也就是 12 + 3 + 7 + 11 + 13 + 9 + 4 + 8 + 0 + 10 + 1 + 2 + 14 + 6 + 15 = 115 。
所以本题答案是 “5 115”,到测试环境下验证通过:
下面为了总结汇编代码的功能,给出还原出来的C语言代码。
概念验证代码(C 语言):
#include int array_3250[] = { 10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6, 5 }; int d_phase_5(int a) { int count = 0, sum = 0; int index = a; do { count++; index = array_3250[index]; sum += index; } while ( index != 15 ); if ( count != 15)// 判断是否满足返回后不爆炸的条件,如果爆炸,则返回 -1 return -1; else return sum; } int main() { for (int i = 1; i
编译运行结果如下:
6.6 phase_6
(1)相关汇编代码
08048db9 :
8048db9: 56 push %esi ; 准备环节
8048dba: 53 push %ebx
8048dbb: 83 ec 4c sub $0x4c,%esp
8048dbe: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048dc4: 89 44 24 44 mov %eax,0x44(%esp)
8048dc8: 31 c0 xor %eax,%eax
8048dca: 8d 44 24 14 lea 0x14(%esp),%eax
8048dce: 50 push %eax
8048dcf: ff 74 24 5c pushl 0x5c(%esp)
8048dd3: e8 a0 04 00 00 call 8049278
8048dd8: 83 c4 10 add $0x10,%esp ; 第一个部分
8048ddb: be 00 00 00 00 mov $0x0,%esi
8048de0: 8b 44 b4 0c mov 0xc(%esp,%esi,4),%eax
8048de4: 83 e8 01 sub $0x1,%eax
8048de7: 83 f8 05 cmp $0x5,%eax
8048dea: 76 05 jbe 8048df1
8048dec: e8 4a 04 00 00 call 804923b
8048df1: 83 c6 01 add $0x1,%esi
8048df4: 83 fe 06 cmp $0x6,%esi
8048df7: 74 1b je 8048e14
8048df9: 89 f3 mov %esi,%ebx
8048dfb: 8b 44 9c 0c mov 0xc(%esp,%ebx,4),%eax
8048dff: 39 44 b4 08 cmp %eax,0x8(%esp,%esi,4)
8048e03: 75 05 jne 8048e0a
8048e05: e8 31 04 00 00 call 804923b
8048e0a: 83 c3 01 add $0x1,%ebx
8048e0d: 83 fb 05 cmp $0x5,%ebx
8048e10: 7e e9 jle 8048dfb
8048e12: eb cc jmp 8048de0
8048e14: 8d 44 24 0c lea 0xc(%esp),%eax ; 第二个部分
8048e18: 8d 5c 24 24 lea 0x24(%esp),%ebx
8048e1c: b9 07 00 00 00 mov $0x7,%ecx
8048e21: 89 ca mov %ecx,%edx
8048e23: 2b 10 sub (%eax),%edx
8048e25: 89 10 mov %edx,(%eax)
8048e27: 83 c0 04 add $0x4,%eax
8048e2a: 39 c3 cmp %eax,%ebx
8048e2c: 75 f3 jne 8048e21
8048e2e: bb 00 00 00 00 mov $0x0,%ebx ; 第三个部分
8048e33: eb 16 jmp 8048e4b
8048e35: 8b 52 08 mov 0x8(%edx),%edx
8048e38: 83 c0 01 add $0x1,%eax
8048e3b: 39 c8 cmp %ecx,%eax
8048e3d: 75 f6 jne 8048e35
8048e3f: 89 54 b4 24 mov %edx,0x24(%esp,%esi,4)
8048e43: 83 c3 01 add $0x1,%ebx
8048e46: 83 fb 06 cmp $0x6,%ebx
8048e49: 74 17 je 8048e62
8048e4b: 89 de mov %ebx,%esi
8048e4d: 8b 4c 9c 0c mov 0xc(%esp,%ebx,4),%ecx
8048e51: b8 01 00 00 00 mov $0x1,%eax
8048e56: ba 54 d1 04 08 mov $0x804d154,%edx ; 结点的首地址
8048e5b: 83 f9 01 cmp $0x1,%ecx
8048e5e: 7f d5 jg 8048e35
8048e60: eb dd jmp 8048e3f
8048e62: 8b 5c 24 24 mov 0x24(%esp),%ebx ; 第四个部分
8048e66: 8d 44 24 24 lea 0x24(%esp),%eax
8048e6a: 8d 74 24 38 lea 0x38(%esp),%esi
8048e6e: 89 d9 mov %ebx,%ecx
8048e70: 8b 50 04 mov 0x4(%eax),%edx
8048e73: 89 51 08 mov %edx,0x8(%ecx)
8048e76: 83 c0 04 add $0x4,%eax
8048e79: 89 d1 mov %edx,%ecx
8048e7b: 39 c6 cmp %eax,%esi
8048e7d: 75 f1 jne 8048e70
8048e7f: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx)
8048e86: be 05 00 00 00 mov $0x5,%esi ; 最后一个部分
8048e8b: 8b 43 08 mov 0x8(%ebx),%eax
8048e8e: 8b 00 mov (%eax),%eax
8048e90: 39 03 cmp %eax,(%ebx)
8048e92: 7d 05 jge 8048e99
8048e94: e8 a2 03 00 00 call 804923b
8048e99: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e9c: 83 ee 01 sub $0x1,%esi
8048e9f: 75 ea jne 8048e8b
8048ea1: 8b 44 24 3c mov 0x3c(%esp),%eax
8048ea5: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048eac: 74 05 je 8048eb3
8048eae: e8 dd f8 ff ff call 8048790
8048eb3: 83 c4 44 add $0x44,%esp
8048eb6: 5b pop %ebx
8048eb7: 5e pop %esi
8048eb8: c3 ret
(2)破解过程与结果
1.下面这段指令序列开辟栈空间并为堆栈检测准备数据(和前面第二关卡类似):
8048db9: push %esi
8048dba: push %ebx
8048dbb: sub $0x4c,%esp
8048dbe: mov %gs:0x14,%eax
8048dc4: mov %eax,0x44(%esp)
8048dc8: xor %eax,%eax
2.这段指令序列实现读入六个数字
8048dca: lea 0x14(%esp),%eax
8048dce: push %eax ; 数组的首地址
8048dcf: pushl 0x5c(%esp) ; 输入字符串的首地址
8048dd3: call 8049278
8048dd8: add $0x10,%esp ; 恢复栈指针位置
我们也可以通过 gdb 来分析 read_six_numbers 具体做了什么(这种方法是我们前面没详细提过的):
首先在终端中使用 gdb bomb 启动调试实例:
然后依次打下几个关键断点:
b phase_6 # 在本关卡入口断下
b read_six_numbers # 断下读入数字函数(可选)
b __isoc99_sscanf@plt # 在 sscanf 函数处断下(用于知道输入格式)
b explode_bomb # 防止炸弹爆炸
然后,填入前几关的Code 到文本文件,并使用 r 指令运行炸弹程序。
由于每一关都会调用 sscanf 函数,程序运行到断点处就会被断下,所以需要手动输入 continue 继续运行,直到第六关开始,我们首先输入一串数字,并在随后触发断点sscanf ,此时就不需要继续过断点了。而是使用stepi 单步步入指令,可以让我们知道 sscanf 的参数:
显然,sscanf 函数读取了6个整形数字,即 int 类型的数据,这和前面第二关静态分析的结果一致。
3. 这段指令序列首先利用 esi 存储数组的索引变量 i ,并初始化为 0 。然后利用 (%esp + 0xc) + 4 * i 计算索引为 i 的数组元素的地址,并利用 mov 指令将该地址上的数据传递给 eax 寄存器,据此,%eax = a[i]。然后将 eax 自减 1 ,并将结果和立即数 5 进行比较,jbe 是无符号比较不大于时跳转,即 a[i] – 1