Metasploit Frameworkの便利ツールを使いこなそう!
はじめに
こんにちは! 👋 この記事では、ペネトレーションテストや脆弱性分析において非常に強力なツールキットである Metasploit Framework に含まれるユーティリティの一つ、msf-pattern_offset
について詳しく解説していきます。特に、ソフトウェアの脆弱性の中でも古典的かつ依然として重要な「バッファオーバーフロー」の解析において、このツールがどのように役立つのかを具体的に見ていきます。
Metasploit Framework は、Rapid7社によって開発・維持されているオープンソースのペネトレーションテストフレームワークです。脆弱性の発見、エクスプロイトコードの開発・実行、ペイロードの生成など、セキュリティテストに必要な多くの機能を提供しています。Kali Linuxなどのセキュリティ診断用OSには標準で搭載されていることが多いです。
バッファオーバーフローは、プログラムが用意したメモリ領域(バッファ)よりも大きなデータを書き込もうとすることで発生し、最悪の場合、攻撃者によってプログラムの制御を奪われてしまう可能性がある脆弱性です。この脆弱性を悪用する(エクスプロイトする)ためには、プログラムの実行フローを乗っ取るために重要な制御情報(例えば、EIP/RIPレジスタの値)が、入力データのどの位置(オフセット)によって上書きされるかを正確に特定する必要があります。
msf-pattern_offset
は、まさにこの「オフセットの特定」を助けてくれるツールなのです。事前に msf-pattern_create
という別のツールで生成したユニークなパターン文字列を脆弱なプログラムに送り込み、クラッシュ時に特定のレジスタに書き込まれた値を確認することで、その値がパターン文字列のどの位置に対応するのかを計算してくれます。
この記事を読むことで、以下の点が理解できるようになります。
- バッファオーバーフローとオフセット特定の基本的な考え方
msf-pattern_create
でユニークなパターンを生成する方法msf-pattern_offset
を使ってオフセットを特定する具体的な手順- オフセット特定後の次のステップについての簡単な紹介
- ツール利用時の注意点
それでは、バッファオーバーフロー解析の重要なステップであるオフセット特定の世界を探求していきましょう! 🚀
バッファオーバーフローとオフセットの重要性 🤔
msf-pattern_offset
の使い方を理解する前に、なぜバッファオーバーフロー攻撃において「オフセット」が重要なのかをもう少し詳しく見ていきましょう。
バッファオーバーフローの仕組み (おさらい)
プログラムが外部からの入力を受け付ける際、そのデータを一時的に保存するためにメモリ上に「バッファ」と呼ばれる領域を確保します。例えば、ユーザー名やパスワード、ファイルパスなどを格納するためです。
#include <string.h>
#include <stdio.h>
void vulnerable_function(char *input) {
char buffer[100]; // 100バイトのバッファを確保
strcpy(buffer, input); // 入力文字列をバッファにコピー (サイズチェックなし!)
printf("Input: %s\n", buffer);
}
int main(int argc, char **argv) {
if (argc > 1) {
vulnerable_function(argv[1]);
} else {
printf("Usage: %s <input>\n", argv[0]);
}
return 0;
}
上記のC言語のコード例では、vulnerable_function
内で100バイトの buffer
がスタック上に確保されています。しかし、strcpy
関数は入力文字列 input
の長さをチェックせずに buffer
へコピーしようとします。もし input
が100バイトを超える長さだった場合、buffer
の境界を越えてデータが書き込まれてしまいます。これがバッファオーバーフローです。
スタックとEIP/RIPレジスタ
関数が呼び出される際、その関数が終了した後に処理を戻すべき場所(リターンアドレス)や、関数内で使われるローカル変数などが「スタック」と呼ばれるメモリ領域に積まれていきます。上記の例では、buffer
変数もスタック上に確保されます。
重要なのは、多くの場合、ローカル変数用のバッファのすぐ近く(メモリのアドレス的に隣接する場所)に関数のリターンアドレスが格納されているということです。
(高位アドレス)
…
関数の引数
リターンアドレス ← 関数終了後にここに戻る
古いフレームポインタ (EBP/RBP)
ローカル変数 (例:
buffer
)…
(低位アドレス)
CPUには、次に実行すべき命令が格納されているメモリアドレスを指し示す特別なレジスタがあります。x86アーキテクチャではEIP (Extended Instruction Pointer)、x64アーキテクチャではRIP (Instruction Pointer) と呼ばれます。関数から戻る際には、スタック上に保存されたリターンアドレスがこのEIP/RIPレジスタに読み込まれ、プログラムはそのアドレスから実行を再開します。
オフセット特定がなぜ必要か?
バッファオーバーフローによって buffer
の境界を越えてデータが書き込まれると、その近くにあるリターンアドレスまで上書きしてしまう可能性があります。もし攻撃者が、リターンアドレスを自分が用意した悪意のあるコード(シェルコードなど)が配置されているメモリアドレスに書き換えることができれば、関数が終了する際にプログラムの制御を奪うことができます。
しかし、攻撃を成功させるためには、入力データのうち、正確にどの部分がリターンアドレスを上書きするのかを知る必要があります。つまり、入力データの先頭から何バイト目にリターンアドレスを書き換えるためのデータ(悪意のあるコードのアドレス)を配置すればよいか、その「オフセット」を特定する必要があるのです。
入力データが [AAAA...][BBBB][CCCC...]
のような構造になっている場合、A
の部分でバッファを埋め尽くし、B
の部分でリターンアドレスを上書きし、C
の部分にシェルコードを配置する、といった戦略が考えられます。このとき、A
の部分の正確な長さ(=オフセット)を知ることが極めて重要になります。
ここで登場するのが msf-pattern_create
と msf-pattern_offset
です! 🎉
msf-pattern_create との連携 🤝
msf-pattern_offset
は単独で使われることは少なく、通常は msf-pattern_create
というツールとセットで利用されます。まずは msf-pattern_create
の役割を見ていきましょう。
ユニークなパターン文字列の生成
オフセットを特定するためには、まず脆弱性が疑われるプログラムに、長くて「ユニークな」パターンを持つ文字列を入力として与える必要があります。なぜユニークでなければならないのでしょうか?
もし単純に大量の “A” (AAAA...
) を入力した場合、プログラムがクラッシュしてEIP/RIPレジスタが 0x41414141
(ASCIIコードで “A” は0x41) になったとしても、入力した多数の “A” のうち、どの4つの “A” がEIP/RIPを上書きしたのか区別がつきません。
そこで msf-pattern_create
を使います。このツールは、指定した長さの、周期的でかつ部分文字列が一意になるような特殊なパターン文字列を生成してくれます。
使い方
基本的な使い方は非常にシンプルです。
# 書式: msf-pattern_create -l <生成する文字列の長さ>
msf-pattern_create -l 200
例えば、長さ200バイトのパターンを生成すると、以下のような出力が得られます(出力は実行ごとに同じです)。
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
この文字列は、例えば “Aa0A” という4バイトの並びはこの文字列中の先頭にしか現れず、”a1Aa” という並びも特定の場所にしか現れない、という性質を持っています。これにより、クラッシュ時にEIP/RIPに入っていた値が、このパターン文字列のどの部分に対応するのかを一意に特定できるのです。
一般的には、ファジング(大量のデータを送り込んでクラッシュするポイントを探す手法)などで判明した、プログラムがクラッシュするおおよそのデータ長よりも少し長めのパターンを生成します。例えば、1000バイトあたりでクラッシュすることが分かっていれば、1200バイトや1500バイトのパターンを生成すると良いでしょう。
生成したパターンの利用
msf-pattern_create
で生成したパターン文字列を、脆弱性が疑われるプログラムの入力として与えます。これは、コマンドライン引数、ネットワーク経由での送信、ファイル入力など、プログラムがデータを受け付ける方法に合わせて行います。
# 例: コマンドライン引数としてパターンを与える
./vulnerable_program $(msf-pattern_create -l 500)
# 例: Pythonスクリプトでネットワーク経由でパターンを送信
import socket
pattern = b"Aa0Aa1Aa2..." # msf-pattern_createで生成したパターン
target_ip = "192.168.1.100"
target_port = 1337
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(b"COMMAND " + pattern + b"\r\n") # プロトコルに合わせて送信
s.close()
このパターン文字列を入力として受け取ったプログラムがバッファオーバーフローを起こしてクラッシュした場合、デバッガ (GDB, Immunity Debugger, WinDbg, x64dbg など) を使ってクラッシュ時のCPUレジスタの状態を確認します。特に注目するのは EIP (または RIP) レジスタの値です。
もしEIPレジスタに 0x31704130
(“0Ap1” のリトルエンディアン) のような値が入っていたら、これは msf-pattern_create
が生成したパターンの一部である可能性が高いです。この値こそが、オフセットを特定するための鍵となります🔑。
msf-pattern_offset の使い方 🎯
さて、いよいよ本題の msf-pattern_offset
です。msf-pattern_create
で生成したパターンを送り込み、デバッガでクラッシュ時のEIP/RIPレジスタの値を確認したら、その値を使ってオフセットを計算します。
基本的なコマンド構文
msf-pattern_offset
の基本的な使い方は以下の通りです。
# 書式: msf-pattern_offset -q <EIP/RIPの値> [-l <パターン長>]
msf-pattern_offset -q <検索したい値> -l <msf-pattern_createで生成した長さ>
-q, --query
: 必須オプション。デバッガで確認したEIP/RIPレジスタの値を指定します。値は以下の形式で指定できます。- 16進数:
0xXXXXXXXX
の形式 (例:0x31704130
) - 文字列: レジスタに入っていた値に対応するASCII文字列 (例:
0Ap1
)。多くの場合、リトルエンディアンでメモリに格納されるため、レジスタの16進数値をASCII文字に変換した際の逆順の文字列になることがあります。デバッガの表示を確認するのが確実です。 - 数値: 10進数の数値も指定できますが、通常は16進数か文字列を使います。
- 16進数:
-l, --length
: オプション。msf-pattern_create
で生成したパターンの長さを指定します。指定しない場合のデフォルト値は 8192 バイトですが、msf-pattern_create
で指定した長さと同じ値を指定するのが一般的です。これにより、検索範囲が限定され、より正確な結果が得られます。-s, --sets
: オプション。msf-pattern_create
でカスタム文字セットを使ってパターンを生成した場合に、そのセットを指定します。通常は使用しません。
実行例
例として、msf-pattern_create -l 500
で生成したパターンをプログラムに送り込んだ結果、EIPレジスタの値が 0x35614134
になったとします。このオフセットを特定するには、以下のコマンドを実行します。
msf-pattern_offset -q 0x35614134 -l 500
または、デバッガで値が “4Aa5” という文字列に対応することが分かっていれば、以下のように指定することもできます。
msf-pattern_offset -q 4Aa5 -l 500
注意: 16進数で指定する場合、アーキテクチャのエンディアン(バイトオーダー)に注意が必要です。x86/x64 は通常リトルエンディアンであり、メモリ上では下位バイトから格納されます。例えば文字列 “ABC” (0x41, 0x42, 0x43) をDWORD (4バイト) として書き込む場合、メモリ上では 43 42 41 00
のようになり、レジスタには 0x00414243
のように読み込まれることがあります(実際の値は状況によります)。しかし、msf-pattern_offset
は内部でエンディアンを考慮してくれるため、デバッガで表示されたEIP/RIPの16進数値をそのまま 0x...
形式で指定すれば、多くの場合問題ありません。心配な場合は、文字列形式で指定するか、両方の形式で試してみると良いでしょう。
出力結果の見方
コマンドを実行すると、通常は以下のような形式で結果が出力されます。
[*] Exact match at offset 140
この出力は、「指定された値 (0x35614134
または 4Aa5
) は、長さ500のパターン文字列の先頭から 140 バイト目 から始まる4バイトと完全に一致しました」ということを意味します。
つまり、この例では、入力データの先頭から140バイトのデータを送り込んだ直後に、EIP/RIPレジスタを上書きするための4バイトのデータを配置すれば良い、ということが分かります。✅
もし完全一致する箇所が見つからなかった場合、msf-pattern_offset
は近い候補を表示しようとすることがあります。
[*] No exact matches, looking for likely candidates...
[*] Possible match at offset 138 (adjusted)
[*] Possible match at offset 142 (adjusted)
[*] If necessary, please refer to framework.log for more details.
このような表示が出た場合は、指定した値やパターン長が間違っているか、あるいはスタックの状態が複雑で単純なオフセット計算では特定できない可能性があります。入力値やデバッガでの確認手順を見直す必要があります。
補足: SEH オーバーフローの場合
Windows環境では、スタックオーバーフローによってリターンアドレスではなく、SEH (Structured Exception Handling) チェーンを上書きすることで制御を奪う攻撃手法もあります。SEHレコードもスタック上に配置されるため、同様に msf-pattern_create
と msf-pattern_offset
を使って、SEHハンドラのアドレスが格納されている場所までのオフセットを特定することができます。クラッシュ時にデバッガで上書きされたSEHハンドラのアドレス(パターンの一部になっているはず)を確認し、その値を -q
オプションに指定します。
実践的な例 (シナリオ) 🧑💻
ここまでの説明を元に、架空の脆弱なサーバーアプリケーション “VulnServer” (仮名) の特定コマンドにおけるバッファオーバーフローのオフセットを特定するシナリオを見てみましょう。
-
ファジングと脆弱性の発見:
まず、様々な長さのデータを “VulnServer” の特定のコマンド (例:
CMD1
) に送信するファジングを行います。その結果、約2000バイトのデータを送信したあたりでサーバーがクラッシュすることが判明しました。 -
パターンの生成:
クラッシュする長さ (約2000バイト) よりも少し長い、例えば2500バイトのユニークなパターンを
msf-pattern_create
で生成します。msf-pattern_create -l 2500 > pattern.txt
(生成されたパターンが
pattern.txt
ファイルに保存されます) -
パターンの送信とデバッグ:
ターゲットマシンで “VulnServer” をデバッガ (例: Immunity Debugger) にアタッチして実行状態にします。そして、攻撃マシンから生成したパターン (
pattern.txt
の内容) を含むデータをCMD1
コマンドとして “VulnServer” に送信するスクリプトを実行します。# send_pattern.py (簡易例) import socket # pattern.txt から読み込み with open("pattern.txt", "rb") as f: pattern = f.read().strip() target_ip = "TARGET_IP" # ターゲットのIPアドレス target_port = 9999 # VulnServerのポート try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((target_ip, target_port)) # コマンドとパターンを結合して送信 (実際の形式に合わせる) payload = b"CMD1 " + pattern + b"\r\n" s.send(payload) print(f"Sent {len(payload)} bytes.") s.close() except Exception as e: print(f"Error: {e}")
スクリプトを実行すると、ターゲットマシン上の “VulnServer” がクラッシュし、Immunity Debugger が停止します。
-
EIPの値の確認:
Immunity Debugger のレジスタウィンドウを確認します。すると、EIP レジスタの値が例えば
0x6F43376F
(“o7Co” のリトルエンディアン) になっていることがわかりました。これはパターン文字列の一部です。📝 メモ: デバッガによっては、レジスタ値の隣に対応するASCII文字が表示されることがあります。それも確認しましょう。この例では “o7Co” と表示されているかもしれません。 -
オフセットの特定:
攻撃マシンに戻り、
msf-pattern_offset
を使ってオフセットを計算します。パターン長は 2500、確認したEIPの値は0x6F43376F
です。msf-pattern_offset -l 2500 -q 0x6F43376F
または、文字列で指定する場合:
# エンディアンを考慮して逆順で指定する必要があるか確認 # デバッガで "o7Co" と見えていれば、そのまま指定できることが多い msf-pattern_offset -l 2500 -q o7Co
実行結果:
[*] Exact match at offset 1982
これにより、
CMD1
コマンドへの入力データの先頭から 1982 バイト目 が、EIPレジスタを上書きし始める位置であることが判明しました! 🎉 -
オフセットの検証 (推奨):
特定したオフセットが正しいかを確認するために、簡単な検証を行います。例えば、1982バイトの “A” (
\x41
) と、それに続く4バイトの “B” (\x42
)、さらにその後に適当な長さの “C” (\x43
) を送信してみます。# verify_offset.py (簡易例) import socket offset = 1982 eip_overwrite = b"\x42\x42\x42\x42" # BBBB padding = b"\x43" * 500 # CCCCC... payload = b"A" * offset + eip_overwrite + padding target_ip = "TARGET_IP" target_port = 9999 try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((target_ip, target_port)) payload_full = b"CMD1 " + payload + b"\r\n" s.send(payload_full) print(f"Sent {len(payload_full)} bytes.") s.close() except Exception as e: print(f"Error: {e}")
再度デバッガで “VulnServer” を実行し、このスクリプトを実行します。クラッシュ後、EIPレジスタの値が
0x42424242
になっていれば、オフセットの特定は成功です! ✅ -
次のステップへ:
オフセットが正確に特定できれば、次はEIPを上書きする値として、スタック上のシェルコードへジャンプさせるためのアドレス(例:
JMP ESP
命令のアドレスなど)を探し、最終的なエクスプロイトコードを組み立てていくことになります。これには、さらに「悪い文字」(Bad Characters) の特定や、適切なリターンアドレスの探索といったステップが必要になりますが、それはまた別の話です。
注意点とベストプラクティス ⚠️
msf-pattern_offset
は非常に便利なツールですが、利用にあたっては以下の点に注意しましょう。
-
正確なEIP/RIPの値が必要:
オフセット計算の精度は、デバッガで確認したEIP/RIPの値が正確であるかに依存します。読み間違いや転記ミスがないように注意深く確認してください。特に16進数値を手入力する際は、タイプミスに気をつけましょう。
-
パターン長 (-l) の指定:
msf-pattern_create
で生成したパターンの長さを-l
オプションで正しく指定することが推奨されます。これにより、意図しない範囲での検索を防ぎ、より信頼性の高い結果を得られます。 -
エンディアンの理解:
特に16進数値を
-q
で指定する場合、ターゲットアーキテクチャのエンディアン(リトルエンディアンかビッグエンディアンか)を意識する必要があります。多くの場合、ツールがうまく処理してくれますが、予期せぬ結果になった場合は、文字列形式での指定や、エンディアンを考慮した値の入力を試みてください。 -
オフセット特定は第一歩:
msf-pattern_offset
でオフセットを特定することは、バッファオーバーフローを利用したエクスプロイト開発の重要なステップの一つですが、これで終わりではありません。その後、ペイロード(シェルコード)の配置、実行フローをペイロードに向けるためのリターンアドレスの選択、そして「悪い文字」(ペイロード内で使用できない文字)の除去など、多くの作業が必要です。 -
複雑なケース:
スタックの構造が複雑だったり、他のセキュリティ機構(ASLR、DEP/NX、スタックカナリアなど)が有効になっている場合、単純なオフセット特定だけでは不十分な場合があります。より高度なテクニック(ROP: Return-Oriented Programming など)が必要になることもあります。
-
倫理的な利用:
最も重要な注意点です。 Metasploit Framework に含まれるツール群は、セキュリティ診断や教育目的で開発された強力なツールです。必ず、自身が管理するシステム、または明確な許可を得たシステムに対してのみ使用してください。 許可なく他者のシステムに対してこれらのツールを使用することは、不正アクセス行為となり、法的に罰せられる可能性があります。
まとめ
この記事では、Metasploit Frameworkのユーティリティツール msf-pattern_offset
を使って、バッファオーバーフロー脆弱性の解析における重要なステップである「オフセット特定」を行う方法について解説しました。
主なポイントは以下の通りです:
- バッファオーバーフローでは、EIP/RIPレジスタを制御可能な任意のアドレスで上書きすることが目標の一つであり、そのためには入力データのどこで上書きが発生するかの「オフセット」を知る必要がある。
msf-pattern_create
を使って、ユニークなパターン文字列を生成する。- 生成したパターンを脆弱なプログラムに送り込み、クラッシュ時のEIP/RIPレジスタの値を確認する。
msf-pattern_offset
に、確認したEIP/RIPの値 (-q
オプション) とパターンの長さ (-l
オプション) を指定して実行し、オフセットを計算する。- 得られたオフセットは、エクスプロイトコードを開発する上で非常に重要な情報となる。
- ツールの使用は、必ず許可された環境でのみ行うこと。
msf-pattern_offset
は、地道で時間のかかるオフセット特定作業を大幅に効率化してくれる強力な味方です。バッファオーバーフローの学習やペネトレーションテストの実務において、ぜひ活用してみてください。 💪
もちろん、オフセット特定はエクスプロイト開発プロセスの一部に過ぎません。この先には、さらに奥深いバイナリ解析の世界が広がっています。興味を持たれた方は、ぜひ学習を続けてみてください!
参考情報 📚
より詳しい情報や関連情報については、以下のリソースを参照してください。
-
Kali Linux Tools – metasploit-framework:
Kali Linuxに含まれるMetasploit Frameworkツールの概要ページ。`msf-pattern_create` や `msf-pattern_offset` も紹介されています。
-
Metasploit Unleashed – Writing an Exploit:
Offensive SecurityによるMetasploitの無料トレーニングコースの一部。`pattern_create` と `pattern_offset` を使ったエクスプロイト開発の例が載っています。
https://www.offsec.com/metasploit-unleashed/writing-exploit/ (上記検索結果[2]のリンク先はOffSecサイト内の別ページを示唆している可能性があるため、関連するであろう公式トレーニングページへのリンクを記載)
-
GitHub – metasploit-framework/tools/exploit/pattern_offset.rb:
msf-pattern_offset
ツールの実際のソースコード (Ruby)。オプションや内部ロジックを確認できます。https://github.com/rapid7/metasploit-framework/blob/master/tools/exploit/pattern_offset.rb
コメント