内容纲要

ROP — 修改返回地址 , 让其指向内存中已有的一段指令

该类攻击方法的主要任务是 : 在内存中确定某段指令的地址 , 并用它覆盖返回地址 .

前面提到了 Return2libc 这种攻击方式 , 它同样可以定位到指定内存地址并覆盖返回地址 . 但很多时候目标函数无法在内存中找到 , 目标操作没有完美适配的特定函数 , 这时就需要在内存中寻找多个指定片段 , 拼凑出一段指令来完成目标操作( 这种攻击方式被称为 " gadget " , 意为小工具 ).

根据上述理论 , 可以构造出如下的 Payload 结构

  • 包含单个 gadget 的溢出数据

    png

     padding : 作为任意填充数据 , 要注意其中不要包含 " \x00 " , 否则会截断后面的内容 . 长度应该刚好覆盖 callee 的基地址 . 同样通过不断填充数据覆盖函数返回地址 , 引发程序报错来拿到 padding 的具体长度
    
     Address of gadget : 想要执行的指令片段地址
    

    因此 , 完整的 Payload 结构应该如下所示

    Payload : padding + address of gadget

  • 包含多个 gadget 的溢出数据

    很多情况下会将内存中多个指令片段拼接起来使用 , 因此你想要执行的指令片段肯定不止一个

    想要连续执行多个 gadget 指令片段 , 就需要每个 gadget 执行完毕后可以将程序控制权交给下一个 gadget , 因此 gadget 的最后一步肯定是 RET 指令 , 这样程序的控制权( 前面说过控制程序执行指令最关键的寄存器是 EIP( 扩展指令寄存器 ) 寄存器 )才可以得到切换 . 这种技术就是 Return Oriented Programming( 返回导向编程 )

    连续执行多个 gadget , 溢出数据的结构应该如下所示

    png

     在这样的构造中 , callee 返回时跳转到 gadget1 , 当该指令片段执行完毕后 , gadget1 的 RET 指令会将此时的栈顶数据( 也就是gadget2 的地址 )弹出至 EIP , 程序继续去执行 gadget2 , 以此类推 , 最终程序会执行完所有的 gadget
    

    因此 , 完整的 Payload 结构应该如下所示

    Payload : padding + address of gadget1 + address of gadget2 + ... + address of gadget3

那么现在的任务及变成了 : 在内存中找出若干以 RET 作为结束的指令片段 , 按照上述的结构将它们的地址填充进栈中 , 从而实现程序栈溢出 .

  • 需要解决的问题
    • 栈溢出后要实现什么效果

      ROP常见的拼凑效果是实现一次系统调用, 在Linux系统下对应的汇编指令为 : int $0x80( 这条指令比较重要 , 我会另外说明 ) , 执行这条指令时 , 调用函数的编号应该存入 eax , 调用参数按照顺序存入 ebx , ecx , edx , esi , edi 中

      这里举个例子 , 编号125对应函数 mprotest( const void *start , size_t len , int prot ) , 那我们可以用该函数将栈的属性改为可执行 , 这样就可以执行 ShellCode 了

       mprotect() : 在Linux系统中 , 修改一段指定内存区域的保护属性 . 具体来说 , 将从 start 开始 , 长度为 len 的内存区保护属性修改为 prot 指定的值
      
       start 和 len 有一定限制 , 因为指定的内存区间必须包含整个内存页( 4K ) , 因此区间的开始地址 start 必须是一个内存页的起始地址 , 并且区间长度 len 必须是页大小的整数倍
      
       prot 可以取下面几个值 , 并且可以用 " | " 将多个属性联合起来使用
       1. PROT_READ : 表示内存段内的内容可写
       2. PROT_WRITE : 表示内存段内的内容可读
       3. PROT_EXEC : 表示内存段内的内容可执行
       4. PROT_NONE : 表示内存段内的内容无法被访问
      

      因此 , 如果想要利用系统调用执行这个函数 , eax 需要被设置为 " 125 " , ebx 需要被设置为内存栈的分段地址( 通过调试工具确定 ) , ecx : 需要被设置为" 0x10000 "( 这个长度根据需求而变化 ) , edx被设置为 " 7 "( RWX权限 )

    • 如何寻找对应的指令片段
      很多开源工具都可以搜索以 RET 结尾的指令片段( 比如 ROPgadget , rp++ , ropeme , ... ) , 或者直接用 grep 等文本匹配工具在汇编指令中搜索 RET 指令进一步筛选.

    • 如何传入系统调用的参数
      类似于上面提到的 mprotect() 函数 , 调用它时会将其参数传输至寄存器 . 这里可以用 pop 指令将栈顶的数据弹入寄存器中 . 如果在内存中可以找到能用的数据 , 也可以用 mov 指令来进行传输 .

      不过向栈中写入数据然后 pop 到指定寄存器肯定会比搜索需要的数据再 mov 到指定寄存器简单 , 那么如果想要用 pop 指令来传输调用参数 , 就需要在溢出数据中包含这些参数 . 因此这里溢出数据的格式又会发生变化 , 对于使用单个 gadget , 在 address of gadget 之后应该还要添加 pop 指令传输的数据 , 如下图所示

      png

      当为栈开启可执行权限后 , 就可以执行 shellcode 了 . 因此要将 shellcode 也放入溢出数据中 , 并将 shellcode 的开始地址放到 int $0x80 的gadget后 .

      确定 shellcode 在内存中的确切地址是很复杂的( 可以参考 ShellCode 方法中是如何通过 NOP 试探出 shellcode 的起始地址的 ) , 找到地址后可以使用 push esp 这个 gadget 将 shellcode 的地址压入栈中 , 具体的执行过程如下所示

      png

      举个例子 , 在内存中可以找到下面这些指令

       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 , 最终的溢出数据结构如下所示

      png

    • 完整的 Payload 构建( 为了演示简单 , 假设溢出数据不受 " \x00 " 截断影响 )

      1. Payload 需要包含函数的对应编号 , 比如125 ( \x7d\x00\x00\x00 ) , 然后将这个参数传输给eax

        当然在真实环境中 , 需要用多个 gadget 通过运算得到这个参数( 125 ) , 比如可以通过下面的语句向 eax 传递参数

         pop eax; ret;    # pop stack top 0x1111118e into eax
         pop ebx; ret;    # pop stack top 0x11111111 into ebx
         sub eax,ebx; ret;     # eax = eax - ebx = 0x1111118e - 0x11111111 = 0x01111101 = 125
        
      2. 然后可以拼接溢出数据 , 为程序调用栈开启可执行权限并执行 shellcode .

        因为 ROP 方法的灵活 , 现在不用去试探 shellcode 的起始地址了~ 因为对于整个输入数据 , 只需要确定具体的栈分段地址( 注意 mprotect() 的 start 和 len 参数 ) , 如果利用 gadget 读取 ebp 的值再加上合适的数值 , 就可以保证溢出数据都具有可执行权限 , 而不用去获取具体地址 , 也就有了绕过 ASLR 的可能性 .

      为了演示简单 , 上述过程假设了所有需要的 gadget 都存在且可以被找到 . 在实际搜索及拼接 gadget 的时候 , 不可能像上述过程那么顺利 , 有两个方面需要注意

      • 很多时候不可能一次性凑齐全部的理想指令片段 , 往往要通过数据地址的偏移 , 寄存器之间的数据传输来间接拿到指令

        假设找不到 pop ebx; ret; 这段指令 , 但可以找到 mov ebv,eax; ret;pop eax; ret; 这两条指令 , 那么可以将这两条指令组合起来以达到将数据传输给 ebx 的功能

      • 要小心 gadget 是否会破坏前面各个 gadget 已经实现的部分

        比如可能修改某个已经写入数值的寄存器 . 特别要小心 gadget 对 ebp 和 esp 的操作 , 因为它们的变化会改变返回地址的位置 , 进而使得后续的 gadget 无法执行 .


总结

上述内容就是 ROP 方法的概念 . 不过要深刻理解它还需要一些具体实例 , 之后我会举例说明

最后修改日期:2019年9月17日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。