問題児たちがwiresharkのdissectorをLuaで書くそうですよ?

FreeBSDのpingコマンドを例に、WiresharkのdissectorをLuaで書く場合のサンプルコードを紹介する

はじめに

Tip
2013/06/05追記: wireshark dissector with lua をPDFで書いて置いたのでそちらの方が見やすいやも。

wiresharkで対応していないプロトコルなんて滅多に無いので、利用シーンは限られるんだろうけど。

wireshark: http://www.wireshark.org/ 言わずと知れたパケット解析ソフトウェア。

  • dissector : 解析部分のこと。デコード、パース等言い方は何でもいいよ。
  • Lua : スクリプト言語的なの。wiresharkにはLua用APIがあるので連携できるの。

以下レッツ、コーディングなので興味ある人だけどーぞ。

準備

特にコレといって無いですね。
動作環境は Windows XP/Vista/7 と wireshark 1.6/1.8 辺りを想定してます。くらいかな。

Luaスクリプト動作のための設定

Wireshark 1.6以降では、今のところLuaサポートはデフォルトで有効なので、そこは割愛。
任意で作成したLuaスクリプトをwiresharkに読み込ませる方法はいくつかあるけど、代表的なのはこういうの。

Windowsの場合

  1. .luaファイルを %WIRESHARK%/plugins/<version>%APPDATA%/Wireshark/plugins に置く。

Windowsの場合は、WireSharkのバージョンアップ過程でwiresharkを一旦アンインストールしたりすると消えちゃったりするので、ちょっと面倒です。

Unix/Linuxの場合

  1. Commentout the disable_lua=true in /etc/wireshark/init.lua (Windowsの場合はこれデフォルトで有効だと思う)
  2. .luaファイルを /usr/share/wireshark/plugins/usr/local/share/wireshark/plugins$HOME/.wireshark/plugins に置く。

デコード対象のプロトコル

ここはひとつ現実に存在するwiresharkでは対応していないプロトコルの方が達成感があるかなー、と思うので、FreeBSDのpingコマンドを対象にしてみる。
一般に、wiresharkでpingをキャプチャした時のデコード結果は、次の図のようになると思うわけだ。

ただ、FreeBSDのpingはペイロードにテストデータだけでなく、先頭8byteにタイムスタンプが埋め込まれている。
これをこんな感じで出力出来るようにしてみる。

コードを書く

本来であれば、プロトコルの理解から入るべきなのだが、wiresharkでデコードするプロトコルを拡張したいとか言ってる人に、
「ICMP Echo requestのペイロード先頭4byteが[sec]、その次の4byteに[usec]が入ってる」
と説明するのに、図も何も要らないんじゃないかと思うんだ。

という訳で fbsdping.lua というファイルを作って、コードを書く。
ちなみに、windowsの場合、このスクリプトの文字コードをUTF-8にすると1行目からエラーが出るので、日本語のコメントなんぞ書こうものならShift-JISにせざるを得ないという苦痛を味わうことを付け加えておく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
-- =================================================
-- FreeBSD ping data
-- =================================================
fbsdping_proto = Proto("fbsdping","FreeBSD ping tool payload")

-- =================================================
-- Read reserve other protocol fileds
-- =================================================
data_len_f = Field.new("data.len")
icmp_type_f = Field.new("icmp.type")

-- =================================================
-- FreeBSD ping tool payload fields define.
-- =================================================
time_F = ProtoField.string("fbsdping.ts","Timestamp")
sec_F = ProtoField.uint32("fbsdping.sec","Timestamp(sec)")
usec_F = ProtoField.uint32("fbsdping.usec","Timestamp(usec)")

-- =================================================
-- Enable FreeBSD ping tool payload fields.
-- =================================================
fbsdping_proto.fields = {time_F, sec_F, usec_F}

-- =================================================
-- Parse FreeBSD ping tool payload fields.
-- =================================================
function fbsdping_proto.dissector(buffer,pinfo,tree)
    local data_len = data_len_f()
    if data_len and (icmp_type_f().value == 8 or icmp_type_f().value == 0) then
        local len = data_len.value
        local start = buffer:len() - len

        local fbsdping_range = buffer(start,8)
        local sec_range = buffer(start,4)
        local usec_range = buffer(start+4,4)

        local sec = sec_range:uint()
        local usec = usec_range:uint()

        local subtree = tree:add(fbsdping_proto, fbsdping_range, "FreeBSD ping tool payload Data")
        local subtimetree = subtree:add(time_F, buffer(start,8), os.date("%Y/%m/%d %H:%M:%S", sec) .. '.' .. tostring(usec) )
        subtimetree:add(sec_F, sec_range, sec)
        subtimetree:add(usec_F, usec_range, usec)

        local data_dissector = Dissector.get("data")
        data_dissector:call(buffer(start+8):tvb(),pinfo,tree)
    end
end

-- =================================================
-- Register fbsdping_proto.
-- =================================================
register_postdissector(fbsdping_proto)

中途半端に説明する

  1. ProtoオブジェクトとProtoFieldオブジェクトを作る。
  2. ProtoFieldをProtoオブジェクトに紐付ける
  3. 必要なデータを拾ってくる。
  4. Wiresharkから貰えるdissectorメソッドの引数であるところのtreeにaddしていく。
  5. その関数をフックするように最後に登録する。

というのがおおよその流れ。

Proto, ProtoFieldオブジェクトを作る。

第1引数がフィルタ名、第2引数が説明文という理解。

1
fbsdping_proto = Proto("fbsdping","FreeBSD ping tool payload")

第1引数がフィルタ名、第2引数が説明文という理解。

1
time_F = ProtoField.string("fbsdping.ts","Timestamp")

ProtoFieldをProtoに関連付け

え、こんだけですけど…。

1
fbsdping_proto.fields = {time_F, sec_F, usec_F}

必要なデータを拾ってくる。

デコード前のデータbufferに入っている(bufferというのはこちらが勝手に決めた名前だが)。
実際に値を取得する時は、範囲を指定し、型を決めて使う。

1
2
local sec_range = buffer(start,4)
local sec = sec_range:uint()

これは、利用するときの形態によって変わってくるので一概には言えないものの、tostring()で囲って文字列にしてもいい。
こう書いてもいいのだけど、Fieldを選択した時にHexの該当部分がカラーリングされて欲しいので、分けて書いたほうが何かと便利だろう。

1
local sec = buffer(start,4):uint()

既にデコード済みデータを拾う場合は、フィルタで引っ掛けた場所のオブジェクトが貰えるので、valueで値にアクセス。

1
2
icmp_type_f = Field.new("icmp.type")
if icmp_type_f().value == 8 then(以下略)

value以外にも、いくつかパラメータがあるよ。

ツリーに追加

プロトコルを追加する時は、第3引数のtreeにProtoオブジェクトを直接addする。

1
local subtree = tree:add(fbsdping_proto, fbsdping, "FreeBSD ping tool payload Data")

フィールドを追加する時は、Protoオブジェクトを追加した時に突っ込んだ方にaddする。

1
local subtimetree = subtree:add(time_F, buffer(start,8), os.date("%Y/%m/%d %H:%M:%S", sec) .. '.' .. tostring(usec) )

ちなみに、bit表現のツリー追加について端折り気味に書くと、
元々のバイト表現

1
flag_F = ProtoField.uint16("pdgplus.flaga","Flag A")

True/Falseでフィルタ出来るように、値と文字の関連付け

1
local VALS_BOOL = {[0] = "False", [1] = "True"}

ProtoFieldの登録。1Byteの場合は、uint8を選択。16進数表現を使うか、表示フォーマットの指定、どのbitを指すのかを指定する。

1
flag_F_B = ProtoField.uint8("pdgplus.flag_b","flag B is test bit.", base.HEX, VALS_BOOL, 0x80)

flag自体はバイトで拾って、そのまま渡す。
flag_F_B = ProtoField.uint8 の部分で base.HEX, VALS_BOOL, 0x80 を指定してるので、勝手に解釈してくれる。

1
2
local flag = flag_range:uint()
subtree:add(flag_B_F, flag_range, flag)

次のプロトコルが決まってる時

以降のプロトコルデコードをwiresharkにお返しする事ができる。今回はdata。

1
2
local data_dissector = Dissector.get("data")
data_dissector:call(buffer(start+8):tvb(),pinfo,tree)

Dissector.get すると、指定した解析用処理ブロックを拾ってこれるので、自分でcallデータ位置をずらして呼び出す。
こんな感じで無理矢理次のデコード方式を決めることも出来るので、スーパー意味分かんないカプセル化を解除することも出来るよ。

1
2
local data_dissector = Dissector.get("ip")
data_dissector:call(buffer(start+8):tvb(),pinfo,tree)

フック関数の登録

全部のデコードが終わった後に指定した関数が走るように登録。

1
register_postdissector(fbsdping_proto)

この場合、取得できるバッファは先頭0byte目から全てなので、必要な部分を読み込めるようにポインタ移動処理が待っている。
今回は、こんな風にずらした。

1
2
3
local len = data_len.value
local start = buffer:len() - len
local fbsdping_range = buffer(start,8)

一般的には、ポート番号を引っ掛けて登録することの方が多いと思う。
その場合は、公式サンプルのように DissectorTable.get して add します。

http://wiki.wireshark.org/Lua/Dissectors

この場合は、取得できるバッファは下位プロトコルから渡されたデータグラムになるので、ポインタ移動は不要である。

参考文献

Hugo で構築されています。
テーマ StackJimmy によって設計されています。