この記事では、以下のような順で Wireshark のプラグインの書き方を説明しています。
- [準備] Wireshark で Lua プラグインを有効にする
- [練習] Hello World を表示するプラグインを書いてみる
- [準備その2] 独自プロトコルを定義する
- 独自プロトコル(UDP ベース)用の Dissector プラグインを実際に書いてみる
- 独自プロトコル(TCP ベース)用の Dissector プラグインを実際に書いてみる
Wireshark に慣れた人なら、本編の前の「初めに」「準備するもの」「知っておくとよい単語」といったあたりは読み飛ばしてもいいかもしれません。
Wireshark はパケットキャプチャができるオープンソースのツールです。(昔は Ethereal とか呼ばれていましたね)
パケットキャプチャはネットワーク関連のプログラムのデバッグに大変効果を発揮します。
Wireshark には最初からものすごくたくさんのプロトコルのためのフィルタが入っているので、ダウンロードしてきてインストールしただけでも十分パケットの解析に役立ちますが、使いこなしていくうちにちょっとした不便を感じるようにもなります。
たとえば、キャプチャされるパケット数が多すぎるので適度にフィルタリングしたいけど条件が複雑なのでフィルタの欄に書ききれない、とか、独自のプロトコルを作っていてそのパケットをキャプチャしてみたはいいものの、毎回毎回「人間デコーダ」になってバイナリを解析するのが辛いので何とかしたいとか、大事なパケットをなかなか見つけられないとか、そんなことを思ったりします。
独自のプラグインを書くことで、そんな悩みが解決できるかもしれません。
プラグインと言っても、作り方は意外と簡単です。
C/C++ でプラグインを書いてコンパイルして使うこともできるようですが、Lua というスクリプト言語を使ってお手軽にプラグインを書くことができます。
Lua という言語を知らなくても、他のプログラミング言語を使ったことがある人ならたぶんすぐに使えるようになると思います。私も今回初めて使いましたが、すぐに慣れてコードを書くことができました。
Lua でプラグインを書いてもほとんどの場合で十分な処理速度を得られると思いますので、まずは Lua で書いてみると良いと思います。もしそれで本当に処理速度が十分でないと思われたら、C/C++ で書き直せばよいのではないでしょうか。
Lua を知らない人も、Lua という新しい言語を覚勧えてまでプラグインを書くべきだとおススメしたくなるくらい、Lua のお手軽さは捨てがたいものがあります。
準備するもの
さて、前置きが長くなってしまいましたが、Lua プラグインを書くために必要なものは以下の通りです:
あとは手元に Lua のリファレンス本
や、Wireshark の本
なんかあるといいかもしれません。
上野 豊
ソフトバンククリエイティブ
売り上げランキング: 12565
おすすめ度の平均:


期待と評価

まさに入門書

待っていました

ちょうどよい厚さ、わかりやすい解説
もちろん本じゃなくても Web のリファレンスでも十分だと思います。
(私はお勧めできるほど良いリファレンスサイトを見つけられませんでしたが・・・)
知っておくとよい単語
Dissector
直訳すると「解剖者」です。キャプチャしたパケットを "解剖" するプラグインのことを Dissector と呼びます。
場合によってはバイト単位やビット単位でパケットのデータを見ていくことになりますので、まさに解剖と呼ぶにふさわしい作業をするわけですね。
Tap
Dissector は基本的に 1 パケット単位で解剖をしますが、Tap は Dissector によって解剖された過去のパケットの情報などを蓄積しておいて統計情報の収集などを行うときに使われるプラグインのタイプです。
今回の記事では説明していません。
Tvb
キャプチャしたパケットのデータが格納されているバッファです。Dissector には、1 パケットごとに Tvb として Wireshark から渡されてきます。Dissector に渡された Tvb は、1 回の Dissector の呼び出しごとに破棄されてしまうので、場合によっては Reassembler を使ってデータを再構築する必要があります。
Reassembler
受信したパケットを再組み立てするための仕組みです。
TCP は一般的に、アプリケーションレイヤの 1 メッセージが 1 つのパケットで受信出来るとは限りませんし、逆に複数のメッセージ(の断片)が 1 つのパケットで受信されることもあります。そのような状況に対処するために Wireshark に組込みで用意された仕組みが Reassembler です。
Lua からこの Reassembler を使う方法はとても簡単です。
詳しくは後で述べますが、dissect する関数の戻り値として「Tvb のうち消費したバイト数」を返すだけです。消費されずに余った分は次のパケットが受信された時に再度 Tvb の先頭にくっつけられて Dissect 関数に渡されてきます。
Wireshark はデフォルトでは Lua プラグインが有効になっていませんので有効にする必要があります。
Wireshark をインストールしたディレクトリ(Windows ならたとえば C:\Program files\Wireshark\)の直下に init.lua というファイルがありますので、そのファイルを開いて以下のように編集します。
(a) 以下の行を探してコメントアウト
disable_lua = true; do return end;
↓
-- disable_lua = true; do return end;
Lua では -- でコメントアウトです。
(b) 以下の行を編集して false を true にする
run_user_scripts_when_superuser = false
↓
run_user_scripts_when_superuser = true
好きなディレクトリに、helloworld.lua というファイルを作成します。
先ほど編集した init.lua ファイルをもう一度開き、最下行に
dofile("C:/wireshark_plugins/helloworld.lua")
という具合に記述します。ファイル名はフルパスで 指定してください。Windows の場合でもディレクトリの区切りは \ ではなく / を使います。
テキストエディタで helloworld.lua ファイルを開いて、以下のような内容を書いて保存してみましょう。
if gui_enabled() then
local splash = TextWindow.new("Hello!");
splash:set("Hello world!")
end
保存したら、Wireshark を起動します。
以下のようなウインドウが現れれば成功です!
3. [準備その2] 独自プロトコルを定義する
それでは、Hello World よりももうちょっと実用的な、UDP を使った独自プロトコルの Dissect をするプラグインを書いてみましょう・・・といきたいところですが、その前に、解析対象となる独自プロトコルがどんなものか、説明します。
本当は何でもいいのですが、簡単な題材として以下のような架空の「郵便番号解決プロトコル」を定義して、解析対象とします。
このプロトコルでは、基本的には、以下のような動作をします。
(1) クライアントからサーバに郵便番号を送信すると、サーバはその郵便番号で特定される住所の文字列を返します。
(2) クライアントからサーバに住所の文字列を送信すると、サーバはその住所のための郵便番号を返します。
このプロトコルにのっとったパケットのやりとりを行うプログラムを用意して、そのパケットを解剖してみましょう。
概要は以上のとおりとして、詳細を決めていきます。
パケットフォーマットは以下のような、いわゆる TLV 型とします。
Type 部分は 8 bit の Code と 8 bit の Version からなり、このパケットの Value 部に含まれているデータの構文や意味を規定します。
Length 部分は 16 ビットの符号なし整数で、Value 部の長さが何バイトあるかを示します。
Value 部分は Length で指定されたバイト数のデータで、その中身の構文や意味は Type 部で指定された値によって変わります。
なお、整数のエンディアンはすべてネットワークオーダ(ビッグエンディアン)とします。
このパケットフォーマットにのっとり、実際にやり取りするメッセージを以下のように規定します。
| Code |
Version |
意味 |
Length |
Value |
| 0x0001 |
0x0001 |
郵便番号から住所への変換を要求 |
常に 7
|
byte 0 - 6 |
郵便番号を ASCII コードで指定。
〒012-3456 なら、ASCII 文字で
’0’ ’1’ ’2’ ‘3’ ‘4’ ‘5’ ‘6’ の 7 バイトを、Value 領域の先頭から順に格納する |
| 0x0002 |
0x0001 |
郵便番号から住所への変換結果を応答 |
可変長 |
byte 0 |
変換できたかどうか。
変換できたら値 1 (0x01)、
変換できなかったら値 0 (0x00)
|
| byte 1 - 7 |
変換元の郵便番号。
ASCII コードで 7 桁
|
| byte 8 - 末尾 |
住所の文字列。
UTF-8 形式、ナル文字 ’\0’ でターミネート。
変換できなかった場合は空文字列。 |
| 0x0011 |
0x0001 |
住所から郵便番号への変換を要求 |
可変長 |
byte 0 - 末尾 |
住所の文字列。
UTF-8 形式、ナル文字 '\0' でターミネート。 |
| 0x0012 |
0x0001 |
住所から郵便番号への変換結果を応答 |
可変長
|
byte 0 |
変換できたかどうか。
変換できたら1、できなかったら0 |
| byte 1 - 7 |
変換後の郵便番号。
変換できなかった場合は "0000000" が格納される
|
| byte 8 - 末尾 |
変換元の住所の文字列
UTF-8 形式、ナル文字 '\0' でターミネート。
|
4. 独自プロトコル(UDP ベース)用の Dissector プラグインを書いてみる
前置きが長くなってしまいましたが、上記で定義したようなプロトコルを解析するための Wireshark プラグインのコードは以下のようになります。
-- works as of Wireshark Version 1.0.5
do
-- (1) Dissector の情報を格納するテーブルを作成します。
-- この情報をあとから Wireshark に登録します。
-- 第1引数で指定している文字列は、この Dissector の名前です。
-- フィルタの選択画面等で表示されます。
-- 第2引数で指定している文字列は、この Dissector の説明です。
udp_sample_proto = Proto("UDPSAMPLE", "Sample protocol dissector (UDP version)")
-- (2) Dissector の実際の処理をする関数を (1) で作ったテーブルに設定します。
-- 引数の意味はそれぞれ以下の通り:
-- buffer: パケットのデータが入った tvb
-- pinfo: パケットの概要を GUI のリスト部分に表示するために使うオブジェクト
-- tree: パケットを解析した情報をツリー形式で GUI のツリー部分に表示
-- するために使うオブジェクト
udp_sample_proto.dissector = function(buffer, pinfo, tree)
local code_range = buffer(0,1)
local code = code_range:uint()
local version_range = buffer(1,1)
local version = version_range:uint()
local length_range = buffer(2,2)
local length = length_range:uint()
local code_names = {
[0x01] = "Postal Code => Address (Request)",
[0x02] = "Postal Code => Address (Response)",
[0x11] = "Address => Postal Code (Request)",
[0x12] = "Address => Postal Code (Response)",
}
local subtree = tree:add("Postal Code Resolver Protocol Data")
subtree:add(code_range, "Code:", code, code_names[code])
subtree:add(version_range, "Version:", version)
subtree:add(length_range, "Length:", length)
dispatch(code, version, buffer(4, length):tvb(), pinfo, subtree)
pinfo.cols.protocol = "UDP Sample"
pinfo.cols.info = code_names[code]
end
-- (3) Dissector を Wireshark に登録します。
udp_table = DissectorTable.get("udp.port")
for i, port in pairs( { 10000, 10001 } ) do
udp_table:add(port, udp_sample_proto)
end
end
function dispatch(code, ver, buffer, pinfo, tree)
local subtree = tree:add(buffer(0), "Value:", buffer(0):tvb())
if code == 0x01 then
subtree:add(buffer(0,7), "Postal Code:", buffer(0,7):string())
elseif code == 0x02 then
subtree:add(buffer(0,1), "Converted:", buffer(0,1):uint())
subtree:add(buffer(1,7), "Postal Code:", buffer(1,7):string())
subtree:add(buffer(8), "Address:", buffer(8):string())
elseif code == 0x11 then
subtree:add(buffer(0), "Address:", buffer(0):string())
elseif code == 0x12 then
subtree:add(buffer(0,1), "Converted:", buffer(0,1):uint())
subtree:add(buffer(1,7), "Postal Code:", buffer(1,7):string())
subtree:add(buffer(8), "Address:", buffer(8):string())
end
end
このコードをファイルに保存し、HelloWorld.lua と同じように init.lua から dofile() で読み込むようにしたら、Wireshark を再起動しましょう。
コードの内容を順に解説します。
大まかな処理の流れとしては、
・Dissector のディスクリプタを用意
(コード中のコメント (1) と (2) に相当)
・そのディスクリプタを Wireshark に登録(コメント(3))
となります。
まず 8 行目の Proto という関数でディスクリプタを作成しています。
引数に与えているのは、この Dissector の名前と概要の説明です。
次に 16 行目でディスクリプタに実際の処理を行う関数を設定しています。関数の本体は 16 行目から 42 行目で定義しています。この内容は後から説明します。
最後に、45 ~ 48 行目で、ディスクリプタを Wireshark に登録しています。
53 ~ 67 行目の関数は Dissector 本体の処理で使うサブルーチンです。
それでは肝心の Dissector としての処理をする関数の中身ですが、16 行目で受け取っている 3 つの引数は、コメントにも説明が書いてありますが、
・パケットのデータが格納されているバッファ(Tvb)
・リスト表示用オブジェクト
・ツリー表示用オブジェクト
です。
(Lua ではすべては "テーブル" というデータ構造なので "オブジェクト" という用語は無いかもしれません)
つまり、バッファ(パケット)の内容を解析して、Wireshark の GUI 上の、リスト部分やツリー部分の表示を更新していくという作業になります。
17 行目から 24 行目では、Code, Version, Length のフィールドを読み取っています。
17 行目の buffer(0,1) というコードは、バッファのオフセット 0 から長さ 1 バイトの範囲(TvbRange)を取り出しています。この TvbRange に対して uint() という関数を呼び出すとその範囲をビッグエンディアンで符号なし整数とみなして整数値を取得することができます。
Tvb や TvbRange が持っている関数について、詳しくはリファレンス(Tvb、TvbRange)を参照してください。
次は 26 行目からのテーブルですが、これは code の値に応じて、表示する文字列をルックアップするためのテーブルです。
33 行目では、ツリー表示に新しい子ノード(TreeItem)を追加しています。
tree:add() は追加された子ノードへのリファレンス(のようなもの)を返すので、そこにさらに孫ノードを追加したりすることができます。
tree:add() の引数としては、TvbRange と、複数のラベルを指定しています。
TvbRange を指定すると、ツリー表示の下のバイナリ表示の部分と連動するようになります。
ラベルは複数指定することができて、指定した分だけ TreeItem のラベルとして並んで表示されます。
38 行目では、dispatch() 関数を呼び出して、code と version の値に応じて処理を振り分けています。
TvbRange を Tvb に変換(サブバッファを作成)するために、tvb() という関数を呼び出している点にだけ注意してください。
最後に 40 行目と 41 行目で、リスト部分の表示をカスタマイズしています。
53 行目からの dispatch 関数の内容はこれといって難しいものは無いと思います。
サンプルのサーバとクライアント(ともに Ruby スクリプト)を用意しましたので、ダウンロードして実行してみてください。
サーバのサンプル: sample_udp_protocol_server.rb
クライアントのサンプル: sample_udp_protocol_client.rb
サーバとクライアントは別々のマシンで実行する必要があります。
私はノートパソコン(Vista)側をクライアントにして、そのノートパソコンの中で動いている仮想マシン(Ubuntu)でサーバを起動しました。
サーバ側かクライアント側どちらかで Wireshark を起動し、フィルタを設定し、キャプチャを開始したら、サンプルのサーバを起動し、最後にクライアントを実行してください。
サーバは引数なしで実行できます。
$ ruby sample_udp_protocol_server.rb
クライアントはサーバのアドレスを引数に指定して起動します。
$ ruby sample_udp_protocol_client.rb <server address>
すると、Wireshark でパケットがキャプチャされて、以下のような画面になるはずです。
(Unicode を表示できるフォントを選択しないと、住所の文字列が文字化けして表示されてしまいます。デフォルトでは Lucida Console が選択されているはずですので、文字化けしてしまいます。上記は MS ゴシックを選択しました)
5. 独自プロトコル(TCP ベース)用の Dissector プラグインを書いてみる
今度は TCP ベースのプロトコルを解析するプラグインを書いてみます。
題材として使用する独自プロトコルは先ほど 3. で使用したものと同じ、「郵便番号解決プロトコル」にしましょう。
先ほどとの違いは、パケットが分断されたりすることがあるので、Reassembler を使う必要があるということです。
(※ただし、Wireshark version 1.0.5 では Lua プラグインから Reassembler を使うことができませんでした。昔のバージョンでは使えていたような気がしたのですが・・・ただ、現在開発中の version 1.1.2 以降で、Lua から Reassembler を使う方法が変更になるようですのであまり深追いしないでおきます・・・)
Dissector のコードは以下のようになります。
-- works as of Wireshark Version 1.0.5
do
-- (1) Dissector の情報を格納するテーブルを作成します。
-- この情報をあとから Wireshark に登録します。
-- 第1引数で指定している文字列は、この Dissector の名前です。
-- フィルタの選択画面等で表示されます。
-- 第2引数で指定している文字列は、この Dissector の説明です。
tcp_sample_proto = Proto("TCPSAMPLE", "Sample protocol dissector (TCP version)")
-- (2) Dissector の実際の処理をする関数を (1) で作ったテーブルに設定します。
-- 引数の意味はそれぞれ以下の通り:
-- buffer: パケットのデータが入った tvb
-- pinfo: パケットの概要を GUI のリスト部分に表示するために使うオブジェクト
-- tree: パケットを解析した情報をツリー形式で GUI のツリー部分に表示
-- するために使うオブジェクト
tcp_sample_proto.dissector = function(buffer, pinfo, tree)
if buffer:len() < 4 then
return DESEGMENT_ONE_MORE_SEGMENT
end
local code_range = buffer(0,1)
local code = code_range:uint()
local version_range = buffer(1,1)
local version = version_range:uint()
local length_range = buffer(2,2)
local length = length_range:uint()
local code_names = {
[0x01] = "Postal Code => Address (Request)",
[0x02] = "Postal Code => Address (Response)",
[0x11] = "Address => Postal Code (Request)",
[0x12] = "Address => Postal Code (Response)",
}
tree:add("buffer:len()", buffer:len())
if buffer:len() < (4 + length) then
tree:add("need one more segment.", buffer:len() - (4 + length))
return buffer:len() - (4 + length)
end
local subtree = tree:add("Postal Code Resolver Protocol Data")
subtree:add(code_range, "Code:", code, code_names[code])
subtree:add(version_range, "Version:", version)
subtree:add(length_range, "Length:", length)
dispatch(code, version, buffer(4, length):tvb(), pinfo, subtree)
pinfo.cols.protocol = "TCP Sample"
pinfo.cols.info = code_names[code]
return 4 + length
end
-- (3) Dissector を Wireshark に登録します。
tcp_table = DissectorTable.get("tcp.port")
for i, port in pairs( { 10000, 10001 } ) do
tcp_table:add(port, tcp_sample_proto)
end
end
function dispatch(code, ver, buffer, pinfo, tree)
local subtree = tree:add(buffer(0), "Value:", buffer(0):tvb())
if code == 0x01 then
subtree:add(buffer(0,7), "Postal Code:", buffer(0,7):string())
elseif code == 0x02 then
subtree:add(buffer(0,1), "Converted:", buffer(0,1):uint())
subtree:add(buffer(1,7), "Postal Code:", buffer(1,7):string())
subtree:add(buffer(8), "Address:", buffer(8):string())
elseif code == 0x11 then
subtree:add(buffer(0), "Address:", buffer(0):string())
elseif code == 0x12 then
subtree:add(buffer(0,1), "Converted:", buffer(0,1):uint())
subtree:add(buffer(1,7), "Postal Code:", buffer(1,7):string())
subtree:add(buffer(8), "Address:", buffer(8):string())
end
end
UDP バージョンとの主な違いは、dissect 関数の中で戻り値を設定していることです。
バッファのサイズ(受け取ったパケットのサイズ)が必要な大きさに達していない場合、負の値を返すようにしています。
十分なサイズのパケットを受け取った場合は、そのうち使用した分のサイズを返しています。
(こうすることで Reassembler の恩恵を受けられるはずだったのですが・・・)
サーバのサンプル: sample_tcp_protocol_server.rb
クライアントのサンプル: sample_tcp_protocol_server.rb
使い方は UDPバージョンと同じです。
6. まとめ
以上、プラグインの書き方の基本中の基本を紹介しましたが、いかがでしたか?これを応用・拡張すれば、それなりのレベルのプラグインを書いてパケットを分析できるようになると思います。
また、Lua の構文や言語仕様はそれほど難しくなかったのではないでしょうか。
とはいえ、Lua プラグインにもやはり限界はあります。
たとえば、Lua には組み込みのビット演算が無いので、専用のライブラリを引っ張ってきたりしなければなりません。(逆に言うと、ライブラリを使えば解決できる)
また、今回はデバッグの仕方を説明することができませんでしたが、それは私自身効率的なデバッグ方法を知らないからです。ブレークポイントやウォッチ式等の高度な仕組みはもちろんのこと、簡単に printf デバッグする方法もわかりません・・・(自分でウインドウを表示して文字列を表示させることはできるのですが、めんどくさい)
仕方ないのでツリーにデータを表示したりしていました。
「これが定石だ」「こういうときはこうすればいいよ」という情報をご存知の方は教えていただきたいと思います。
というか、デバッグ以前に、Lua プラグインのスクリプトをリロードさせる方法がわからないので、毎回 Wireshark を再起動しているくらいです。
実際にプラグインを作成してみれば、ほかにも難問に突き当たってしまうこともあるかもしれませんが、多くの場合得られるメリットのほうが大きいと思いますので、上記を参考にぜひみなさんも挑戦してみてください。
記事の間違いの指摘やご意見・ご感想など、大歓迎ですのでコメントかトラックバックでお寄せください。
参考リンク
10.4. Wireshark’s Lua API Reference Manual
http://www.wireshark.org/docs/wsug_html_chunked/wsluarm_modules.html
Lua – The Wireshark Wiki http://wiki.wireshark.org/Lua
Lua/Dissectors – The Wireshark Wiki http://wiki.wireshark.org/Lua/Dissectors
Lua/Examples – The Wireshark Wiki http://wiki.wireshark.org/Lua/Examples
README.developer
2.7 Reassembly/desegmentation for protocols running atop TCP.
http://anonsvn.wireshark.org/wireshark/trunk/doc/README.developer