最近结合软件安全课程上学习的理论知识和网络资料,对缓冲区溢出漏洞的简单原理和利用技巧进行了一定的了解。这里主要记录笔者通过简单的示例程序实现缓冲区溢出漏洞利用的步骤,按由简至繁的顺序,依次描述简单的 shellcode、ret2libc、ROP、Hijack GOT 等缓冲区溢出攻击技术的原理和步骤,以供总结和分享。为了保证缓冲区溢出实践能够顺利进行,需要对编译器选项和操作系统环境进行设置,可参见笔者博客使用Linux进行缓冲区溢出实验的配置记录。同时,针对使用 gdb 动态调试获得的程序局部变量地址较之程序直接运行时的地址可能存在差异的问题,可参见笔者博客针对 Linux 环境下 gdb 动态调试获取的局部变量地址与直接运行程序时不一致问题的解决方案。
1.shellcode
1.1 示例程序
(1)示例程序如图所示,程序的主要功能为接受用户输入的字符串,之后显示“end of main”后结束运行。
1 #include<stdio.h> 2 #define buf_size 64 3 4 void double_print() 5 { 6 char buf[ buf_size ]; 7 8 gets( buf ); 9 } 10 11 int main( int argc , char *argv[] , char *envp[] ) 12 { 13 double_print(); 14 15 printf("end of main!\n"); 16 }
(2)编译得到名为 hello 的可执行程序。
gcc -m32 -g -fno-stack-protector -z execstack -o hello hello.c //编译生成可执行文件hello
运行结果如图所示
注:在进行基础缓冲区溢出实验时,需对实验环境进行设置以去除某些编译器和操作系统设置的保护机制,包括通过 -fno-stack-protecotor 关闭 SSP 机制,-z execstack 使得栈可执行,关闭 ALSR 等,具体原理可以参见使用Linux进行缓冲区溢出实验的配置记录。
1.2 shellcode注入
根据程序源码分析,这里的 double_print 函数结束后,会返回至 main 函数执行,并输出“end of main”字符串。在读取输入时,源程序并没有对输入数据长度进行检查,则可通过构造输入,将 double_print 的返回地址覆盖,改变源程序的执行流程。
(1)构造所需的 shellcode,构造数据时需保证 shellcode 中不包含 '\n' 。因为 gets 会在读取到换行符或 EOF 后停止字符串的读取,从而造成字符串输入的截断。
一个可行的 shellcode 如下左图所示,其对应的十六进制表示如右图所示,其作用为启动一个shell。( 来源:Shellcoding for Linux and Windows Tutorial )
上述 shellcode 共有55个字节。
(2)通过 gdb hello 启动 gdb ,通过 disas double_print 查看 hello 程序中 double_print 函数的反汇编指令,获得缓冲区数组 buf 的实际大小,可知 buf 数组的起始地址为 %ebp - 0x48,也就是说, buf 的起始地址距返回地址的长度为 0x48 + 4 = 76 bytes,也就是构造的 shellcode 应该为 shellcode + 填充 + 指向shellcode的地址 的格式,其中 shellcode + 填充 长度为 76 字节。
(3)通过 gdb hello 动态调试 hello 程序。注意,使用 gdb 动态调试获得的程序的局部变量的地址与程序直接运行时的地址可能有所不同,解决方法见针对 Linux 环境下 gdb 动态调试获取的局部变量地址与直接运行程序时不一致问题的解决方案。这里笔者使用的方法是不传递环境变量数组给运行的 hello 程序,这样其通过 gdb 运行或直接运行时进程栈上的结构会保持一致。
set exec-wrapper env - //gdb 中设置不传递环境变量给调试程序 env - hello的完整路径 //直接运行时,同样设置 hello 程序不从 shell 中继承环境变量。使用 hello 的完整路径是为了使 argv[0] 的内容保持一致
在 double_print 函数中下断点,运行程序,查看缓冲数组 buf 的起始位置为 0xffffddf0,该地址即为用于覆盖原返回地址的新地址值。
(4)构造完整的输入数据,数据格式为 shellcode( 55 bytes ) + 填充字节( 21 bytes ) + 新的返回地址( 4 bytes ,小端法存放 ),将上述十六进制形式的输入数据存放在名为 shellcode 的文件中。
(5)通过以下指令执行注入过程。
./hex2raw < shellcode > shellcode.bin //将 shellcode 文件中存放的数据转换为二进制数据,存放在shellcode.bin文件中 { cat shellcode.bin ; cat - ; } | env - hello程序完整路径 //将输入注入至 hello 程序的缓冲区,这里使用{cat - ;}是为了保证输入流的持续
//使得打开的 shell 不会随输入流的结束而结束
其中 hex2raw 为 csapp 的 buffer lab 中提供的一个将十六进制表示的数据转换为二进制流的工具,可将之前构造的十六进制 shellcode 转换为二进制形式输入。使用 env - path_to_hello 的方式是为了保证调试状态下获得的局部变量地址与程序直接运行时的地址保持一致。
运行结果如图所示:
1.3 总结
通过简单的构造注入数据,覆盖函数返回地址的方式,可以使用 shellcode 实现一定的效果。但在现实的操作系统环境下意图直接实现 shellcode 注入是十分困难的,在实验过程中进行了若干处的理想化处理:
1.在编译过程中使用了特殊的编译选项,关闭了栈保护和栈不可执行的保护,从而使得能够 shellcode 中的代码能够直接执行;
2.使用了一个不安全的 c 标准函数 gets,实际上在编译时编译器会警告最好不使用 gets 函数;
进行基础的 shellcode 实验时,需要通过编译器和操作系统的设置取消众多已存在的保护手段以便实验的进行。而在后续的实验中,则会进一步的构造输入数据,从而使得构造的输入能够绕过某些安全机制的防护,达到攻击目的。
2.ret2libc
ret2libc的核心思想是把函数的返回地址直接指向系统某个已存在的函数,而在栈上则构造所需的参数格式,这样函数在返回时会直跳转至系统函数执行,从而绕过 DEP 保护机制,实现攻击效果。一般可以选用 system、execve、mprotect 等函数作为指向目标。
使用 ret2libc 技术主要需要两个方面的准备:(1)获得所需要的系统函数如 system 和参数字符串的地址;(2)构造栈上的数据,使得 system 函数能够正常执行;
2.1 示例程序
这里同样通过 shellcode 中使用的实例程序进行说明。使用 gcc -m32 -fno-stack-protector -g -o hello hello.c 生成可执行文件 hello,此时 hello 的栈数据是不可执行的。
gcc -m32 -fno-stack-protector -g -o hello hello.c //生成可执行文件 hello,其栈不可执行
2.2 ret2libc注入过程
典型的函数调用发生时的栈栈结构如下图所示,函数调用发生时,调用者将返回地址 ret 入栈,之后控制权转移至被调用函数。被调函数首先将 %ebp 的值保存入栈,随后即可进行其本身的函数操作,被调者可以通过 %ebp + 8 、 %ebp + 12 访问调用者压入栈中的参数的值。当函数调用返回时,被调用者会将保存的 %ebp 恢复,之后通过 ret 指令将返回地址 ret 的值赋值给 EIP,正常情况下,此时控制流跳转回调用者,由其对栈上的参数进行清除等操作。
借助缓冲区溢出手段,我们可以构造输入数据,使得其将返回地址 ret 的值覆盖,修改为系统中已存在的 system 函数的值,则函数返回时控制流会跳转至 system 函数,system 函数会将寄存器 %ebp 保存,之后其通过 %ebp + 8 获得所需的参数,而在函数返回时,其会认为位于保存的 %ebp 之后的栈顶数据为函数调用的返回地址。通过构造数据,可以将原函数调用的 ret 修改为目标函数地址,之后放置我们所需的返回地址( 比如 exit 函数的地址),并根据正常的函数调用栈结构构造参数列表。
可通过 gdb 动态调试获得构造输入所需要的地址。使用如下指令进行 gdb 调试。
gdb hello //启动 gdb set exec-wrapper env -u COLUMNS -u LINES -u _ //设置忽略某些环境变量,这里没有选择直接忽略所有的环境变量,因为在后续过程中使用了环境变量SHELL的内容 r //开始运行程序
设置完成后即可对 hello 程序进行动态调试,获得的运行时局部变量的地址与通过命令 env -u _ /home/yh/sc/hello 直接运行 hello 程序时保持一致。
(1)通过 p system 和 p exit 命令获得进程中已存在的系统函数 system 和 exit 的地址;
(2)通过程序的环境变量 SHELL 获得参数字符串"/bin/bash"的地址,通过 gdb 的 x 命令查看环境变量字符串。
可知环境变量字符串的首地址为 0xffffd05c。
通过 gdb 的 x 命令查看得到环境变量 SHELL 的起始地址为 0xffffd356,则字符串 /bin/bash 的起始地址为 0xffffd35c.
(3) 通过上述获得的信息,即可构造输入数据,为 填充数据( 76字节 ) + system函数地址( 4字节,小端法 ) + exit函数地址( 4字节,小端法 ) + 参数字符串地址( 4字节,小端法)。构造好的数据格式如图所示。
(4) 通过构造的数据对目标程序 hello 进行注入。
./hex2raw < shellcode_ret2libc > shellcode.bin //将 shellcode_ret2libc 中的数据转化为二进制形式并保存在文件 shellcode.bin 中 { cat shellcode.bin ; cat - ; } | env -u _ hello程序完整路径 //对 hello 程序进行注入
运行结果如图所示,可以看到在栈不可执行的环境下成功打开了一个 shell。
2.3 总结
相对而言,ret2libc 方法的实践效果较之简单的 shellcode 注入的方法要更好,其可以成功的绕过栈不可执行的保护,使用系统中已均在的函数实现攻击效果。但 ret2libc 方法的实践有一定的局限性,其主要通过栈上数据进行参数构造,这对于以寄存器传参的 x86_64 体系是无效的。同时,与 shellcode 一样,ret2libc 方法也没有克服 SSP 机制和 ALSR 机制,想要在实际环境中使用还需要进一步的处理。
参考资料:
1.Shellcoding for Linux and Windows Tutorial