程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,
通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓
冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢
出。下面要谈到的栈溢出就是缓冲区溢出的一种。

  由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。
向这些单元写入任意的数据,一般只会导致程序崩溃之类的事故,对这种情况我们
也至多说这个程序有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遵循的是“高高低低”原则),然后把它放到与原程序同一目
录下。好了,现在来看运行结果:

就这样,漏洞利用宣告成功!

  谢谢观赏,敬请关注下一节:漏洞利用之格式化字符串