概述
缓冲区溢出中比较典型的一类是堆栈溢出漏洞。下面以 x86 为例,讲述基础堆栈溢出原理及其利用。
X86 堆栈溢出原理
函数调用约定(X86 cdecl 规范)
- 参数从右向左依次压入堆栈.
- 由调用者实现堆栈平衡。
- 返回值存放于 EAX 寄存器中。
下面的内容将遵循该规范。
关于堆栈
一个程序要运行,首先会被加载到内存空间,下图为进程的地址空间分布。(图片来自网络)
代码段存储了用户程序的所有可执行代码,程序的全局变量或静态变量放在数据段,局部变量放在栈中,手动申请的对象则存放在堆中。
来重点看下程序是怎么实现函数调用的。
每个函数在堆栈内都会有一段自己的空间,又称函数的栈帧,用来保存现场及函数中的局部变量。
1 | func1(param_1){ |
举个例子。当 func2 即将被调用时,会有如下操作:
- 从右向左依次将 func2 的参数压入堆栈.
- 保存下一条指令的内存地址,即函数的返回地址。当被调用函数 func2 执行完毕时,程序将从该地址继续执行。
- 开辟新的栈帧。
之后,PC 寄存器指向 func2 的函数入口。
在 func2 函数内部:
- 将 EBP 压入堆栈。
- 保存现场(将相关寄存器压入堆栈)
- 函数执行完毕后,恢复相关寄存器的值。
- POP 堆栈中的返回地址到 PC。
程序从返回地址处继续执行,成功完成函数调用。
堆栈溢出示例
1 |
|
编译,运行(为了便于理解,暂时关闭栈保护)。
可以看到,当输入过长的字符串时,程序会报段错误异常退出。
GDB 调试
下面我们使用 gdb 对程序进行调试。推荐 GDB 插件:https://github.com/hugsy/gef
func 的反汇编代码如下。(使用 IDA 截图是为了好看…)
根据调用 strcpy()
函数的前两条指令可得参数 buf 的内存地址为 [ebp-0x12]
。
所以,当我们输入超过 0x12
字节,就可以覆盖 EBP 和函数返回地址。
如上图所示,我们已经成功劫持程序指针 EIP。
堆栈溢出漏洞利用
ShellCode 注入
ShellCode 是一串使用机器语言编写的漏洞利用代码,一般短小精悍。通过堆栈溢出成功劫持 PC 后,我们可以控制 PC 跳转到提前部署在堆栈中的 ShellCode 处,进而任意代码执行。该方法需要程序的堆栈空间有可执行权限。
下面是 linux X86 平台上的一段通过中断方式使用 execve
系统调用执行 /bin/sh
的 ShellCode 代码,只有 23 个字节。
1 | \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" |
execve()
为内核级系统调用,linux x86 下中断编号 0x80。其函数原型为:
int execve(const char filename,char const argv[ ],char * const envp[ ]);
第一个参数是执行文件的路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针结束,最后一个参数则为传递给执行文件的新环境变量数组。
上述 ShellCode 的汇编代码如下:
1 | 0x00000000: xor eax, eax # eax 置零(NULL) |
构造如下输入,保证程序能跳转到 ShellCode 区域。
1 | # 0x12 padding + 0x04 ebp + 0x04 ShellCode 起始地址 + nop 缓冲区 + ShellCode 代码 |
调试环境中利用结果如下,成功由堆栈溢出到本地命令执行。
总结
基于堆栈的典型缓冲区溢出的利用过程整体来看是比较简单的,缓冲区溢出漏洞的利用方式很多,ShellCode 也只是其中一种利用方式。而且为了便于讲解,本文关闭了缓冲区溢出缓解措施。
参考链接
[1] X86 ShellCode
http://shell-storm.org/shellcode/files/shellcode-827.php