程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,
通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓
冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢
出。下面要谈到的栈溢出就是缓冲区溢出的一种。
由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。
向这些单元写入任意的数据,一般只会导致程序崩溃之类的事故,对这种情况我们
也至多说这个程序有bug。但如果向这些单元写入的是精心准备好的数据,就可能
使得程序流程被劫持,致使不希望的代码被执行,落入攻击者的掌控之中,这就不
仅仅是bug,而是漏洞(exploit)了。
说到这里,应该注意到这样一个事实:当前应用最广的Intel x86体系构架的
CPU,对于指令和数据并没有区分,而是cs:eip指向什么它就执行什么。换言之,
我们只要能操纵eip,让它指向我们所期望的代码,CPU也就乖乖地听话照办了。
不过,操纵eip毕竟不是那么容易。可以改写8个通用寄存器的那些指令如mov、
lea、pop、add/sub等等,到了eip身上统统行不通。修改eip只能通过很有限的几
条专属指令。这也难怪,eip始终代表着CPU的前进方向,如果eip可以象通用寄存
器那样随意操作,CPU也就只好象只没头苍蝇到处乱撞,到时候不要说CPU自己,恐
怕连程序员都无法预测它下一步要干什么了。
然而不能随意操作不意味着不能操纵。在修改eip的指令中,call/ret是很有
名堂的一对。call把当前eip——或者叫返回地址——保存在栈上,到了子程序执
行完了再由ret把这个值弹回eip。栈是什么?栈是一片可读可写的存储区域。既然
可写,那就意味着可以暗地里修改这个返回地址。(这么重要的东西竟然放在一个
可以随便进行写操作的地方!这算不算CPU本身的机制缺陷?)
光是这一点,call/ret还够不上称为“很有名堂”,得综合另外一方面的因素
进行考虑。在高级语言的实现中,子程序里如果用到变量——即所谓局部变量,这
些变量的存储空间是从栈上分配的,具体表现形式就是通过减少栈指针esp的值来
留出这些空间。又是跟栈有关!你有没有联想到什么呢?
一方面是修改栈上的返回地址,另一方面是在栈上分配缓冲区。再联想到前面
所谈到的缓冲区溢出。如果在栈上造成溢出……
哼哼,聪明的你,总算猜对了!
作为一个具体的例子,我们来看下面的程序:
;===============栈溢出利用的例子,由MASM编写===============
.386
.model flat, stdcall
option casemap: none
include windows.inc
include kernel32.inc
include user32.inc
includelib kernel32.lib
includelib user32.lib
.data?
hKeyFile dd ?
dwBytesRead dd ?
.const
szKeyFileName db 'thekey.nk',0
szMessCap db 'message',0
szBadboy db 'The program will continue in trial mode.', 0
szGoodboy db 'Thanks for registering. The program is unlocked now.', 0
.code
_HomeProc proc
local loc_Buff[8]:byte ;分配8字节缓冲区,作溢出用
invoke CreateFile, offset szKeyFileName, GENERIC_READ, FILE_SHARE_READ, \
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL
cmp eax, INVALID_HANDLE_VALUE
jne @F
ret
@@:
mov hKeyFile, eax
invoke ReadFile, hKeyFile, addr loc_Buff, 16, offset dwBytesRead, NULL ;此处读入16字节
invoke CloseHandle, hKeyFile
ret
_HomeProc endp
_HijackedProc proc
invoke MessageBox, NULL, offset szGoodboy, offset szMessCap, MB_ICONINFORMATION
invoke ExitProcess, NULL
ret
_HijackedProc endp
START:
call _HomeProc
invoke MessageBox, NULL, offset szBadboy, offset szMessCap, MB_ICONWARNING
ret
end START
;===============栈溢出利用的例子,由MASM编写===============
乍一看,这个程序一开始只是调用一个子过程_HomeProc,接着就弹出一个消息框
显示szBadboy的内容。再一看_HomeProc这个过程,只是打开一个叫做"thekey.nk"
的文件,并读入一些字节,似乎也没有什么特别之处。
另一方面,整个程序中虽然有一处MessageBox调用显示szGoodboy的内容,但
这个地方在一个_HijackedProc的子程序中,而根据主程序的流程来看,并没有任
何地方调用这个_HijackedProc,于是szGoodboy的内容似乎无法显示出来,你只能
永远做个Bad Boy。果真如此吗?
实际上,妙就妙在_HomeProc当中。这段程序经过masm预处理后,会变成下面
的样子:
;==========================================================
START: call _HomeProc
....
_HomeProc: push ebp
mov ebp, esp
add esp, -8
....
;==========================================================
其中用add esp, -8留出8字节缓冲区loc_Buff,执行完该句后,栈的情形如下图所
示。
loc_Buff只分配了8个字节的空间,但是后来却从thekey.nk文件中读出16个字节放
到以它为起始地址的内存空间,放不下多出来的那8个字节跑到哪里了呢?显然,
这8个字节数据将会覆盖所保存的ebp值(图中绿色部分)和返回地址(图中黄色部
分)。
因此,我们只要控制thekey.nk中覆盖返回地址的那4个字节的内容,就达到了
修改返回地址的目的。等_HomeProc执行到末尾时,ret指令不会发现这个返回地址
被动了手脚,仍然忠实地把它弹出到eip中,我们就到了想去的地方了!
刚才不是还为szGoodboy无法显示而叹息吗,我们现在就来把这个返回地址修
改成_HijackedProc的入口地址,相当于间接调用这个过程。_HijackedProc的入口
地址可以通过反汇编而得到,在笔者机器上,这个地址是0x401053。这也就是说,
构造一个thekey.nk文件,它的前12字节任意,但第13到16字节依次为53 10 40 00
(别忘了Intel x86遵循的是“高高低低”原则),然后把它放到与原程序同一目
录下。好了,现在来看运行结果:
就这样,漏洞利用宣告成功!
谢谢观赏,敬请关注下一节:漏洞利用之格式化字符串