0%

栈溢出与ROP分析与利用

基础知识#

栈溢出与ROP#

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。栈溢出漏洞就是由于栈溢出而导致的漏洞。在程序执行过程中,常使用栈帧记录程序执行过程中的状态,在栈帧中保存着返回地址,当利用栈溢出漏洞修改函数返回地址跳转到非预期地址执行时,就产生了ROP攻击,又称返回导向编程。

程序调用栈#

栈帧是记录程序执行过程中状态的结构。在cdecl调用约定中,当程序P调用Q时,会出现以下行为:

  1. P将Q需要的参数保存到栈或者约定寄存器中,再将Q的返回地址存入栈;
  2. Q在栈中保存当前esp/rsp寄存器中的值,再将ebp/rbp寄存器中值赋值给esp/rsp寄存器形成栈帧;
  3. Q申请的局部变量需要的栈空间;
  4. 当Q执行完毕,使用栈中保存的esp/rsp值恢复给esp/rsp寄存器;
  5. 从栈中取出返回地址,跳转回P中继续执行;

正是因为存在程序调用栈,当出现栈溢出漏洞时,我们能够对返回地址进行劫持,修改程序默认执行路径,达到自己的目的。

常见保护机制#

在栈溢出漏洞攻防博弈中,程序的保护机制十分重要。对白客来说,通过添加程序保护机制,能够提高漏洞利用的门槛,一定程度上防止程序漏洞被利用;而对于黑客来说,熟悉程序保护机制,能够在漏洞利用过程中少走弯路,提高漏洞利用的效率。

图2.1所示是使用checksec工具检查linux系统中sh二进制文件的结果。可以看到它启用了RELRO、Canary、NX、PIE和FORTIFY保护。其中Canary、NX和PIE这三种在实际利用中较为常见且有用。其中Canary 和NX在Windows系统中也有类似的保护机制。

img

图2.1 linux下sh中存在的保护机制

NX与DEP#

NX是No eXcute的缩写,意为不可执行保护,在Windows系统中为DEP保护。其根本原理如图2.2所示,黑客需要利用漏洞劫持返回地址到某个位置执行,而这一保护将区域置为不可执行,当跳转到该区域内时,检测到异常,触发异常处理并退出程序,使得劫持失效。

img

图2.2 NX/DEP原理

Canary与GS#

Linux下的Canary保护对应Windows下的GS保护。利用栈溢出是线性连续的覆盖栈内的数据这一特性,在返回地址前插入一个随机的不可预测值,并在函数返回时检查是否被修改,如果被修改,则一定产生了栈溢出,此时会退出程序执行。如图2.3所示,是x86下的Canary保护时栈内布局示意图。

img

图2.3 Canary/GS保护栈布局图

PIE与ASLR#

PIE是编译过程中的选项,是位置独立的可执行区域的意思。当操作系统开启ASLR(内存地址随机化)时,会打乱二进制文件加载的基址,使得返回地址随机,即使动态调试中EXP通过,但远程攻击时也会由于地址随机化机制失效。操作系统中ASLR存在3个可选项,如下:

  • 值为0:无随机化,堆栈地址每次都相同
  • 值为1:随机化出了堆基址以外的所有加载基址
  • 值为2:随机化所有加载基址(包括堆)

ROP分析与利用#

ROP,又称返回导向编程,利用程序指令集中存在的ret指令,改变指令流的执行顺序。其利用条件为:

  1. 程序存在溢出且能够控制返回地址;
  2. 可以找到满足条件的gadgets和gadgets地址。

基本的分类如下:ret2text、ret2shellcode、ret2syscall、ret2libc。

注:以下分析与利用都为32位程序,示例二进制文件来源于ctfwiki

ret2text#

ret2text是指返回到程序中text段已有的代码中执行。

分析与利用步骤: 以附件中的ret2text程序为例

  1. checksec查看保护结果如图3.1.1所示,能够看到没有开启保护。

img

图3.1.1 checksec查看ret2text结果
2. 使用IDA查看ret2text main函数如图3.1.2所示,能够看到存在栈溢出漏洞,由于gets函数未判断读入长度,能使用读入的字符串写入s覆盖到main函数返回地址。

img

图3.1.2 ret2text main函数
3. 在secure函数中寻找到system(“/bin/sh”),如图3.1.3

img

图3.1.3 secure函数
4. 通过输入对栈进行布局。由于未开启保护,我们能直接找到调用system(“/bin/sh”)的指令地址system_binsh_addr,因此我们通过栈溢出布置栈布局如图3.1.4所示,栈溢出首先需要覆盖旧的ebp值,进而覆盖到返回地址,将其修改为system_binsh_addr的地址即可。

img

图3.1.4 ret2text栈布局
5. 最后我们需要确定填入的padding长度,使得正好溢出到我们需要的位置,通过gdb和其插件动态调试可以获得,如图3.1.5调试信息可知,padding长度为108。

img

图3.1.5 gdb调试padding长度
6. 确定输入,由以上信息我们就能构造最后的输入,为108*’A’+p32(0xdeadbeaf)+p32(system_binsh_addr)。

ret2shellcode#

shellcode是能让黑客获得shell的16进制机器码,当text段没有能获取shell的代码时,就需要我们自己想办法将shellcode放入内存中了。各种条件下的shellcode可以参考shellstorm获得。

img

图3.2.1 16进制shellcode
图3.2.1是一段16进制shellcode,对应的汇编代码如图3.2.2 所示。可以看到是将字符串/bin/sh\x00存入,再通过系统调用好获取shell。

img

图3.2.2 shellcode汇编代码
分析与利用步骤:以附件中的ret2shellcode文件为例
  1. checksec查看程序保护机制,如图3.2.3所示,没有开启。

img

图3.2.3 ret2shellcode 保护机制
2. 使用IDA查看main函数如图3.2.4所示,可以看到gets存在栈溢出问题,并且将s内容放到了.bss段的buf2内。由于.bss段具有可执行权限,所以我们可以将shellcode输入存在buf2中,再修改返回地址到buf2处执行。

img

图3.2.4 ret2shellcode main函数
3. 通过输入对栈布局如图3.2.5所示,由于存在拷贝,会将shellcode拷贝到buf2,再跳转到buf2执行shellcode即可。

img

图3.2.5 ret2shellcode栈布局
4. 同ret2text一样确定padding长度为108。
  1. 构造输入为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。

img

图3.3.1 execve获取shell的汇编代码
分析与利用步骤:以附件中的retsyscall程序为例
  1. checksec查看保护如图3.3.2 所示,未开启保护。

img

图3.3.2 ret2syscall保护机制
2. IDA打开查看main函数如图3.3.3所示,存在栈溢出。

img

图3.3.3 ret2syscall main函数
3. 通过ROPgadgets工具能找到修改寄存器值,int 0x80,/bin/sh的gadgets地址如图3.3.4所示。

img

图3.3.4 ret2syscall gadgets
4. 通过gadgets和栈溢出,就能构造栈布局进行利用了,如图3.3.5所示。

img

图3.3.5 ret2syscall 栈布局
当函数返回时,首先会利用两个pop的gadgets及栈中数据,修改eax、ebx、ecx、edx寄存器的值,最后执行int 0x80,就能获得shell了。
  1. 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文件为例

  1. checksec查看ret2libc1保护如图3.4.1所示,开启了NX保护。

img

图3.4.1 ret2libc1 保护机制
2. IDA查看main函数,可以看到存在栈溢出漏洞,如图3.4.2所示。

img

图3.4.2 ret2libc1 main函数
3. 使用ROPgadget工具,在程序中能够找到/bin/sh字符串地址binsh_address,且在程序的plt表中能够找到system函数地址system_plt_address,即为第一类。
  1. 通过栈溢出布局,由于我们知道字符串地址和system在plt表中地址,所以不需要泄露libc基址。如图3.4.3所示,覆盖返回地址为system_plt_address,并放入参数为/bin/sh地址即可。
  2. 同前,padding长度为108,构造输入为:

’A’*108+p32(0xdeadbeaf)+p32(system_plt_address)+p32(0xdeadbeaf)+p32(binsh_address)

img

图3.4.3 ret2libc1 栈布局

类型2:没有”/bin/sh”,有system函数#

分析与利用步骤:以附件中ret2libc2文件为例

  1. checksec查看保护机制,如图3.4.4所示,只开启了NX保护

img

图3.4.4 ret2libc2 保护机制
2. IDA打开查看main函数,如图3.4.5所示,同样存在栈溢出漏洞。与ret2libc1不同的是,ret2libc2中不存在/bin/sh字符串,但是我们能在.bss段找到一个全局的buf2数组,长度为0x64。

img

图3.4.5 ret2libc2 main函数
3. 由于plt表中存在gets函数地址gets_plt和system函数地址system_plt,.bss段buf2数组地址为buf2_addr。因此利用可以分为两步,首先构造执行gets(buf2_addr)将/bin/sh字符串写入全局buf2中,再执行system(buf2_addr)达到目的。
  1. 经过上述分析,可以对栈布局如图3.4.6所示,在这里要注意,由于在程序执行中gets有一个参数,因此需要将其pop出去才能继续执行。

img

图3.4.6 ret2libc2 栈布局
5. 同样padding长度仍然为108,最后可以构造输入如下:

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保护

img

图3.4.7 ret2libc3保护机制
2. IDA打开查看main函数,如图3.4.8所示,存在栈溢出。存在gets和puts两个libc.so中的函数

img

图3.4.8 ret2libc3 main函数
3. 根据之前所说的利用思路,可以想到以下利用步骤:
  • 第一次ROP:利用puts函数泄露__libc_start_main函数地址

  • 通过低12位查找libc.so版本

  • 得到实际的system函数和/bin/sh地址

  • 第二次ROP:执行system(“/bin/sh”)

这里我们需要让main函数执行两次,这样才能进行两次ROP

  1. 第一次ROP栈布局如图3.4.9所示,返回时,先调用puts函数输出__libc_start_main函数地址,然后会进入第二次main函数的执行,得到了第二次ROP的机会

img

图3.4.9 ret2libc3 第一次ROP栈布局
5. 通过泄露的__libc_start_main函数地址,可以得到system函数地址和/bin/sh地址,从而第二次ROP栈布局如图3.4.10所示,执行system(“/bin/sh”) 。

img

图3.4.10 ret2libc3 第二次ROP栈布局
在通过低12位查询libc.so版本时可以利用[libc版本数据库](https://github.com/niklasb/libc-database), 以及其对于python的查询封装[LibcSearcher](https://github.com/lieanu/LibcSearcher)。
坚持原创技术分享,您的支持将鼓励我继续创作!