转自http://blog.csdn.net/lpmygod/article/details/7458769
本文的实验来源于《Computer Systems A Programmer's Perspective》(深入理解计算机系统》一书中第三章的一个实验。
函数getxs的功能类似于库函数gets的功能,除了它是以十六进制数字对的编码方式读入的字符。例如,要读入字符串“0123”,你得给出输入字符串“30 31 32 33”,这个函数会忽略空格。
分析这个程序,可以得知,正常情况下,这个函数会在getbuf中,调入getxs函数读入数字对,然后不管任何情况下,都会对test函数返回0x1,然后由test中的printf函数打印处getbuf的返回值。
现在你的任务,就是,利用缓冲区溢出的漏洞,输入些特殊的数字,使得屏幕中打印的是0xdeadbeef。
我是在WindowsXP,visual c++6.0环境解决这个问题的。
在做这个题目之前,你当然要知道什么是帧栈结构(请参阅《深入理解计算机系统》第三章),了解%ebp和%esp的含义。
题目中已经说了,“分析这个程序,可以得知,正常情况下,这个函数会在getbuf中,调入getxs函数读入数字对,然后不管任何情况下,都会对 test函数返回0x1,”那我们该怎么办了?我们马上可以想到在getbuf这个函数里定义的char buf[12]上做手脚,可以看到在getxs函数里的while循环,结束条件只是以回车或者是eof结束符为判断标准,所以,根本没对char输入的数量做判断!这样的话,我们可以输入多于12个的数,从而缓冲区溢出!
在这里还是提一下帧栈结构,如下:
+-------------------------------+高地址
|函数参数 n 个 |
+-------------------------------+
|函数参数第 n-1 个 |
+-------------------------------+
| . . . |
| . . . |
| . . . |
+-------------------------------+
|函数参数第1个 |
+-------------------------------+
|return 返回地址 |
+-------------------------------+
|ebp指针入栈 |
+-------------------------------+
|local var(局部变量) |
+-------------------------------+
| others |
+-------------------------------+低地址
按照上面说的函数栈的存放情况,在getbuf这个函数里,函数参数没有,我们不管,然后就是return返回地址,然后就是ebp指针,然后就是char buf[12]。
+-------------------------------+低地址
|return 返回地址 |
+-------------------------------+
|ebp指针入栈 |
+-------------------------------+
| buf[11] |
+-------------------------------+
| buf[10] |
+-------------------------------+
:
:
:
+-------------------------------+
| buf[0] |
+-------------------------------+
| others |
+-------------------------------+高地址
如果我们对buf溢出,能改写ebp和return地址!下面看看,ebp是多少,return地址是多少。
要知道这里的%ebp存的是test函数的%ebp,因而我们在调试的时候就可以在test函数得到%ebp的值,它应该是我们写入的 buf[12]-buf[15]的值,而且它要保持原来的值,不然返回之后就乱套了,在我机器上是0x0012efa0。这个很容易,解决了第一步。
下面我们再来看返回地址,先看一段汇编码(不同的机器有所不同):
58: val = getbuf();
004011C5 call @ILT+10(getbuf) (0040100f)
004011CA mov dword ptr [ebp-4],eax
59: printf("getbuf returned 0x%x/n", val);
004011CD mov eax,dword ptr [ebp-4]
004011D0 push eax
004011D1 push offset string "getbuf returned 0x%x/n" (0042001c)
004011D6 call _printf (00401510)
004011DB add esp,8
在getbuf()返回后,肯定会接着执行004011CA ,我们能让它从这执行吗?当然不行!不然就要push eax,那是我们不想看到的,因为eax的值就是1。因而我们会想到能不能跳过这?当然能,改返回地址啊!顺水推舟,我们通过buf数组来覆盖返回地址。 此时,我们想要它直接跳到004011D1处,因而可以通过设置buf[16]-buf[19]的值来覆盖返回地址。
到了考虑如何加进deadbeef了,在返回后,将直接执行push offset string "getbuf returned 0x%x/n" (0042001c),没eax怎么行了?不然printf函数就少了参数。再回到帧栈结构一下,printf在调用之前,要压入参数,先压入val,再 压入offset string "getbuf returned 0x%x/n",也就是说参数val(等同那个eax)在offset string "getbuf returned 0x%x/n“之上,而且紧挨着。此时我们可以想到,既然返回之后(返回地址及其以下的元素都已弹出,返回地址的上一个字节成了栈顶)就执行 push offset string "getbuf returned 0x%x/n" (0042001c)进行压栈,那此时栈顶一定是参数val,而它原来就在返回地址上面,因而我们可以通过设置buf[20]-buf[23]的值来覆盖 这个地方。
综上所述,%ebp的值为0x0012efa0,修改后的返回地址为0x004011D1,因而我们可以输入
00000000 00000000 00000000 a0ef1200 d1114000 efbeadde
这24个0可以输入别的,不影响,关键是后面24个数。
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里 … 。
实现上有硬件方式和软件方式(有些体系不支持硬件栈)
缓冲区溢出攻击主要是利用栈帧的构成机制,利用疏忽的程序破坏栈帧从而使程序转移到攻击者期望或设定的代码上。
/*******************************************************************************************/
详细分析过程调用的相关操作和汇编指令的作用。
还是以代码来分析,非常简单的c程序,不过对于我们所关注的问题,已经足以说明问题了:
int f(int *a, int *b)
{
int c;
c = a + b;
return c;
}
int main(void)
{
int a, b;
a = 1, b = 2;
f(&a, &b);
return 0;
}
这个程序生成的汇编代码如下(省略了不相关的部分,专注于两个函数的函数体部分):
_f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
movl (%eax), %eax
addl (%edx), %eax
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
_main:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
movl $0, %eax
movl %eax, -12(%ebp)
movl -12(%ebp), %eax
call __alloca
call ___main
movl $1, -4(%ebp)
movl $2, -8(%ebp)
leal -8(%ebp), %eax
movl %eax, 4(%esp)
leal -4(%ebp), %eax
movl %eax, (%esp)
call _f
movl $0, %eax
leave
ret
用几幅图来说明问题:
1)函数的建立部分:
任何一个函数在开始调用之前都要建立起函数体的栈帧(stack frame)结构,使得ebp作为帧指针(frame pointer),
而esp作为栈指针(stack pointer),需要注意的是esp是移动的,因为esp指针保存的是栈顶元素的地址,压栈或者退栈操作都将影响到esp指
针的值,而ebp则是不动的,因此关于过程的许多信息比如局部变量的位置等等都是相对于ebp来给出的,ebp和esp这两个指针是如此的重要以
至于如果在过程中被破坏了,那么整个过程的栈帧结构也就破坏了,结果不堪设想。涉及到函数体建立的汇编代码是:
pushl %ebp ;保存上一个函数的ebp
movl %esp, %ebp ;使ebp成为目前这个过程的栈帧结构的帧指针
subl $24, %esp ;为过程的栈帧结构分配空间,这样ebp和esp之间的空间就
;可以容纳本过程的局部变量和调用下一个函数
;时需要调用的参数了,注意这个空间的大小可能还要考虑到对齐等因素。
执行完上面三个指令之后,函数的栈帧结构大致如图:
_____ebp(main) 地址高位
| |
| ........ |
| ........ |ebp和esp相差24个字节
| |____esp(main) 地址低位
2)函数体的主体部分:
具体每个函数主体部分可能有较大的差别,不过一般的,函数体的局部变量的都是放在紧跟着ebp的地址空间里,在我们的例子里在main函
数中有这样的指令:
movl $1, -4(%ebp) ;-4(%ebp)就是a的所在
movl $2, -8(%ebp) ;-8(%ebp)就是b的所在
因此,此时的main函数的栈帧结构大致如图:
_____ebp(main) 地址高位
|这个空间属于a|
| a |____a的地址在ebp - 4处
|这个空间属于b|
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |____esp(main) 地址低位
需要注意的是图中a和b指针的指向,需要提醒的是读取数据的时候是从低地址开始读取的,因此当我们说"a的地址是ebp-4"的时候意味着 ebp-4到ebp这32位的地址空间存放的是a的数据,而不是从ebp-4到ebp-8,也就是说存储数据是从低到高的,而给出地址的时候都是给的最低 位的地址。
3)准备调用f函数时的参数准备:
在一个函数的栈帧结构中除了存放有本过程的局部变量之外,在栈帧的最低的位置还要存放着这个函数将要调用的函数要用到的参数,如在我们的例子中main函数要调用函数f要用到&a, &b,那么相关的代码是:
leal -8(%ebp), %eax ;把b的地址送入eax寄存器中
movl %eax, 4(%esp) ;再通过eax寄存器把b的地址存入esp + 4处
leal -4(%ebp), %eax ;把a的地址送入eax寄存器中
movl %eax, (%esp) ;再通过eax寄存器把a的地址存入esp处
这样之后,main函数的栈帧结构大致如图所示:
_____ebp(main) 地址高位
|这个空间属于a |
| a |____a的地址在ebp - 4处
|这个空间属于b |
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |
| &b的地址 |____esp(main) + 4
| |
| &a的地址 |____esp(main) 地址低位
这就是一个比较完整的main函数的栈帧结构的示意图了,可以看出在最上面的是函数的局部变量,而在最下面的是为被调用函数准备好的参数,不同的机器,操 作系统压栈顺序也是不一样的,在我的机子上调用f(&a, &b)的时候压栈是从右到左的,也就是先压入b再压入a。
再说一说lea这个指令,这个指令并不真正的访问存储器,而是产生一个有效地址,假设在这里ebp为0x12345678,那么执行leal -8(%ebp), %eax之后eax的数据就是0x12345670了,这个指令非常有用,经常用于生成指针(指针其实就是地址嘛),还可以用于访问数组的元素,比如说我 们要访问数组E[i],假设E的起始地址存放在edx中,而索引也就是i存放在ecx中,再假设这个数组是int类型的,也就是4个字节大小的,那么访问 E[i]并且把它的指放入eax就相当于:
movl (%edx, %ecx, 4), %eax
注意这时eax中的是数据,类型是int类型的,如果要得到E[i]的地址,那么我们可以:
leal (%edx, %ecx, 4), %eax
4)执行call指令后的栈帧结构:
call指令相当于下面的两个操作,首先把调用函数中在执行完函数调用后的下一条指令的地址压入栈,在这里其实就是相当于把指令movl $0,
%eax的地址压入栈中,然后修改eip指针使它指向被调用函数的起始处(我们知道eip指针存放的是下一条指令的执行地址),此时的栈帧结构
大致如图:
_____ebp(main) 地址高位
|这个空间属于a |
| a |____a的地址在ebp - 4处
|这个空间属于b |
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |
| &b的地址 |____esp(main) + 4
| |
| &a的地址 |____esp(main) 地址低位
|main函数中下 |
|一条指令的地址|____esp(此时esp指针已经不再指向main函数的栈帧结构了)
同时,可以看到的是push操作的相当于下面的两条指令,比如说pushl %ebp,就相当于:
subl $4, %esp
movl %ebp, (%esp)
效果都是相同的,不同的是push指令的机器码要比这两个指令要简单的多。
还需要注意的是在访问内存时的两种不同操作,比如:
movl $4, %esp
相当简单,直接把4送进esp就是了。
而
movl $4, (%esp)
就复杂一点,首先得到esp中的值,然后把这个值作为地址,然后把4送入这个地址。
简而言之,没有加括号的时候,寄存器相当于普通的整形变量,而加了括号以后寄存器就相当于指针了,存放着变量的地址。
5)进入f函数以后的栈帧分布情况:
同样的,在进入f函数的时候也要建立起函数的栈帧结构,同样要调用这样的三条指令:
pushl %ebp ;保存上一个函数的ebp
movl %esp, %ebp ;使ebp成为目前这个过程的栈帧结构的帧指针
subl $4, %esp ;为过程的栈帧结构分配空间,
;这样ebp和esp之间的空间就可以容纳本过程的局部变量和调用下一个函数
;时需要调用的参数了,注意这个空间的大小可能还要考虑到对齐等因素。
这时的栈帧结构大致如图:
_____ebp(main) 地址高位
|这个空间属于a |
| a |____a的地址在ebp - 4处
|这个空间属于b |
| b |____b的地址在ebp - 8处
| ........ |
| ........ |ebp和esp相差24个字节
| |
| &b的地址 |____esp + 4
| |
| &a的地址 |____esp(main)
|main函数中下 |
|一条指令的地址 |____ebp + 4(f)
|main函数中 |
|ebp保存在这里 |____ebp(f) 地址低位
注意,首先执行pushl %ebp指令不仅把main函数的ebp栈帧保存在栈中,而且还使得esp指针减少了4,在执行movl %esp, %ebp完后,ebp和esp指向同一个位置了,因此这是ebp处保存的就是main函数的ebp指针的值,而ebp + 4的地方存放的就是main函数中下一条指令的地址了,我们前面说过过程中的局部变量是紧跟着ebp指针的,一旦发生溢出,存储的数据往地址高位走就会覆 盖这两个关键的值,如果程序修改指令的地
址让它指向一段有害的代码,或者修改ebp指针使得返回的时候到一个有害的函数处,那么后果是不堪设想的--这就是所谓的"缓冲区溢出"。
6)退出函数时的恢复堆栈准备:
一般的,在函数返回的时候都有如下的两条指令:
leave
ret
逐条来解释,首先leave指令相当于以下的两条代码:
movl %ebp, %esp ;恢复esp指针
popl %ebp ;恢复ebp指针
注意看5)的示意图,在执行完movl %ebp, %esp之后,ebp和esp指向同一个位置,前面说过ebp处保存的时main函数栈帧结构的ebp指针的值,再执行popl %ebp的时候,就可以顺利的把调用函数(这里时main函数)的ebp指针恢复了,由此再次强调在函数运行的过程中切记不可修改
ebp和esp指针,否则结果是不可预料的。
同时也可以看出pop指令的相当于下面的两条指令,如popl %ebp相当于
movl (%esp), %ebp
addl $4, %esp
执行完leave指令之后,也就是在执行完popl %ebp后,上面说了esp指针已经加上4了,也就是说esp处存放的是main函数中下一条指令的地址了,执行ret指令就相当于popl %eip,也就是把这个地址送入eip指针中(实际并不能这样做)。
上面就是一个函数从建立到调用其它函数再返回的具体分析了,说的应该够清楚的了