32位与64位ROP构造差异

前言

做pwn的时候遇到这个疑惑,为什么32位和64位的ROP写起来不一样?

ROP

什么是ROP

ROP的全称为Return-oriented programming(返回导向编程),也可以理解为面向返回地址的编程,这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。通过上一篇文章栈溢出,我们可以发现栈溢出的控制点是ret处,那么ROP的核心思想就是利用以ret结尾的指令序列把栈中的应该返回EIP的地址更改成我们需要的值,从而控制程序的执行流程。

为什么要ROP

探究原因之前,我们先看一下什么是NX(DEP) NX即No-execute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。所以就有了各种绕过办法,ROP就是一种。

32位

32位程序的栈溢出,只有输入的字节填满栈空间,和返回地址,即可劫持栈帧,达到控制执行流的效果,

假设栈开辟了20字节的空间,rbp为8字节,由此可得payload如下

python
1
2
payload = b'A'*(0x20+0x8) + p32(system_addr) + p32(bin_addr)

64位

寄存器传参

64位和32位程序的不同点之一就是,它的前6个参数是通过寄存器传递的,有更多的参数才用栈,所以构造rop链的方式和32位不同。

分别是rdirsirdxrcxr8r9作为第1-6个参数。rax作为返回值 64位没有栈帧的指针,32位用ebp作为栈帧指针,64位取消了这个设定,所以rbp作为通用寄存器使用。

堆栈平衡

还有一点要考虑的就是堆栈平衡,那么问题来了,什么是堆栈平衡。

  1. 如何要返回父程序,则当我们在堆栈中进行堆栈的操作时候,一定要保证在RET这条指令之前,ESP指向的是我们压入栈中的地址
  2. 如果通过堆栈传递了参数了,那么在函数执行完毕后,要平衡参数导致的堆栈变化

人话就是,当函数在一步步执行的时候 一直到ret执行之前,堆栈栈顶的地址 一定要是call指令的下一个地址。

也就是说函数执行前一直到函数执行结束,函数里面的堆栈是要保持不变的。

如果堆栈变化了,那么,要在ret执行前将堆栈恢复成原来的样子。

构造payload

64位还要考虑堆栈平衡,由此可以有3种payload写法。

清空rdi寄存器,ret维持堆栈平衡,将GOT表地址和PLT表地址弹到rdi寄存器,在通过libc搜索得到libc版本

python
1
2
# 泄露libc地址
payload1=padding+p64(pop_rdi_ret) + p64(puts_got_addr) + p64(puts_plt_addr)

塞满栈空间和返回地址,维持堆栈平衡,清空rdi寄存器获取偏移地址,塞入/bin/sh字符和system函数

python
1
2
# 命令执行
payload = 'a' * (0x0f+0x08) + p64(ret_addr) + p64(pop_rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
python
1
2
3
# 寄存器调用顺序:rdi、rsi、rdx、rcx、r8、r9
# 先利用pop+ret将bin_sh_addr地址弹到rdi寄存器中去,再调用system函数来执行
payload = 'a' * (0x0f+0x08) + p64(pop_rdi_addr) + p64(bin_sh_addr) + p64(system_addr)

前两个payload利用64位程序函数调用参数规则构造

python
1
2
// ret堆栈平衡,直接返回bin_sh_addr处也可以获得系统权限。
payload = 'a' * (0x0f+0x08) + p64(ret_addr) + p64(bin_sh_addr)