--修改返回地址,让其指向内存中已有的一段指令
要完成的任务包括:在内存中确定某段指令的地址,并用其覆盖返回地址。可是既然可以覆盖返回地址并定位到内存地址,为什么不直接用上篇提到的 return2libc呢?因为有时目标函数在内存内无法找到,有时目标操作并没有特定的函数可以完美适配。这时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。本方法用于绕过NX保护机制
假如要执行某段指令(我们将其称为“gadget”,意为小工具),溢出数据应该以下面的方式构造(padding 长度和内容的确定方式参见上篇):
payload : padding + address of gadget
如果想连续执行若干段指令,就需要每个 gadget 执行完毕可以将控制权交给下一个 gadget。所以 gadget 的最后一步应该是 RET 指令,这样程序的控制权(eip)才能得到切换,所以这种技术被称为返回导向编程(ReturnOrientedProgramming )。
要执行多个 gadget,溢出数据应该以下面的方式构造:payload :padding + address of gadget 1 + address of gadget 2 + ......+ address of gadget n
现在任务可以分解为:针对程序栈溢出所要实现的效果,找到若干段以 ret 作为结束的指令片段,按照上述的构造将它们的地址填充到溢出数据中。所以我们要解决以下几个问题。
首先,栈溢出之后要实现什么效果?
ROP 常见的拼凑效果是实现一次系统调用,Linux系统下对应的汇编指令是 int 0x80。
其次,如何寻找对应的指令片段?
有若干开源工具可以实现搜索以 ret 结尾的指令片段,著名的包括ROPgadget、rp++、ropeme等,甚至也可用 grep 等文本匹配工具在汇编指令中搜索 ret 再进一步筛选。
最后,如何传入系统调用的参数?
对于32位的程序, 我们可以直接将参数作为gadget片段, 用栈来传入. 如果有系统级函数system等, 可以通过此方法直接调用, 来获取权限.
对于64位的程序, 需要用寄存器传参. 可以用 pop 指令将栈顶数据弹入寄存器, 这需要在溢出数据内包含这些参数,所以上面的溢出数据格式需要一点修改。对于单个 gadget,pop 所传输的数据应该在 gadget 地址之后,如下图所示。
如果没有系统级函数, 可以去libc 动态链接库中找(用IDA), 还可以通过系统调用来调用mprotect函数为栈开启可执行权限, 之后我们希望执行一段 shellcode,所以要将 shellcode 也加入溢出数据. 但确定 shellcode 在内存的确切地址是很困难的事(想起上篇里面艰难试探的过程了吗?),我们可以使用 push esp 这个 gadget(假如可以找到的话)。
对于所有包含 pop 指令的 gadget,在其地址之后都要添加 pop 的传输数据,同时在所有 gadget 最后包含一段 shellcode,最终溢出数据结构应该变为如下格式。
payload :padding + address of gadget 1 + param for gadget 1 + address of gadget 2 + param for gadget 2 + ...... + address of gadget n + shellcode
出于演示的目的,我们假设了所有需要的 gadget 的存在。在实际搜索及拼接 gadget 时,并不会像上面一样顺利,有两个方面需要注意。
第一,很多时候并不能一次凑齐全部的理想指令片段,这时就要通过数据地址的偏移、寄存器之间的数据传输等方法来“曲线救国”。
第二,要小心 gadget 是否会破坏前面各个 gadget 已经实现的部分,比如可能修改某个已经写入数值的寄存器。另外,要特别小心 gadget 对 ebp 和 esp 的操作,因为它们的变化会改变返回地址的位置,进而使后续的 gadget 无法执行。
最后声明一下 此文章仅为摘抄笔记不是原创