一、实验题目:Bomb Lab
二、实验目的:
- 了解并熟悉底层汇编语言的特点,能够看懂汇编语言
- 能够通过汇编语言,联系起高级语言,推测出每一行汇编代码的意义
- 用汇编相关知识拆除六颗炸弹以及发现隐藏炸弹并进行拆除
三、实验环境:Ubuntu12.04环境,gdb-peda
四、实验内容及操作步骤:
打开文件夹,出现如下文件:
![[()(https://gitee.com/dominique-yiu/csdn/raw/master/image-20210509132755227.png)]](https://i-blog.csdnimg.cn/blog_migrate/1b981047bacd0ff6c534b01c93c54ecc.png#pic_center)
其中bomb是可调式文件,bomb.c文件是“源文件”,README是一个文档。bomb无法直接打开以及README没有内容,因此首先查看bomb.c文件中是否有有用信息。C程序内容如下:
#include <stdio.h>
#include <stdlib.h>
#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. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
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 [<input_file>]\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! */
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");
/* 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;
}
通过观察C程序可以清晰的看出该C程序的结构,每个炸弹小关的结构都是四行:read_line进行输入、phase_i进入第i个炸弹关卡、phase_defused函数以及通关后的祝贺字符串。因此,这说明在每一关卡都得输入一些数值,而你的输入会决定炸弹是否会爆炸。
但遗憾的是这个C程序是不完整的,它并没有每个调用函数的源程序,因此为了探究这个实验必须研究它的汇编代码。
由于接下来的汇编代码会非常多,而为了有好的研究体验,可以首先配置好一个插件。在本实验中,我们使用了gdb-peda插件,gdb-peda具有更加友好的用户页面,使得调试更加有效,并且gdb-peda能够实时跟踪查看寄存器、反汇编语句以及栈帧之中的部分内容,并且在进行函数跳转时,提供了可能进行传递的参数,使得gdb调试更加可视化。
- 首先安装git:
sudo apt-get install git
- 安装gdb-peda插件:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
- 实验任务
3.1. phase_1
反汇编,phase_1汇编代码如下:

下面对其进行解释与说明(我的输入:Brownie, you are doing a heck of a job.):
Dump of assembler code for function phase_1:
0x08048b50 <+0>: sub esp,0x1c
// 新栈帧的初始化
0x08048b53 <+3>: mov DWORD PTR [esp+0x4],0x804a184
// 函数调用前参数的准备,这是第一个参数的地址
0x08048b5b <+11>: mov eax,DWORD PTR [esp+0x20]
// 第二个参数首先放在eax
0x08048b5f <+15>: mov DWORD PTR [esp],eax
// 把存放在eax的参数给到栈顶
0x08048b62 <+18>: call 0x8048fa4 <strings_not_equal>
// 调用函数strings_not_equal,由字面意思可知是判断两个字符串是否一致的函数
0x08048b67 <+23>: test eax,eax
// 判断eax的符号,以及给一些标志位赋值
0x08048b69 <+25>: je 0x8048b70 <phase_1+32>
// 判断标志位ZF,即eax是否为0
0x08048b6b <+27>: call 0x80490b6 <explode_bomb>
// 如果不为0,那么就调用explode_bomb函数引爆炸药
0x08048b70 <+32>: add esp,0x1c
// exp恢复
0x08048b73 <+35>: ret
// 返回到call的下一条语句
End of assembler dump.
为了验证自己对strings_not_equal函数功能的猜测,将该函数汇编中重要关键的部分拿出来进行研究:
Dump of assembler code for function strings_not_equal:
0x08048fbb <+23>: mov DWORD PTR [esp],ebx
0x08048fbe <+26>: call 0x8048f8b <string_length>
0x08048fc3 <+31>: mov edi,eax
0x08048fc5 <+33>: mov DWORD PTR [esp],esi
0x08048fc8 <+36>: call 0x8048f8b <string_length>
0x08048fcd <+41>: mov edx,0x1
0x08048fd2 <+46>: cmp edi,eax
0x08048fd4 <+48>: jne 0x8049009 <strings_not_equal+101>
// 以上是先比较两个字符串的长度,如果不同的话直接输出1
0x08048fd6 <+50>: movzx eax,BYTE PTR [ebx]
0x08048fd9 <+53>: mov dl,0x0
0x08048fdb <+55>: test al,al
0x08048fdd <+57>: je 0x8049009 <strings_not_equal+101>
// 判断是否为空字符串
......
......
......
// 接下来就是对字符串的每一个字符进行对比
End of assembler dump.
通过上面的汇编分析,可以验证我们猜想是正确的,如果两个字符串不相同返回值为1,反之为0。于是,我们就要重点关注在调用strings_not_equal函数前进行传递的参数它们到底是什么?于是我们查看得到如下结果(由于我已知答案,就直接输入正确答案了):

通过gdb-peda的帮助,我们一下子就知道了,两个参数的地址和内容是什么。为了验证自己推断的是否和peda提供的一致,因此通过以上的汇编代码,我们直到第一个参数的地址是0x8040a184,另外一个的地址放在%esp当中。因此首先x/s 0x8040a184查看得到一个参数,这个显然是本来提供的参数,那么另外一个就是自己输入的参数了。在汇编代码中可以看出这个地址被传递到栈顶了,因此首先查看%esp得到输入参数的地址,然后用相同的方式查看得到<input_strings>的字样,那么这就是我们的输入了。

于是我们可以合理的推测,我们只需要输入和地址0x8040a184下内容相同的字符串就可以通过第一关,因此有以下的场景:

3.2. phase_2
对于第二关,依然是先查看它的汇编代码,如下:

下面对每一行汇编代码进行解释和说明(我的输入:0 1 3 6 10 15):
Dump of assembler code for function phase_2:
=> 0x08048b74 <+0>: push ebx
// 保留旧的ebx
0x08048b75 <+1>: sub esp,0x38
// 给栈帧开辟0x38的空间
0x08048b78 <+4>: lea eax,[esp+0x18]
// 第一个参数的传递,先存放在eax
0x08048b7c <+8>: mov DWORD PTR [esp+0x4],eax
// 参数放在esp+0x4

可以看到传递的地址存放的值是0,接着下一步看看传递第二个参数:
0x08048b80 <+12>: mov eax,DWORD PTR [esp+0x40]
0x08048b84 <+16>: mov DWORD PTR [esp],eax
// 这两行是在传递另外一个参数,这个参数就是我们输入的字符串

在执行完这两步之后,可以发现eax存放的就是输入的字符串,exp存放的地址的内容是输入字符串的地址,接着进行剩下汇编语句的分析:
0x08048b87 <+19>: call 0x80491eb <read_six_numbers>
// 调用read_six_numbers函数,猜测读入六个数字
0x08048b8c <+24>: cmp DWORD PTR [esp+0x18],0x0
0x08048b91 <+29>: jns 0x8048b98 <phase_2+36>
// 以上两行是判断符号,我们有知道esp+0x18存放的是第一个数字的值,在这里是0
0x08048b93 <+31>: call 0x80490b6 <explode_bomb>
// 如果是负数,那么就引爆炸药
0x08048b98 <+36>: mov ebx,0x1
// 初始化ebx为1
0x08048b9d <+41>: mov eax,ebx
// 传递给eax
0x08048b9f <+43>: add eax,DWORD PTR [esp+ebx*4+0x14]
// eax = eax + 第i个数
0x08048ba3 <+47>: cmp DWORD PTR [esp+ebx*4+0x18],eax
// 把刚刚计算的结果和第i+1个数进行比较
0x08048ba7 <+51>: je 0x8048bae <phase_2+58>
// 如果相等不爆炸
0x08048ba9 <+53>: call 0x80490b6 <explode_bomb>
0x08048bae <+58>: add ebx,0x1
// ebx = exb + 1
0x08048bb1 <+61>: cmp ebx,0x6
// 由于只输入了六个数,所以不大于六
0x08048bb4 <+64>: jne 0x8048b9d <phase_2+41>
// 当ebx<6的时候继续相同的以上操作
0x08048bb6 <+66>: add esp,0x38
// 还原esp
0x08048bb9 <+69>: pop ebx
// 还原ebx
0x08048bba <+70>: ret
// 返回call的下一条语句
End of assembler dump.
通过上述的分析,我们得知对于输入有以下的限制条件:
- 输入是六个数字
- 六个数字中第一个数字必须要为非负数
- 对于第i+1个数字,它必须是第i个数字与i的和
结合上述的限制条件,我们可以得到以下符合条件的输入例子:
| 0 1 3 6 10 15; |
|---|
| 1 2 4 7 11 16; |
| 2 3 5 8 12 17……; |
| 100 101 103 106 110 115…… ; |
| n n+1 n+3 n+6 n+10 n+15; 其中n是任何非负整数 |
于是我们输入的0 1 3 6 10 15自然能够得到以下画面:

3.3. phase_3
按照之前的步骤,首先查看phase_3的汇编代码:

phase_3的汇编代码的长度一下子就上来了,下面就一段一段进行分析:
首先是栈帧的初始化以及调用scanf函数前的参数准备:
首先看一看寄存器eax的值,它保存着输入的字符串的地址(我的输入:2 50):

然后看看第一部分每一行的意义:
=> 0x08048bbb <+0>: sub esp,0x2c
// 为栈帧开辟0x2c的空间
0x08048bbe <+3>: lea eax,[esp+0x1c]
// 通关观察寄存器,得知传给eax的是存放ebx值的地址
0x08048bc2 <+7>: mov DWORD PTR [esp+0xc],eax
// 第一个参数的准备
0x08048bc6 <+11>: lea eax,[esp+0x18]
// EAX: 0xbffff278 --> 0xa ('\n')
0x08048bca</

本文详细介绍了一项名为BombLab的拆弹实验,该实验旨在通过分析底层汇编语言来解除虚拟炸弹。实验共包含六个阶段及一个隐藏阶段,每个阶段都需要特定的知识与技巧来解决。通过对汇编代码的深入解读,读者将学会如何使用gdb-peda工具辅助分析,并掌握一系列有趣的编程挑战。
最低0.47元/天 解锁文章
556

被折叠的 条评论
为什么被折叠?



