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

Posted on 2013/02/03(Sun) 03:03 in technical

はじめに

通報

2013/06/05追記: wireshark_dissector_plugin_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%Wiresharkplugins" に置く。

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" に置く。

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

サンプルプロトコル

サンプルプロトコルを取り上げてもいいのだけど、ここはひとつ現実に存在するwiresharkでは対応していないプロトコルの方が達成感があるかなー、と思うので FreeBSD の ping を対象にしてみる。

一般に、wiresharkでpingをキャプチャした時のデコード結果は、次の図のようになると思うわけだ。

fbsdping_001

ただ、FreeBSDのpingはペイロードにテストデータだけでなく、先頭8byteにタイムスタンプが埋め込まれている。

これをこんな感じで出力出来るようにしてみる。

fbsdping_002

コードを書く

本来であれば、プロトコルの理解から入るべきなのだが、wiresharkでデコードするプロトコルを拡張したいとか言ってる人に、

「ICMP Echo requestのペイロード先頭4byteが[sec]、その次の4byteに[usec]が入ってる」

と説明するのに、図も何も要らないんじゃないかと思うんだ。

という訳で、fbsdping.luaというファイルを作って、コードを書く。

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

-- =================================================
-- 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引数が説明文という理解。:

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

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

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

Proto : http://www.wireshark.org/docs/wsug_html_chunked/lua_module_Proto.html#lua_class_Proto

ProtoField : http://www.wireshark.org/docs/wsug_html_chunked/lua_module_Proto.html#lua_class_ProtoField

ProtoFieldをProtoに関連付け

え、こんだけですけど...。:

fbsdping_proto.fields = {time_F, sec_F, usec_F}

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

デコード前のデータbufferに入っている(bufferというのはこちらが勝手に決めた名前だが)。

実際に値を取得する時は、範囲を指定し、型を決めて使う。:

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

これは、利用するときの形態によって変わってくるので一概には言えないものの、tostring()で囲って文字列にしてもいい。

こう書いてもいいのだけど、Fieldを選択した時にHexの該当部分がカラーリングされて欲しいので、分けて書いたほうが何かと便利だろう。:

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

既にデコード済みデータを拾う場合は、こんな感じになる。

フィルタで引っ掛けた場所のオブジェクトが貰えるので、valueで値にアクセス。:

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

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

Field : http://www.wireshark.org/docs/wsug_html_chunked/lua_module_Field.html#lua_class_Field

ツリーに追加

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

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

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

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

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

元々のバイト表現:

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

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

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

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

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を指定してるので、勝手に解釈してくれる。

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

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

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

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

Dissector.getすると、指定した解析用処理ブロックを拾ってこれるので、自分でcallデータ位置をずらして呼び出す。

こんな感じで無理矢理次のデコード方式を決めることも出来るので、スーパー意味分かんないカプセル化を解除することも出来るよ。

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

フック関数の登録

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

register_postdissector(fbsdping_proto)

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

今回は、こんな風にずらした。

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

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