ROP

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)

      ROP是一种攻击技术,其中攻击者使用堆栈的控制来在现有程序代码中的子程序中的返回指令之前,立即间接地执行精心挑选的指令或机器指令组。

      因为所有执行的指令来自原始程序内的可执行存储器区域,所以这避免了直接代码注入的麻烦,并绕过了用来阻止来自用户控制的存储器的指令的执行的大多数安全措施。

      因此,ROP技术是可以用来绕过现有的程序内部内存的保护机制的。

      ROP要完成的任务包括要完成的任务包括:在内存中确定某段指令的地址,并用其覆盖返回地址。有时目标函数在内存内无法找到,有时目标操作并没有特定的函数可以完美适配,此时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。假如要执行某段指令(我们将其称为“gadget”,意为小工具),溢出数据应该以下面的方式构造:

      payload : padding + address of gadget

 

 

  上图是包括单个gadget的溢出

      如果想连续执行若干段指令,就需要每个 gadget 执行完毕可以将控制权交给下一个 gadget。所以 gadget 的最后一步应该是 RET 指令,这样程序的控制权(eip)才能得到切换,所以这种技术被称为返回导向编程( Return OrientedProgramming )。要执行多个 gadget,溢出数据应该以下面的方式构造:

      payload : padding + address of gadget 1 +address of gadget 2 + ...... + address of gadget n

      在这样的构造下,被调用函数返回时会跳转执行 gadget 1,执行完毕时 gadget 1 的 RET 指令会将此时的栈顶数据(也就是 gadget 2 的地址)弹出至 eip,程序继续跳转执行gadget 2,以此类推。

 

 

   上图是包含多个gadget的溢出数据

      现在任务可以分解为:针对程序栈溢出所要实现的效果,找到若干段以 ret 作为结束的指令片段,按照上述的构造将它们的地址填充到溢出数据中。所以我们要解决以下几个问题。

 

首先,栈溢出之后要实现什么效果?

 

      ROP 常见的拼凑效果是实现一次系统调用,Linux系统下对应的汇编指令是int 0x80。执行这条指令时,被调用函数的编号应存入 eax,调用参数应按顺序存入ebx,ecx,edx,esi,edi 中。例如,编号125对应函数

 

      mprotect (void *addr, size_t len, int prot)

 

      可用该函数将栈的属性改为可执行,这样就可以使用 shellcode 了。假如我们想利用系统调用执行这个函数,eax、ebx、ecx、edx 应该分别为“125”、内存栈的分段地址(可以通过调试工具确定)、“0x10000”(需要修改的空间长度,也许需要更长)、“7”(RWX 权限)。

 

      其次,如何寻找对应的指令片段?

 

      有若干开源工具可以实现搜索以ret 结尾的指令片段,著名的包括ROPgadget、rp++、ropeme 等,甚至也可以用 grep 等文本匹配工具在汇编指令中搜索 ret 再进一步筛选。

 

      最后,如何传入系统调用的参数?

 

      对于上面提到的mprotect 函数,我们需要将参数传输至寄存器,所以可以用 pop 指令将栈顶数据弹入寄存器。如果在内存中能找到直接可用的数据,也可以用 mov 指令来进行传输,不过写入数据再 pop 要比先搜索再 mov 来的简单,对吧?如果要用 pop 指令来传输调用参数,就需要在溢出数据内包含这些参数,所以上面的溢出数据格式需要一点修改。对于单个 gadget,pop 所传输的数据应该在gadget 地址之后,如下图所示。

 

 

上图是以gadget“pop eax; ret;”为例

      在调用 mprotect()为栈开启可执行权限之后,我们希望执行一段 shellcode,所以要将 shellcode 也加入溢出数据,并将 shellcode 的开始地址加到 int 0x80 的 gadget之后。我们可以使用 push esp 这个 gadget。

 

 

  我们假设现在内存中可以找到如下几条指令:

      pop eax; ret;    # pop stack top into eax

      pop ebx; ret;    # pop stack top into ebx

      pop ecx; ret;    # pop stack top into ecx

      pop edx; ret;    # pop stack top into edx

      int 0x80; ret;   # system call

      push esp; ret;   # push address of shellcode

      对于所有包含 pop 指令的 gadget,在其地址之后都要添加 pop 的传输数据,同时在所有 gadget 最后包含一段 shellcode,最终溢出数据结构应该变为如下格式。

      payload : padding + address of gadget 1 +param for gadget 1 + address of gadget 2 + param for gadget 2 + ...... + addressof gadget n + shellcode

 

 

  此处为了简单,先假定输入溢出数据不受“\x00"字符的影响,所以 payload 可以直接包含 “\x7d\x00\x00\x00”(传给 eax 的参数125)。如果希望实现更为真实的操作,可以用多个 gadget 通过运算得到上述参数。比如可以通过下面三条 gadget 来给 eax 传递参数。

      pop eax; ret;         # pop stack top 0x1111118e into eax

      pop ebx; ret;         # pop stack top 0x11111111 into ebx

      sub eax, ebx; ret;    # eax -= ebx

      解决完上述问题,我们就可以拼接出溢出数据,输入至程序来为程序调用栈开启可执行权限并执行 shellcode。

      出于简单化考虑,我们假设了所有需要的 gadget 的存在。在实际搜索及拼接 gadget 时,并不会像上面一样顺利,有两个方面需要注意。

      第一,很多时候并不能一次凑齐全部的理想指令片段,这时就要通过数据地址的偏移、寄存器之间的数据传输等方法来“曲线救国”。举个例子,假设找不到下面这条 gadget

      pop ebx; ret;

      但假如可以找到下面的gadget

      mov ebx, eax; ret;

      我们就可以将它和

      pop eax; ret;

      组合起来实现将数据传输给ebx 的功能。上面提到的用多个gadget 避免输入“\x00”也是一个实例应用。

      第二,要小心 gadget 是否会破坏前面各个 gadget 已经实现的部分,比如可能修改某个已经写入数值的寄存器。另外,要特别小心gadget 对 ebp 和 esp 的操作,因为它们的变化会改变返回地址的位置,进而使后续的 gadget 无法执行。

 

 

 

 

 

 

 

posted @ 2019-12-04 20:02  阿沓  阅读(741)  评论(0编辑  收藏  举报