在基础的软件安全实验中,缓冲区溢出是一个基础而又经典的问题。最基本的缓冲区溢出即通过合理的构造输入数据,使得输入数据量超过原始缓冲区的大小,从而覆盖数据输入缓冲区之外的数据,达到诸如修改函数返回地址等目的。但随着操作系统和编译器针对缓冲区溢出问题引入防护机制,初学者想要由简入繁的学习和实践缓冲区溢出的原理变得困难。在 Linux 环境下,用户可以通过设置编译和系统环境来去除某些防护措施,从而方便的完成某些简单的缓冲区溢出实验。
1.关闭SSP( Stack Smashing Protector )
实现原理
SSP 是 gcc 提供的针对栈上缓冲区溢出提供检查的机制。典型的缓冲区溢出攻击会通过构造输入数据覆盖缓冲区外的数据,实现一定的溢出效果,如修改函数返回地址等。启用 SSP 机制后,在编译器生成的代码中,对于那些存在缓冲区的函数,函数开始时会在对应栈帧中入栈一个随机值( canary ),在函数调用结束准备返回时,编译器生成的代码会检查上述引入的随机值是否发生了变化,若发生变化则说明出现了缓冲区溢出,控制流会转移至对应的处理函数处。
以下列代码为例进行说明
void simpleCopy( char *src )
{
char buff[10];
strcpy( buff , src );
}
上述代码将缓冲区 src 中的数据简单的拷贝至 buff 数组中,而没有考虑 src 中的数据量是否超出了 buff 数组的容纳能力。
使用 gdb 查看得到 simpleCopy 的汇编如图所示。
在开启了 SSP 机制的 gcc 生成的代码中,函数开始数据处理之前,将 gs:0x14 处的值存放在了 %ebp - 12 处,在函数返回时,对 %ebp - 12 处的值进行检查,若此时值不等于 gs:0x14 处的值,则说明存在数据的覆盖和修改,程序会转入 __stack_chk_fail 执行。通过 simpleCopy("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")调用函数的结果如下图所示。上述 simpleCopy 函数中,缓冲区 buff 首地址为 %ebp - 22 , 而引入的 canary 存放在 %ebp - 12 处,当缓冲区存在数据溢出时,会修改 canary 的值,从而在函数返回时被检测到。
选项设置
gcc的编译设置中,默认设置SSP机制是有效状态,用户可在编译时加入 -fno-stack-protector 参数禁用SSP机制。
-fstack-protector //编译时启用SSP机制 -fno-stack-protector //编译时禁用SSP机制
2.关闭DEP( Data Execution Prevention )
在某些栈缓冲区数据溢出实践中,会将构造好的机器指令序列通过输入写入位于栈上的输入缓冲区中,同时,构造数据覆盖位于数据缓冲区外的函数调用的返回地址,将其值修改为缓冲区中构造好的机器指令序列的地址,这样当函数调用返回时,程序控制流会跳转至输入缓冲区中的构造指令序列,从而实现执行流的修改和劫持。而现代编译器中,为防止用户通过栈上的缓冲区构造数据,使用了DEP机制,即限制内存的属性,使得某些可写内存不可执行( 如栈 )或可执行内存不可写( 如.text段),从而达到防护的效果。
使用 gcc 进行编译时,可通过 -z execstack 参数,使得最终生成的程序中栈内存段具备可执行权限。
-z execstack //设置栈内存段具备可执行权限
在程序运行时,可通过查看 cat /proc/pid/maps 文件查看 pid 所对应的进程的内存映射情况,其中包括对进程的段的属性描述。
3.关闭ASLR( address space layout randomization )
在基础栈缓冲区溢出实践中,一个重要的步骤为定位某些目标的内存映射地址,如最简单的 shellcode 需定位注入栈上的构造指令序列的地址( 从而修改函数返回地址指向该指令序列),ret2libc 方法则需要攻击者定位位于内存中的标准库函数的内存映射地址等。ASLR ,即内存布局随机化机制由操作系统实现,其主要被划分为映像随机化、栈随机化和堆随机化这几类,分别针对程序的加载基地址、栈基址和堆基址进行随机化。
设计原理
已经编译完成的程序的各个段在内存中的加载基位地址是固定的,也就是说程序运行时的内存映射情况是固定的,攻击者可以通过多次调试运行程序,获得他们所需的目标地址。在编译好的 ELF 文件中,段头部表描述了程序需加载入内存的各个段的基地址,可通过 readelf -h filename 对其进行查看。对于运行中的程序,可以通过 cat /proc/pid/maps 查看 pid 对应的进程的内存映射情况,包括栈、堆的基地址等。
objdump -h hello //查看可执行文件hello的段头部表 cat /proc/pid/maps //pid为进程号,查看pid对应进程的内存映射情况
(1)通过 objdump -h hello 查看该 ELF 文件的.text 加载基地址为 0x08048370,而.rodata段的加载基地址为 0x08048558。
(2)通过 cat /proc/$$/maps 查看 shell 的内存映射情况(64位Ubuntu),可以看到 shell 进程的 stack 段的内存范围以及其属性为可读可写但不可执行,p 表示该虚拟内存段为私有的( private ),s 表示该虚拟内存段为共享的( shared )。
在过去的编译环境中,程序的数据段包括.text、.bss、.rodata 段加载进内存的基地址在编译时即已确定,程序运行时进程中 stack 和 heap 的起始地址也总是固定的,这就使得攻击者可以较容易的定位内存中目标的地址,从而进行攻击尝试。在引入 ALSR 机制后,对于程序的某个特定的段( .text 、stack 、heap ),操作系统会在加载时自其原始的起始基地址处加入一个随机大小的“填充”,对应的程序数据总是自“填充”区域后开始,由于每次使用的“填充”的大小并不固定,同一程序多次运行时的内存布局会发生变化,从而使得攻击者较难通过固定的地址去访问其所需要的目标。
选项设置
用户可通过cat /proc/sys/kernel/randomize_va_space 查看当前 ALSR 机制的运行情况。其中,为 0 表示 ALSR 机制未启用,为 1 表示 ALSR 机制会随机化 stack、vdso和 mmap 的起始基地址,为 2 表示除对上述目标进行随机化外还会对堆基地址进行随机化。用户可通过 echo 0 > /proc/sys/kernel/randomize_va_space 设置关闭 ALSR。
cat /proc/sys/kernel/randomize_va_space //查看 ALSR 机制的运行情况 echo 0 > /proc/sys/kernel/randomize_va_space //设置关闭 ALSR 机制,需要 root 用户进行操作
上述对 /proc/sys/kernel/randomize_va_space 的操作为针对系统的全局设置,需要 root 权限且存在弊端,用户可通过以下命令将当前终端 /bin/bash 的 ALSR 机制关闭,则通过该 shell 运行的程序均不会启动 ALSR 机制。该终端关闭后上述设置即失效。
setarch `uname -m` -R /bin/bash
4.通过 gdb 获得目标地址
基础的缓冲区溢出实践通常需要确定运行状态下程序中的某些地址,如需要确定输入缓冲区的起始地址从而获得注入缓冲区中的机器指令的地址等。在 Linux 环境下,可通过 gdb 对程序进行动态调试,从而获得程序运行状态下的信息( 关闭 ALSR 机制后效果更好 ),基础的 gdb 操作可参见笔者的文章Linux下编辑、编译、调试命令总结——gcc和gdb描述。使用 gdb 可以方便的获取程序动态运行状态下的信息,但通过 gdb 动态调试获取的诸如缓冲区的起始地址等信息可能与程序实际运行时的信息并不相同,从而影响缓冲区溢出实践的效果。关于保证通过 gdb 动态调试程序获得的局部变量的地址与直接运行该程序时的地址一致的问题,笔者另外通过博文针对 Linux 环境下 gdb 动态调试获取的局部变量地址与直接运行程序时不一致问题的解决方案进行描述。
5.总结
为了完成基础的缓冲区溢出实践,用户可通过 gcc 的编译选项 -fno-stack-protector 关闭 SSP 保护机制,通过 -z execstack 选项使得生成的可执行程序的栈可执行,通过 setarch `uname -m` -R /bin/bash 设置关闭当前终端中运行程序的 ALSR 机制,并通过 gdb 动态调试获取某些所需要的程序局部变量的信息。
6.参考资料
1.Stack Smashing Protector - OSDev Wiki
2.protect against buffer overflow exploits
3.Address space layout randomization - Wiki