通常微软在发现漏洞后,会公布补丁,而攻击者通过补丁的说明和监控补丁的执行过程,利用漏洞开发工具定位可能存在攻击点的位置,从而编制溢出程序,攻击未打补丁的系统,这种漏洞如果普通用户打上补丁,就没有任何意义了。而0Day通常指微软未发现的漏洞,既然未发现,那么自然也无标准的补丁可打,因此对于这种Windows操作系统的普通用户,只能是“人为刀俎,我为鱼肉”,任人宰割了。缓冲区溢出攻击漏洞几十年来,其攻击的思想并没有本质的改变,本文将向那些有志于溢出梦想的朋友提供一个入门讲解,希望能对缓冲区溢出的核心思想有所了解。
本文内容所提及均为本地测试或经过目标授权同意,旨在提供教育和研究信息,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,作者不鼓励或支持任何形式的非法行为。
基础知识
1)call指令和ret指令
call指令和ret指令是下面我们讲解内容的基础,因此这里不嫌啰嗦,对call指令和ret指令背后的动作进行一个较为详细的讲解。
call指令通常表现为“callxxxxxxxx;”的汇编形式,其幕后的动作为将“callxxxxxxxx”这条指令后的指令地址压入堆栈,然后跳转到xxxxxxxx指向的地址执行代码,其执行如图1所示。
图1call指令的背后
ret指令的功能则是弹出当前栈顶单元的内容到EIP,从而控制程序的流程执行EIP指向地址的代码,其执行如图2所示。
2)Windows函数调用的结构
了解了call指令和ret指令,下面我们需要费一番脑筋,看些稍微令人头疼的C语言代码和汇编代码,但是我保证并不长,也不难。
ADD(inta,intb)
{
intc;
c=a+b;
Return(c);
}
Main()
{
inta,b,c;
a=1;b=1;
c=ADD(a,b);<--①
printf(“%d”,c);<--②
}
相信上过大二的理工科同学对上述代码不会感到陌生,否则计算机二级是很难通过的!
主函数调用ADD函数,完成一个相加操作。下面我们将焦点集中在从步骤①开始到步骤②之前一个简单的函数调用背后,程序运行的一些细节。
首先说明的是,函数调用的规则有很多种,比较典型的是_cdcel(C方式)、_stdcall(标准调用方式,用于WinAPI)和_fastcall(快速调用方式)等,为了专注于主题,我们仅以_cdcel为例。这种函数调用的过程是:
①调用者将参数按照从右至左的顺序,使用push指令压栈;
②使用call指令调用函数,返回值通过eax进行传递;
③函数返回后,由调用者addesp,n指令来恢复堆栈,其中n表示函数参数的长度。做好以上铺垫,我们看看函数调用的庐山真面目。为了使讲解清楚简单,下面的代码省略了一些编译器自动添加的一些操作,如ebx、esi、edi的压栈出栈。以下的代码分为函数的调用和ADD函数内部的实现。
函数调用的实现:
Pushb
Pusha
;压入参数b,对应步骤①
;压入参数a,对应步骤①
CallADD;调用子函数,对应步骤②
Addesp8;调用者清理堆栈,对应步骤③
为了配合函数的调用,在ADD函数内部也会有相应的操作,请看下面代码。
ADD函数的实现:
这三段汇编代码是函数调用后通用的模版,其中Pushebpsubesp4;是为了给ADD函数中的内部变量c使用,通Movebp,esp常来说,内部变量占用多大的空间,堆栈中就要开辟多大的空间。
Movebx,[ebp+0C]
Movecx,[ebp+8]
将参数b存入寄存器ebx,将参数a存入寄存器ecx,完成相加的操作,并将结果存入eax,准备走人。
Moveax,ebx
Addesp,4
Movesp,ebp
Popebp
Ret
使用addesp,4的方式弹出内部变量占用的堆栈,恢复esp,ebp,然后通过ret指令退出本次函数调用。
上述代码都有较为详细的说明,其中堆栈对应的变化请看图3示例,会发现这是一个左右对称富有美感的图,原因很简单,因为call/ret指令本身就是互相匹配的,换句话说,在call指令执行后和ret指令执行前堆栈的情况应该完全一样。还需要强调一点的是,函数内部使用的变量都保存在通过函数开始的指令Subesp,n保留的堆栈空间中。换句话说,C语言中对内部变量的操作,实际上对应的是对堆栈某个地址的操作,这也很好理解,内部变量一般局限于函数内部,退出这个函数后,其保留的堆栈中的空间也就随之释放,非常合理。
图3函数调用过程中堆栈的变化
简化一些说,在进入函数前,call指令会将下一条指令的地址存入堆栈,以便返回时恢复。进入函数后,将寄存器ebp压入堆栈保存,再将esp的内容送到ebp,以后由ebp操作控制堆栈,subesp,n空出内部变量的空间,在函数返回时,执行movesp,ebp、popebp,将ebp的内容返回esp,ebp出栈,执行ret指令,程序继续执行。
如果能够在脑海中将上述过程堆栈的变化动画般重演一遍,那么恭喜你,对系统运行原理的理解又上了一个新的台阶。
缓冲区溢出漏洞的利用
一个成功的黑客攻占对方电脑的标志,本质上来说就是取得对方电脑的控制权。这里所谓的控制权就是能够以最高权限去执行侵入者的代码。那么好,这里涉及到几个问题:黑客如何将他的代码存入对方的主机,如何控制程序的流程去执行存入的代码,又如何解决权限的问题。
网络的盛行,就在于电脑之间的交互,这个交互说直白些就是我给你数据,你也给我数据,客户端和服务器端都为每条连接开辟了缓冲区,存储这些数据,这些数据可以是QQ聊天、浏览的网页,当然也可以是一些恶意的代码。那么如何控制对方去执行这些代码呢?否则岂不是劳而无功,我们看看下面一段存在缓冲区溢出漏洞的代码。
Printsome(char*bufferIn)
{
CharszBuff[8];
Strcpy(szBuff,bufferIn);
Printf(“%s”,szBuff);
}
Main()
{
Charsz_in[20];、
Scanf(“%s”,sz_in);
Printsome(sz_in);
}
这段函数很容易理解,主函数中等待用户输入,然后在Printsome函数中显示用户的输入,如输入abcde,会在结果中显示abcde。在Printsome函数内部,会将参数sz_in指向的字符串通过strcpy函数拷贝到内部变量szBuff数组中,然后进行显示,其中szBuff数组的长度是8个字节。图4中的左图说明了执行strcpy函数后堆栈的存储情况,需要注意的是,字符串的存储是从低地址向高地址延伸的。好了,似乎一切都很正常。但如果我们输入的字符超过8个字节,如输入“abcdefghijklmnopqrst”共20个字节,由于strcpy函数并没有校验拷贝源的长度,因此它会将这20个字节一股脑的拷入szBuff数组,导致的结果就如图4右半部所示,很多重要的压栈数据被覆盖,尤其特别的是返回地址被修改了!现在好像有些事情要发生了!是的,我们可以将恶意代码(通常称为shellcode)与一个或者一组地址组合提交给对方电脑,这个地址的位置是经过精心构造的,它正好能够覆盖堆栈中“返回地址”位置中的内容,并且这个地址正好执行我们的shellcode,从而执行之,而又恰好这个含有漏洞的接收程序运行在管理员权限下,那么很明显,我们完成了对这台电脑的控制。
图4溢出的图示
事情到这里,似乎曙光初现,但先不要高兴得太早。Shellcode的地址是如何知晓的呢?换句话说,我覆盖“返回地址”的内容是什么呢?由于动态链接库的装入和卸载等原因,进程的栈的位置会变化,所以shellcode在内存中的位置也会动态变化。曾经的解决办法是一个字:猜。当然猜也会有一些技巧,但是效果并不好,直接导致结果就是很长的一段时间缓冲区溢出成功的比率并不高。后来一位叫Dildog(网名)的高人在1998提出了“jmpesp”大法,最终真正的将缓冲区溢出技术推广开来。
很多似乎高深的东西实际上就是一层窗户纸,我们将图3中ADD函数执行完ret指令后的堆栈结构单独拿出来,会看到此时esp指向的地址就在“返回地址”的下方。一个天才的想法就是在系统DLL中找到包含jmpesp的指令地址,并将这个地址覆盖堆栈中的“返回地址”,而在“返回地址”下方覆盖我们的shellcode,那么当执行ret指令后,就会执行jmpesp指令,从而完成到shellcode的跳转。其流程如图5所示。
图5jmpesp大法
我们经常会在微软补丁的说明中看到这样的字样:通过输入经过精心构造的数据结构,从而取得主机的控制权。经过以上的讲解,会看到所谓的“精心构造”并不神秘,一般来说这个数据结构会如图6所示。
图6所谓精心构造的数据结构
其中的特殊地址就是系统DLL中包含jmpesp代码的地址。需要注意的一点是,因为每个操作系统的版本不同,也导致了jmpesp的地址不同,因此攻击的代码也会因为操作系统的不同而变化。
小结
现在回头来看,本文缓冲区溢出的根源在于Strcpy函数没有进行边界检查。如果在每个Strcpy函数前都对拷入数据的长度进行检查,似乎就可以避免这样的问题,但是事情并没有这样简单,首先内核的代码如此庞大,会产生溢出的部分并不只局限于这个函数,另外应用层的程序、驱动的程序,马虎的程序员都有可能犯下低级的错误。
为了防范缓冲区溢出,在WindowsXPsp2和Windows2003sp1系统中加入了数据执行保护(DEP)技术,只要CPU支持DEP,且在系统中启用,那么程序的堆栈和默认堆都不能执行代码。但是魔高一尺,道高一丈,有一种规避DEP的技术,其思路是这样的,在系统启动之初,用户可以选择DEP是否开启,攻击者可将本文中的“返回地址”设为关闭DEP那段代码的起始地址,进而执行关闭DEP的程序,接着再进行溢出的尝试。但还是要看到,在使用DEP技术后,通过缓冲区溢出攻击成功的概率大大减小。
除了缓冲区溢出漏洞,比较重要的还有堆溢出漏洞、格式化串漏洞、针对异常处理机制的攻击等,其思想不外乎利用漏洞修改某个重要地址中的内容,从而控制程序的流程。希望本文介绍的思想能对读者了解这个领域有所帮助。