栈迁移原理分析
栈迁移
背景
由于栈地址的随机化,有时利用缓冲区溢出进行多次ROP时不方便覆盖prev rbp位置的地址(假如覆盖的返回地址为函数中间,不包括push rbp; mov rbp,rsp;部分);或者更常遇到的是,我们输入的内容很多,然而真正溢出的部分很少,难以进行ROP的情况。我们希望用一种办法,使栈迁移至已知地址处,这就是栈迁移(stack pivot)攻击技术。
原理
在一个函数的结尾,通常有
1 | leave |
指令,这是为了恢复之前保存的rbp寄存器,同时返回至原先的指令位置。其中leave相当于
1 | mov rsp, rbp |
这是很自然的,一个函数中,rbp常常不变(因为要作为栈底指针,便于寻找变量对应的内存位置),而rsp可能会改变(比如c99标准中引入的可变长数组特性)。无论被调用函数将栈开辟到哪个位置(即无论rsp指针指向哪里),leave都能将栈指针指向栈底,并且将栈底保存的原先的rbp地址pop给rbp,此时rsp+=8,rsp指向返回地址,再执行ret指令,返回至原先的位置。
那么考虑构造ROP链,使得连续执行两次leave,将栈安排如下:
1 | [fake stack] <- prev rbp |
使用pop rip代替ret,用mov rsp,rbp;pop rbp;代替leave,可以如此展示程序流程
1 | mov rsp,rbp |
由此,我们实现了对栈指针的劫持。
实战中,我们常将栈迁移至bss段后的位置,或堆上的位置。因为linux系统分配内存时,一个内存页的大小至少为0x1000,而bss段经常没有这么长,所以可以将栈劫持到(bss&~(0xfff))+0x900之类的位置上。
实践
VNCTF2023 traveler
程序中有system,直接两次栈迁移构造ROP
1 | from pwn import * |