计算机系统实验:BombLab

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

一、实验题目:Bomb Lab

二、实验目的:

  1. 了解并熟悉底层汇编语言的特点,能够看懂汇编语言
  2. 能够通过汇编语言,联系起高级语言,推测出每一行汇编代码的意义
  3. 用汇编相关知识拆除六颗炸弹以及发现隐藏炸弹并进行拆除

三、实验环境:Ubuntu12.04环境,gdb-peda

四、实验内容及操作步骤:

  1. 题目审阅

打开文件夹,出现如下文件:

[()(https://gitee.com/dominique-yiu/csdn/raw/master/image-20210509132755227.png)]

其中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程序是不完整的,它并没有每个调用函数的源程序,因此为了探究这个实验必须研究它的汇编代码。

  1. 环境准备

由于接下来的汇编代码会非常多,而为了有好的研究体验,可以首先配置好一个插件。在本实验中,我们使用了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
  1. 实验任务

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.

通过上述的分析,我们得知对于输入有以下的限制条件:

  1. 输入是六个数字
  2. 六个数字中第一个数字必须要为非负数
  3. 对于第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</
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

call me Patrick

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值