無線通信エンジニアの備忘録

無線通信だったり、ITだったり、仕事で覚えた専門知識の備忘録

Wiresharkで独自プロトコルのメッセージを統計解析し、結果をコンソールに表示する(その1)

ちょっと長ったらしいタイトルになってしまいました・・・(;^_^A

前回までに、Wiresharkで独自プロトコルのメッセージフォーマットを解析し、結果をGUIに反映するLuaプラグインの作成方法について紹介してきました。


taekwongineer.hatenablog.jp

taekwongineer.hatenablog.jp


今回は、「Wireshark Developer’s Guide」の「Chapter 11. Wireshark’s Lua API Reference Manual」で規定されている

・Field
・FieldInfo
・TextWindow
・Listener

の4つのクラスの使用方法について、タイトルのアプリケーションの作成を例に紹介したいと思います。


この記事の目次

1.今回作成するアプリケーションでできることの概要

今回の解析対象として扱う独自プロトコルは、前回の記事同様、図1-1のメッセージフォーマットを持つサンプルプロトコルを対象とします。


f:id:taekwongineer:20200419163817p:plain
図1-1 解析対象プロトコルのメッセージフォーマット

図1-1のメッセージのうち、Messageの値の内容を集計し、どの値をどれだけキャプチャしたかを図1-2のようにコンソール上に一覧で表示させます。


f:id:taekwongineer:20200425160904p:plain
図1-2 Messageの値の統計解析結果のコンソール表示

図1-2のコンソールアプリケーションの作成も、これまでに紹介した独自プロトコル解析プラグインと同様、Luaスクリプトプラグインで作成します。

2.アプリケーションの作成手順

まず、図1-2のコンソールアプリケーションのサンプルコードを以下に示します。

-- message_counter.lua
-- (1)Fieldクラスのオブジェクトを定義
SampleMsg_f = Field.new("sample.msg")

-- (2)GUIからコンソール呼び出し時に実行される関数の定義
function MsgCount()

    -- メッセージの統計計算に使用する変数の定義
    local MsgCountTable = {} -- 統計情報を格納するテーブル

    -- (3)解析結果を表示するTextWindowオブジェクトの定義
    local tw = TextWindow.new("Message Counter")

    -- (4)ボタン追加及びクリック時の動作の定義
    tw:add_button("Reset", function()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルの初期化
        MsgCountTable = {}
    end)

    -- (5)Listenerオブジェクトの定義
    local listener = Listener.new(nil, "sample")

    -- (6)コンソールウィンドウクローズ時の処理
    tw:set_atclose(function()
        -- Listenerを停止
        listener:remove()
    end)

    -- (7)サンプルプロトコルメッセージ受信時の解析処理の定義
    function listener.packet(pinfo, buffer)
        -- ローカル変数の定義
        local Msg    -- サンプルプロトコルのMessage
        local Count    -- Messageの数

        -- キャプチャしたサンプルプロトコルパケットのMessageフィールドの情報を取得
        Msg = tostring(SampleMsg_f())

        -- 取得したMessageを統計テーブルに追加
        Count = MsgCountTable[Msg] or 0
        MsgCountTable[Msg] = Count + 1
    end

    -- (8)コンソールウィンドウに統計結果を書き出し
    function listener.draw()
        tw:clear()
        for msg, count in pairs(MsgCountTable) do
             tw:append(msg .. " : " .. count .. "\n")
        end
    end

    -- (9)キャプチャファイルの読込時に呼ばれる関数
    function listener.reset()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end
end

-- (10)メニュー登録
register_menu("Test/Message Counter", MsgCount, MENU_TOOLS_UNSORTED)

上記のサンプルコードは、「Wireshark Developer’s Guide」の10.4. Example: Listener written in Luaを参考に作成しました。

以降、(1)~(10)の手順の詳細について説明していきます。

(1)Fieldクラスのオブジェクトを定義

初めにFieldクラスのオブジェクトを定義します。

-- (1)Fieldクラスのオブジェクトを定義
SampleMsg_f = Field.new("sample.msg")

Fieldクラスについては、「Wireshark Developer’s Guide」で以下のように規定されています。

11.2.1. Field
A Field extractor to obtain field values. A Field object can only be created outside of the callback functions of dissectors, post-dissectors, heuristic-dissectors, and taps.
Once created, it is used inside the callback functions, to generate a FieldInfo object.
11.2.1.1. Field.new(fieldname)
Create a Field extractor.
Arguments
fieldname
The filter name of the field (e.g. ip.addr)
Returns
The field extractor
Errors
A Field extractor must be defined before Taps or Dissectors get called

Fieldクラスは、field.new()の引数で指定したプロトコルフィールドの値を取得するために使用します。
後に(7)でも説明しますが、SampleMsg_f()の形式で、サンプルプロトコルのMessageフィールドの情報として、FieldInfoクラスのオブジェクトを得ることができます。

(2)GUIからコンソール呼び出し時に実行される関数の定義

Wiresharkのメニューから、コンソールアプリケーションを起動するときに実行される巻数を定義します。

-- (2)GUIからコンソール呼び出し時に実行される関数の定義
function MsgCount()

    -- この中に(3)~(9)の処理を記述していく

end

(3)解析結果を表示するTextWindowオブジェクトの定義

TextWindow.new([タイトル])の形式で、TextWindowクラスのオブジェクトを初期化します。
コンソールウィンドウのテキストの書き込みやボタンの追加等はこのオブジェクトに対して行います。

    -- (3)解析結果を表示するTextWindowオブジェクトの定義
    local tw = TextWindow.new("Message Counter")

(4)ボタン追加及びクリック時の動作の定義

コンソールウィンドウにボタンを追加し、クリック時の動作を定義します。

    -- (4)ボタン追加及びクリック時の動作の定義
    tw:add_button("Reset", function()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end)

ボタン追加及びクリック時の処理の定義は、TextWindowクラスのオブジェクトtwに対し、tw:add_button([表示名], [クリック時に実行する関数名])の形式で記述します。
第2引数には関数を代入しますが、上記のサンプルコードでは、無名関数(function() ~ end)の形式で代入しています。

(5)Listenerオブジェクトの定義

パケットのキャプチャを行うLiestenerクラスのオブジェクトを定義します。

     -- (5)Listenerオブジェクトの定義
    local listener = Listener.new(nil, "sample")

Listenerクラスの概要及びListenerクラスのオブジェクトの初期化方法については、「Wireshark Developer’s Guide」で以下のように規定されています。

11.4.1. Listener
A Listener is called once for every packet that matches a certain filter or has a certain tap. It can read the tree, the packet’s Tvb buffer as well as the tapped data, but it cannot add elements to the tree.
11.4.1.1. Listener.new([tap], [filter], [allfields])
Creates a new Listener tap object.
Arguments
tap (optional)

The name of this tap. See Listener.list() for a way to print valid listener names.
filter (optional)
A display filter to apply to the tap. The tap.packet function will be called for each matching packet. The default is nil, which matches every packet. Example: "m2tp".
allfields (optional)
Whether to generate all fields. The default is false. Note: This impacts performance.
Returns
The newly created Listener listener object
Errors
tap registration error

Listener.new()は、[tap]、[filter]、[allfield]の3つの引数を持ちますが、[filter]以外の引数は正直何に使うのかよく分かりません(;^_^A
今回はサンプルプロトコルのメッセージのみをキャプチャしたいため、第1引数にはnil、第2引数には"sample"を入力します。(第3引数は省略)
全ての引数を省略した場合は、全てのパケットがキャプチャされます。

(6)コンソールウィンドウクローズ時の処理

コンソールウィンドウにデフォルトで付いているcloseボタンもしくは右上の×ボタンをクリックしたときの動作です。
TextWindowクラスのオブジェクトtwに対し、tw:set_atclose([クリック時に実行する関数名])の形式で記述します。

    -- (6)コンソールウィンドウクローズ時の処理
    tw:set_atclose(function()
        -- Listenerを停止
        listener:remove()
    end)

ここでも引数には無名関数を使用しています。
また無名関数内での処理として、Listenerクラスのオブジェクトlistenerに対し、listener:remove()を実行していますが、これはListenerオブジェクトによるパケットのキャプチャを停止するための処理となります。

11.4.1.4. listener:remove()
Removes a tap Listener.
11.4. Post-Dissection Packet Analysis

(7)サンプルプロトコルメッセージ受信時の解析処理の定義

Listenerクラスのオブジェクトlistenerがサンプルプロトコルのパケットをキャプチャした際に実行する処理を
function listener.packet(pinfo, buffer)
 キャプチャ時の処理
end

の関数の形式で記述します。

2つの引数pinfo及びbufferはそれぞれ
・pinfo:Pinfoクラスオブジェクト
・buffer:Tvbクラスオブジェクト
であり、キャプチャしたパケットに関する情報や生データが格納されています。
(今回のサンプルコードでは特に使用しません)

    -- (7)サンプルプロトコルメッセージ受信時の解析処理の定義
    function listener.packet(pinfo, buffer)
        -- ローカル変数の定義
        local Msg    -- サンプルプロトコルのMessage
        local Count    -- Messageの数

        -- キャプチャしたサンプルプロトコルパケットのMessageフィールドの情報を取得
        Msg = tostring(SampleMsg_f())

        -- 取得したMessageを統計テーブルに追加
        Count = MsgCountTable[Msg] or 0
        MsgCountTable[Msg] = Count + 1
    end

本サンプルコードの関数内では、サンプルプロトコルのMessageフィールドの値を取得するために、(1)で定義したFieldクラスのオブジェクトSampleMsg_fを使用します。

SampleMsg_fに対し、SampleMsg_f()と記述することで、listenerがキャプチャしたサンプルプロトコルのメッセージから、Messageフィールドの値をFieldInfoクラスのオブジェクトとして取得することができます。

また本サンプルコードでは、SampleMsg_f()で取得したFieldInfoオブジェクトに対し、更にtostring(SampleMsg_f())とすることで、文字列変換を行っています。

Fieldinfoクラスの詳細については、以下を参照
11.2. Obtaining Dissection Data

(8)コンソールウィンドウに統計結果を書き出し

Listenerクラスオブジェクトlistenerに対し、draw()関数を定義し、コンソールウインドウにMessageの統計結果を書き出します。

    -- (8)コンソールウィンドウに統計結果を書き出し
    function listener.draw()
        tw:clear()
        for msg, count in pairs(MsgCountTable) do
             tw:append(msg .. " : " .. count .. "\n")
        end
    end

TextWindowクラスオブジェクトtwに対するテキストの書き込み方法はいくつかありますが、ここではtw:append([テキスト])を使用しています。

11.3.2.5. textwindow:append(text)
Appends text to the current window contents.
Arguments
text

The text to be appended.
Returns
The TextWindow object.
Errors
GUI not available
11.3. GUI Support

listener.draw()関数は、listenerがパケットをキャプチャした数秒後に実行される仕様となっているようで、パケットキャプチャ後、listener.draw()関数が実行される前に新たなパケットをキャプチャしても、一度関数が実行されるまでは新規のタイマーは設定されないみたいです。

短い時間に何度もTextWindowへの書き込みが行わることによる負荷を軽減するための仕様と思われます。

(9)キャプチャファイルの読込時に呼ばれる関数

listener.reset()関数は、listenerがパケットキャプチャを実行している状態で、キャプチャファイルの読込をしたときに呼ばれる関数のようです。
参考にしたサンプルコードに記載されていたので、本サンプルコードでも記載していますが、あまり重要ではないような気がします(;^_^A

    -- (9)キャプチャファイルの読込時に呼ばれる関数
    function listener.reset()
        -- コンソールのテキストを削除
        tw:clear()
        -- メッセージ統計テーブルを初期化
        MsgCountTable = {}
    end

(10)メニュー登録

最後に(2)~(9)で記述したMsgCount()関数をGUIから呼び出すための処理を記述します。

-- (10)メニュー登録
register_menu("Test/Message Counter", MsgCount, MENU_TOOLS_UNSORTED)


register_menu()関数に上記のように引数を与えることで、図10-1のようにGUIのメニューからMsgCount関数を呼び出すことができるようになります。


f:id:taekwongineer:20200426180556p:plain
図10-1 GUIへのメニュー登録

regiser_menu()関数の詳しい使用方法については、下記を参照

11.3.3.2. register_menu(name, action, [group])
Register a menu item in one of the main menus. Requires a GUI.
Arguments
name

The name of the menu item. Use slashes to separate submenus. (e.g. Lua Scripts → My Fancy Statistics). (string)
action
The function to be called when the menu item is invoked. The function must take no arguments and return nothing.
group (optional)
Where to place the item in the menu hierarchy. If omitted, defaults to MENU_STAT_GENERIC. One of:
MENU_STAT_UNSORTED: Statistics
MENU_STAT_GENERIC: Statistics, first section
MENU_STAT_CONVERSATION: Statistics → Conversation List
MENU_STAT_ENDPOINT: Statistics → Endpoint List
MENU_STAT_RESPONSE: Statistics → Service Response Time
MENU_STAT_TELEPHONY: Telephony
MENU_STAT_TELEPHONY_ANSI: Telephony → ANSI
MENU_STAT_TELEPHONY_GSM: Telephony → GSM
MENU_STAT_TELEPHONY_LTE: Telephony → LTE
MENU_STAT_TELEPHONY_MTP3: Telephony → MTP3
MENU_STAT_TELEPHONY_SCTP: Telephony → SCTP
MENU_ANALYZE: Analyze
MENU_ANALYZE_CONVERSATION: Analyze → Conversation Filter
MENU_TOOLS_UNSORTED: Tools
11.3. GUI Support

3.まとめ

ここまで

・Field
・FieldInfo
・TextWindow
・Listener

の4つのクラスのオブジェクトを使用し、サンプルプロトコルのMessageフィールドの値の統計結果をコンソールウィンドウに表示させるアプリケーションの作成方法について紹介しました。

listener.packet()関数内で、Field、FieldInfoクラスオブジェクトを使用することで、簡単にMessageフィールドの値を取得することができました。

ただ、このやり方ですと、1つのIPパケット内に複数のサンプルプロトコルメッセージが含まれていた場合については、先頭のメッセージに対してしか、listener.packet()関数が実行されません。

全てのサンプルプロトコルメッセージのMessageフィールドの値を読み込むためには、また別の方法が必要となります。

次回は1つのIPパケット内に複数のサンプルプロトコルメッセージが含まれている場合を考慮したアプリケーションの作成方法について紹介したいと思います。