栈溢出与ROP分析与利用
基础知识#
栈溢出与ROP#
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。栈溢出漏洞就是由于栈溢出而导致的漏洞。在程序执行过程中,常使用栈帧记录程序执行过程中的状态,在栈帧中保存着返回地址,当利用栈溢出漏洞修改函数返回地址跳转到非预期地址执行时,就产生了ROP攻击,又称返回导向编程。
程序调用栈#
栈帧是记录程序执行过程中状态的结构。在cdecl调用约定中,当程序P调用Q时,会出现以下行为:
- P将Q需要的参数保存到栈或者约定寄存器中,再将Q的返回地址存入栈;
- Q在栈中保存当前esp/rsp寄存器中的值,再将ebp/rbp寄存器中值赋值给esp/rsp寄存器形成栈帧;
- Q申请的局部变量需要的栈空间;
- 当Q执行完毕,使用栈中保存的esp/rsp值恢复给esp/rsp寄存器;
- 从栈中取出返回地址,跳转回P中继续执行;
正是因为存在程序调用栈,当出现栈溢出漏洞时,我们能够对返回地址进行劫持,修改程序默认执行路径,达到自己的目的。
常见保护机制#
在栈溢出漏洞攻防博弈中,程序的保护机制十分重要。对白客来说,通过添加程序保护机制,能够提高漏洞利用的门槛,一定程度上防止程序漏洞被利用;而对于黑客来说,熟悉程序保护机制,能够在漏洞利用过程中少走弯路,提高漏洞利用的效率。
图2.1所示是使用checksec工具检查linux系统中sh二进制文件的结果。可以看到它启用了RELRO、Canary、NX、PIE和FORTIFY保护。其中Canary、NX和PIE这三种在实际利用中较为常见且有用。其中Canary 和NX在Windows系统中也有类似的保护机制。
NX与DEP#
NX是No eXcute的缩写,意为不可执行保护,在Windows系统中为DEP保护。其根本原理如图2.2所示,黑客需要利用漏洞劫持返回地址到某个位置执行,而这一保护将区域置为不可执行,当跳转到该区域内时,检测到异常,触发异常处理并退出程序,使得劫持失效。
Canary与GS#
Linux下的Canary保护对应Windows下的GS保护。利用栈溢出是线性连续的覆盖栈内的数据这一特性,在返回地址前插入一个随机的不可预测值,并在函数返回时检查是否被修改,如果被修改,则一定产生了栈溢出,此时会退出程序执行。如图2.3所示,是x86下的Canary保护时栈内布局示意图。
PIE与ASLR#
PIE是编译过程中的选项,是位置独立的可执行区域的意思。当操作系统开启ASLR(内存地址随机化)时,会打乱二进制文件加载的基址,使得返回地址随机,即使动态调试中EXP通过,但远程攻击时也会由于地址随机化机制失效。操作系统中ASLR存在3个可选项,如下:
- 值为0:无随机化,堆栈地址每次都相同
- 值为1:随机化出了堆基址以外的所有加载基址
- 值为2:随机化所有加载基址(包括堆)
ROP分析与利用#
ROP,又称返回导向编程,利用程序指令集中存在的ret指令,改变指令流的执行顺序。其利用条件为:
- 程序存在溢出且能够控制返回地址;
- 可以找到满足条件的gadgets和gadgets地址。
基本的分类如下:ret2text、ret2shellcode、ret2syscall、ret2libc。
注:以下分析与利用都为32位程序,示例二进制文件来源于ctfwiki。
ret2text#
ret2text是指返回到程序中text段已有的代码中执行。
分析与利用步骤: 以附件中的ret2text程序为例
- checksec查看保护结果如图3.1.1所示,能够看到没有开启保护。
ret2shellcode#
shellcode是能让黑客获得shell的16进制机器码,当text段没有能获取shell的代码时,就需要我们自己想办法将shellcode放入内存中了。各种条件下的shellcode可以参考shellstorm获得。
- checksec查看程序保护机制,如图3.2.3所示,没有开启。
- 构造输入为shellcode+(108-len(shellcode))+p32(0xdeadbeaf)+p32(buf2)即可。
ret2syscall#
ret2syscall是指返回到系统调用执行。例如执行execve(‘/bin/sh’,0,0)获取shell。通过系统调用号,来调用系统函数,不使用libc中的函数,更加底层。在利用过程中,首先通过栈溢出和gadgets将寄存器置为需要的值,再使用int 0x80进行系统调用。
图3.3.1所示是execve(‘/bin/sh‘,0,0)对应汇编代码。可以看到从栈中取出了参数放入对应的寄存器,寄存器edx置0,ecx置0,ebx置为’/bin/sh’地址,在eax中存入系统调用号0xb。
- checksec查看保护如图3.3.2 所示,未开启保护。
- padding长度依旧为108,构造输入为
108*’A’+p32(0xdeadbeaf)+p32(pop_eax_ret)+p32(0xb)+p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(binsh_address)+p32(int_0x80_addr)即可。
ret2libc#
ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,会选择执行 system("/bin/sh"),故而需要知道 system 函数的地址。可以分为以下三种情况:
- 类型1:有”/bin/sh”,有system函数
- 类型2:没有”/bin/sh”,有system函数
- 类型3:没有”/bin/sh”,没有system函数
类型1:有”/bin/sh”,有system函数#
分析与利用步骤:以附件中的ret2libc1文件为例
- checksec查看ret2libc1保护如图3.4.1所示,开启了NX保护。
- 通过栈溢出布局,由于我们知道字符串地址和system在plt表中地址,所以不需要泄露libc基址。如图3.4.3所示,覆盖返回地址为system_plt_address,并放入参数为/bin/sh地址即可。
- 同前,padding长度为108,构造输入为:
’A’*108+p32(0xdeadbeaf)+p32(system_plt_address)+p32(0xdeadbeaf)+p32(binsh_address)
类型2:没有”/bin/sh”,有system函数#
分析与利用步骤:以附件中ret2libc2文件为例
- checksec查看保护机制,如图3.4.4所示,只开启了NX保护
- 经过上述分析,可以对栈布局如图3.4.6所示,在这里要注意,由于在程序执行中gets有一个参数,因此需要将其pop出去才能继续执行。
108*’A’+p32(0xdeadbeaf)+p32(gets_plt)+p32(pop_ebx_ret)+p32(buf2_addr)+p32(system_plt)+p32(0xdeadbeaf)+p32(buf2_addr)
类型3:没有”/bin/sh”,没有system函数#
这一类的漏洞利用思路如下:
system函数属于
libc.so
,在libc.so中的相对偏移是固定的即使开启了ASRL保护,也不会改变加载地址的低12位
可以通过GOT表泄露已执行过的libc.so中的函数的地址
通过泄露的低12位找到libc.so版本,得到system函数和/bin/sh地址
ROP利用
分析与利用步骤:以附件中的ret2libc3文件为例
1.checksec查看ret2libc3保护机制,如图3.4.7所示,只开启了NX保护
第一次ROP:利用puts函数泄露__libc_start_main函数地址
通过低12位查找libc.so版本
得到实际的system函数和/bin/sh地址
第二次ROP:执行system(“/bin/sh”)
这里我们需要让main函数执行两次,这样才能进行两次ROP
- 第一次ROP栈布局如图3.4.9所示,返回时,先调用puts函数输出__libc_start_main函数地址,然后会进入第二次main函数的执行,得到了第二次ROP的机会