缓冲区溢出是一种非常普遍的漏洞,它的原理为输入大量的数据,超出了缓冲区的大小,且系统没有对输入数据的长度进行检查,这样数据可能覆盖了内存的重要区域,例如返回地址等,攻击者只需制作特定的数据,就可以让受攻击的计算机执行指定的代码。为了重现一些经典的缓冲区溢出漏洞,需要关闭 Linux 下针对这方面的一些保护措施,例如:
SSP( Stack Smashing Protector )
SSP 是 gcc 提供的针对栈上缓冲区溢出提供的检查机制。典型的缓冲区溢出攻击会构造输入数据来覆盖缓冲区之外的数据,尤其是函数返回地址。启用 SSP机制后,在编译器生成的代码中,对于那些存在缓冲区的函数,函数开始时会在对应栈帧中压入一个随机值,当函数快要返回时,检查该随机值是否发生了变化,如果发生了变化,就将控制流转移到特定的函数。比较常见为输出以下信息,并退出进程。
1 | *** stack smashing detected ***: <unknown> terminated |
使用以下代码进行说明:
1 |
|
使用 GDB 查看 foo 的反汇编代码:
1 | (gdb) info function |
由上述代码可知,在函数开始,gs:0x14 的值被存储在 ebp - 0xc,在函数返回之前对 ebp - 0xc处的值进行检查,如果和 gs:0x14 不一样,说明发生了溢出,紧接着执行__stack_chk_fail_local 并退出进程。
根据汇编代码,可以知道foo 函数中 buffer 缓冲区的位置为 ebp-0x20。当我们输入的数据长度大于缓冲区的大小时,就可能覆盖缓冲区以外的内容。在函数结束时,就可能被检查出来。
1 | gcc -g -m32 t.c -o t |
在 gcc 中 SSP 是默认开启的。使用以下命令开启和关闭 SSP。日常建议开启,如果需要对简单的缓冲区漏洞进行重现,就需要关闭该选项。
1 | -fstack-protector //编译时启用SSP机制 |
DEP( Data Execution Prevention )
某一类缓冲区溢出攻击通过将攻击指令存储在栈上,然后通过缓冲区溢出,修改函数的返回地址为栈上攻击指令的首地址。这样当函数返回时,函数的执行流就跳转到栈上构造好的指令序列。在现代编译器中,为防止栈中的数据被作为指令执行,使用了 DEP机制,即限制内存的属性。使得栈(可写不可执行),代码段(可执行不可写)。
使用 gcc 编译时,使用 -z execstack 参数来让最终的可执行程序中的栈具有可执行权限。
1 | -z execstack //设置栈内存段具备可执行权限 |
在程序运行时,可通过cat /proc/pid/maps 查看 pid 所对应的进程的内存映射情况,其中包括对进程的段的属性描述。例如:
1 | $ cat /proc/28214/maps |
ASLR( address space layout randomization )
在基本的缓冲区溢出攻击中,最基本的步骤就是定位某些目标的内存映射地址。例如最简单的 shellcode 注入需要在栈上构造特定指令序列,并通过缓冲区溢出使用攻击指令序列地址覆盖函数的返回地址。ret2libc 方法则需要定位内存中的标准库函数的内存映射地址。ASLR ,即内存布局随机化机制由操作系统实现,主要被划分为映像随机化、栈随机化和堆随机化这几类,分别针对程序的加载基地址、栈基址和堆基址进行随机化。
编译好的程序,它的各个段的加载地址是固定的,也就是程序运行时它的各个段的地址是固定的。通过查看 ELF 文件的 Program header table 信息,可以知道需加载入内存的各个段的基地址。通过命令 objdump -h
查看,例如:
1 | objdump -h t |
对于运行中的程序,使用 cat /proc/pid/maps,查看 pid 的内存映射情况,例如:
1 | $ cat /proc/28214/maps |
在过去的系统环境中,程序的.text、.bss、.rodata等段地址在加载入内存时是确定的,程序运行时进程中 stack 和 heap 的起始地址也总是固定的。这样攻击者就更容易定位到特定代码的地址。ALSR 机制后,操作系统会在加载段时在其原始的基地址上加上一个随机值,这样同一程序多次运行,它的内存布局都会不一样,这样攻击者更难实施特定的攻击。
使用命令 cat /proc/sys/kernel/randomize_va_space
查看 ASLR 的设置情况:
- 0: ASLR未启用
- 1:ALSR 机制会随机化 stack、vdso和 mmap 的起始基地址
- 2:对上述目标进行随机化外还会对堆基地址进行随机化。
通过命令 echo 0 > /proc/sys/kernel/randomize_va_space
关闭 ALSR,或设置为其它值。
上述命令为全局生效,且需要 root 权限,使用以下命令只在当前终端中关闭 ALSR。
1 | setarch `uname -m` -R /bin/bash |
通过 GDB 获得目标地址
使用 GDB 可以确定目标代码的地址,例如目标缓冲区的地址。
使用以下代码查看 foo 的汇编代码:
1 | disassemble foo |
由上述代码可知,foo 函数内 buffer 缓冲区的地址为:ebp - 0x1c
1 | (gdb) info function |
由上述信息可知,foo 的地址 和 hacked 的地址。
总结
在 GDB 中看到的地址不一定是真实程序运行的地址,原因多种多样,例如环境变量不一致会导致地址差异。Linux 近年来为了预防缓冲区漏洞的产生,也采取了多种措施,不只是上述提到的三种。在增强了 Linux 安全性的同时,也大大提高了缓冲区溢出攻击的门槛。