选择从栈溢出开始学习Shellcode的编写,是因为在没有保护机制(栈Cookie,ASLR,DEP,SafeSEH)的系统中使用栈溢出是一件很简单的事情。栈区随着函数调用动态变化,每个函数调用时在栈上占用的空间称为栈帧。用一个示例来说明栈上保存的内容及动态变化的过程。
下面是一个程序,生成一个对话框显示一条“Hello World!”消息。下面是该程序的C代码:
在VS2008中用Debug版编译之后,拖入Immunity Debugger中:
图1 example_1.exe入口点
与用汇编直接编写的程序不同,VS在真正进入main函数之前加入了很多其它的代码,入口点停在了一条跳转指令上(如图1),因此,需要找到真实的main函数。调试别的软件时由于不知道具体细节,需要跟踪判断以及一些特殊方法。但由于是我们自己编写的程序,就不用了,直接“bp MessageBoxA”在MessageBoxA函数下断点,然后使用回溯法,即可快速找到main函数。下面是真实的main函数:
图2 真正的main函数入口
这里可以看到调用MessageBoxA函数。
我们要跟踪函数MessageBoxA的调用过程,在004113C0的PUSH 0指令处下断点。Windows API函数的调用方式为stdcall,即参数从右向左入栈,被调用者负责清栈。MessageBoxA函数来自于user32.dll,函数原型如下:
/*********************************************************/
int WINAPI MessageBoxA(
_In_opt_ HWND hWnd,
_In_opt_ LPCTSTR lpText,
_In_opt_ LPCTSTR lpCaption,
_In_ UINT uType
);
/*********************************************************/
示例example_1中调用所给的参数如下:
/*********************************************************/
MessageBox( NULL, "HelloWorld", "example_1", MB_OK );
/*********************************************************/
从在004113C0的PUSH 0开始的四条PUSH语句,就是参数的入栈过程。
执行PUSH 0之前,先记一下当前栈的内容:
图3
下面执行四条PUSH语句,直到CALL处停住,看栈的内容:
图4
看到参数从右向左都已入栈。
下面是CALL语句,按F7跟踪进函数MessageBoxA,注意栈的变化:
图5
执行PUSH EBP之前,看当前栈:
图6
栈上多了一个值:004113D4,这个值正是CALL MessageBoxA下一条语句的地址(见图2),这正是CALL指令的作用。将下一条指令(返回地址)入栈,然后跳转到函数执行。这个地址是栈溢出利用的重点,因为它在函数返回的时刻将被放入EIP,作为指令地址。
下面接着执行MessageBoxA函数中的下一条指令PUSH EBP。下面两条指令:
/*********************************************************/
PUSH EBP
MOV EBP, ESP
/*********************************************************/
标志着函数栈帧的建立,也就是说,函数现在开始接手栈,它开始圈出自己的一块地(开辟新的栈基址EBP和栈范围ESP),使用这一块地来保存自己的局部变量,保存自己的信息。函数RET 之前还有POP EBP,表示函数自己的栈帧使用完了,可以清理掉了。因此,在调用函数的过程中,栈内容分布如下:
图7
函数返回的过程不再跟踪。下面看图7,开始说栈溢出的内容。
前面说到函数在栈上开辟自己的栈帧,用来保存自己的变量,正常情况下,函数应该使用它自己圈出来的地(栈帧),而不应该越界,因为一旦它越界,就会破坏调用者保存在栈上的内容(返回地址,参数,以及调用者的局部变量等)。调用者所保存的这些内容中有一个最重要的内容:返回地址,因为它控制着程序的流程,一旦它被破坏,程序就不再受调用者控制(再也回不到调用者指令流程中了)。因此,一旦有栈溢出漏洞,黑客们就可能利用它来控制程序流程,做坏事。