发新话题
打印

[原创]堆栈溢出点定位原理分析

[原创]堆栈溢出点定位原理分析

文章作者:DarkBoxer暗夜拳师
信息来源:邪恶八进制信息安全团队(www.eviloctal.com

嘻嘻,今天把电脑的一些东西转移到移动硬盘的时候,发现自己以前投过的几篇稿件,粗看了一篇觉得有几篇还是有点余热,索性提交到邪八,希望可以对有需要的朋友一些帮助...菜鸟写的菜文,高手就不要笑话了
本文曾发表于黑防06年第10期

前言
大家都知道,缓冲区溢出的利用可以简单的分成三了步骤①漏洞分析,定位溢出点②编写shellcode③修改溢出点,使其能够跳转到shellcode的内存地址。这样有漏洞的程序便会改变执行流程进入到我们精心构造的shellcode中,达到攻击的目的。
步骤虽然少,但十分精炼,每一步的都不是那么简单的。shellcode,相信是大家比较熟悉的,<黑防>几乎期期都有讲解,它的编写涉及各方面的知识,并且每一个漏洞对shellcode都有不同的限制,所以它的编写需要很深厚的编程功底和底层逆向分析能力;修改溢出点,常用的就是JMP ESP和覆盖异常CALL/JMP EBX,但是操作系统或者程序不一样,限制也不一样,比如xp不允许执行栈中的代码,所以需要在异常处理链里写入另一个跳转地址,再返回到栈中等等,操作起来也比较困难的;漏洞分析, 定位溢出点,如果是一般的第3方程序结合IDA,OD等工具还好说,但是如果是分析操作系统内部,那么就需要大家对系统低层有很深入的了解,应该说是非常熟悉才行。
道路是曲折的,前途是光明的.虽然在安全领域,遇到的困难比我们想象的要多的多,但决不会成为我们追梦的绊脚石。
缓冲区堆栈溢出原理
不管是安全杂志,还是网络上,都有很多通过溢出经典函数strcpy讲解堆栈溢出,几乎都是使用下面的这个草图,原理:当拷贝的字符串长度超过了buffer的大小,便会覆盖EBP和EIP,比如都覆盖为了A(16进制41),当系统返回的时候便会恢复EIP的值,并读取之,这时候因为EIP被覆盖了,所以出现41414141不可读的错误,所以我们只要把EIP覆盖为SHELLCODE的地址,那么就可以达到溢出攻击的目的。这个草图是怎么分析出来的呢?①
……………………
系统或程序分配的缓冲区buffer大小
堆栈寄存器EBP
进入函数前保存的EIP
……………………

堆栈溢出定位的方法
关于堆栈溢出定位的办法,主要有2种,一种是通过不断修改字符串长度,通过OLLYDBG等调试器或者系统错误提示来直接分析读出溢出点,这样最好是自己写个程序和脚本实现,毕竟数据量大的时候手工测试会让人很头疼;另一种是通过字符串有规律的循环来计算出溢出点的位置.关于这2个方法的详细操作步骤,大家可以去网上参考<堆栈定位的2种办法>.这里就不讨论了.后面的讲解也是会稍有涉及的.
堆栈溢出定位的原理
相信大家和我一样,在定位有堆栈溢出漏洞的程序的时候,大多数时候经常采用上面的2种的办法,特别是后者.但是方法毕竟只是属于技巧,就像测试web安全的时候在get提交方式的网址后面加’或者and 1 = 1等,判断是否存在注入漏洞一样。做安全的我们可不能知其然,而不知其所以然。大家有没有想过,为什么这2种办法可以精确定位到溢出点呢②?下面我就和大家带着上面共2个疑问和大家来分析讨论一下.毕竟实践出真知嘛^_^
先写一个将会出问题的程序吧,很简单的,相信会一点C的同学都可以看懂。
复制内容到剪贴板
代码:
1.  #include <stdio.h>
2.  #include <string.h>
3.  void overflow(char buf[])
4 . {
5.   char out[400];
6.   int i
7.   strcpy(out,buf); //溢出问题函数
8.   for (i = 0; i < 300; i++)
9     printf("\\x%x",out[i]);
10. }
11. int main(void)
12. {
13.   char buf[500] = {0};
14.   int i;
15.   for (i = 0; i < 300; i++)
16.     buf[i] = &#39;A&#39;;
17.   overflow(buf);
18.   return 0;
19.  }
当然上面是个正常的程序test.exe,结果是打印300个字符A.这里分析test.exe主要是为了分析一下堆栈里的正常情况,从而解决问题①,大家都知道在进入一个函数的时候,程序都会将指向下一条指令的寄存器EIP压入堆栈,以便返回时,能够继续保持程序正常运行.具体情况是怎样的呢?
笔者用OD载入test.exe,进入到overflow函数的时候,在堆栈窗口我们可以看见EIP:004010DD已经被压入堆栈,通过OD,我们也知道,在此之前,overflow函数的参数也已经被压入堆栈.结构如下
指令寄存器EIP(堆栈指针ESP此时也指向这里)
overflow函数的参数(如传递的AAAA….)

进入overflow函数后,我们来分析一下汇编代码.
注意,入栈一次esp-4,出栈一次esp+4
push ebp
//将ebp压入堆栈,esp-4指向ebp寄存器(原本//esp是指向eip的,看作esp-0)
mov ebp,esp
//将esp的值送入ebp,由ebp操作堆栈,此时ebp = //esp-4,这里要和倒3行的汇编代码对应: mov //esp,ebp
sub esp,194
//开辟缓冲区空间大小404,16进制194等于10进//制404:此时esp-404
mov eax,dword ptr ss:[ebp+8]
//结合上面的结构,[ebp+8]往栈底移8个字节.肯//定就是是指向overflow函数的参数300个A
push eax
// 将eax压入堆栈,esp+4,剩下的缓冲区空间大小//就是16进制190,也就是10进制400,此时//:esp-408
lea ecx,dword ptr ss:[ebp-190]
//[ebp-190]往栈顶方向移动16进制的190个字节//也就是10进制的400个字节处,ecx指向程序所//分配的缓冲区
push ecx
//ecx入堆栈,此时:esp-412
call overflow.00401120
//strcpy函数->strcpy(ecx,eax)
add esp,8
//堆栈平衡,esp+8,此时esp-404
//下面进入for循环
mov dword ptr ss:[ebp-194],0
jmp short test.00401037
/mov edx,dword ptr ss:[ebp-194]
|add edx,1
|mov dword ptr ss:[ebp-194],edx
> cmp dword ptr ss:[ebp-194],190
|jge short test.00401073
|mov eax,dword ptr ss:[ebp-194]
|movsx ecx,byte ptr ss:[ebp+eax-190]
|test ecx,ecx
|je short test.00401073
|mov edx,dword ptr ss:[ebp-194]
>|movsx eax,byte ptr ss:[ebp+edx-190]
|push eax
|push test.00406030 ; ASCII "\x%x"
|call test.004010E7
|add esp,8
|^\jmp short test.00401028
//以上为for循环,打印AAAAAAAA……
|> mov esp,ebp
//将ebp的值还给esp,此时:esp-4
|pop ebp
//恢复ebp原先保存的值,出栈esp+4,此//时:esp-0也就重新指向eip了
\retn
//这个retn要注意!!!!!

在汇编中,retn相当于pop eip,也就是将esp的值送到eip中,而esp的内容就是当前eip的内容,通过最后几条汇编代码,我们知道这样的传送方式esp->eip.
通过我们的分析实践,可以得出这么个草图来概括overflow函数的简单堆栈结构.
……………………
系统或程序分配的缓冲区大小
寄存器ebp
进入函数前要保存的指令寄存器EIP
overflow函数的参数
………………………

通过上面OD调试实践,我们可以得到以下结论:在进入函数前程序会将指向下一条指令的EIP寄存器压入堆栈,以便返回时恢复;进入函数后,将寄存器EBP压入堆栈保存(正如上面所绘草图),再将ESP的内容送到EBP中,由EBP操作控制堆栈,紧接着压入的就是一些参数以及执行其他一些函数,在函数返回时,执行MOV ESP,EBP;POP EBP将EBP的内容还给ESP,EBP弹出堆栈,恢复原先的值;执行RETN,将ESP寄存器指向的内容的送到EIP中,程序读取EIP内容,然后程序继续执行…
我们可以看出,函数strcpy没有进行边界检查,如果我们输入的参数超过400,那么多出的就会覆盖ebp和eip,在函数返回的时候,esp指向了被覆盖的eip的值,最后执行的retn的时候,再把已覆盖的值给eip,肯定出错!
既然程序在函数返回时是读取EIP的内容,如果我们把EIP的内容覆盖为shellcode 的地址,那么溢出攻击便实现了.通过我们的分析,问题①的草图就是这样分析出来的.^_^不过,大家还是要记住,具体问题需要我们具体分析.不同的有问题的函数堆栈结构还是有差别的.
这里有个小知识,VC编译器是按四字节对齐的,有了前面的分析,那么我们对源代码进行分析话,400/4=0,EBP占4个字节,那么从404字节开始便是EBP.但是这属于看着源代码的进行的白箱测试,大多数情况,我们都是黑箱测试(因为人家的代码怎么可能随便给你呢),也就是系统和程序到底分配多大的缓冲区我们是不知道的.
这样的情况的,我们可以根据对正常情况下得出的分析和草图来定位溢出点.
将C代码中15行的for循环中300改为500,OD载入运行,我们将看见熟悉的提示,如图一



通过上面的分析,我们知道,过多的A(16进制41)将EBP覆盖了,返回前EBP->ESP->EIP,最后函数返回后程序便读取EIP的内容,当然EIP这时候的值是不可读的,就出错了.
第一种办法就是不断修改字符串长度,直到定位溢出点(比如,350个字节正常,402个字节时候未完全覆盖,加到403的时候就完全覆盖了,那么404个字节开始就是溢出点了),相信大家应该能明白,我就不多说了,最好是用脚本来实现.
第二种办法是先将保持字符串长度为500,因为这个时候能产生图一的错误,然后将C代码16行改为
复制内容到剪贴板
代码:
buf[i] = &#39;A&#39;+i/10;
运行程序得到如图二提示



再将16行改为
复制内容到剪贴板
代码:
buf[i] = &#39;A&#39;+i%10;
运行程序得到如图3提示



然后用0x69-0x41=0x28,0x45-0x41=0x4。然后将0x28和0x4转换为10进制0x28->40,0x4->4
最后40*10+4得出溢出点为404
为什么这样的算法就可以定位溢出点呢.这是我们的第2个问题吧
赋值语句
复制内容到剪贴板
代码:
buf[i] = &#39;A&#39;+i/10;
可以知道字符串是从A(0x41)开始按每10个数循环的,没有重复的去填充缓冲区,当执行到retn时,ESP的内容已经是69696969,当然是因为开始把EIP覆盖了,如图4,所以从A(0x41)到i(0x69),中间相隔0x28(10进制40),又因为是按每10个数循环,所以距离 EIP是40*10=400个字节
但是i也是循环10次,到底是哪些覆盖了EIP,还需要进一步确定.



图四
复制内容到剪贴板
代码:
buf[i] = &#39;A&#39;+i%10;
是在A~J循环(0x41~0x4A),所以当系统报错48474645不可读时,照同样的分析,我们可以得出进入EIP寄存器的第一个字节在0x45处!注意,windows堆栈是按高地址向低地址分配的,所以第一个字节是0x45而不是0x48!因为是一个字节循环,所以0x45与0x41相隔4*1=4个字节,所以最后得出溢出点400+4=404个字节开始,这和白箱测试结果一样,问题②也解决^_^
小结
大家可以把out数组的长度改为398,最后得出覆盖点也是404,为什么呢,我们前面也讲过VC编译器是按4字节对齐,在为out数组分配空间的时候,按4字节也分配400个字节大小的缓冲区。
虽然这只是一个小小的分析,但我个人觉得,有问题就要想办法解决,最好是自己动手,这样可以增强了自己的逆向分析能力,很多时候教程讲解的都是和作者操作机器或者程序有关,所以自己动动手,实践一下,会学会很多以前没学到的东西^_^

TOP

发新话题