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

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

Wiresharkで独自プロトコルを解析する(その2)

前回、UDPで伝送される独自プロトコルメッセージの解析プラグインの作成方法について記事をまとめました。

taekwongineer.hatenablog.jp

記事でも書いた通り、UDPで伝送されるメッセージについては、シンプルなメッセージフォーマットであれば、お作法さえ覚えてしまえば割と簡単に解析プラグインを作成することができました。
しかし、TCPの場合は、メッセージの送信タイミングによっては、1つのIPパケットの中に複数のメッセージが格納されて送信されてしまうことがあるため、これらを解析できるようにするためには一工夫が必要です。

今回は1つのIPパケットに複数メッセージが含まれるケースを考慮した独自プロトコル解析プラグインの作成方法についてまとめます。



この記事の目次

1.解析対象の独自プロトコル

今回は以下の図1-1のメッセージフォーマットのプロトコルを例に、解析プラグインを作成していきます。


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

Message SizeにはMessageのデータ長[byte]を格納し、Messageには、UTF-8エンコードされた文字列を格納します。
この記事では上記のメッセージフォーマットのメッセージをサンプルプロトコルと呼ぶことにします。

2.解析プラグインのサンプルコード(複数メッセージ対応未考慮)

まずは、複数メッセージ対応が考慮されていない場合のサンプルコードを以下に示します。

-- 独自プロトコルの定義
SampleProto = Proto.new("sample", "Sample Protocol")

-- プロトコルフィールドの定義
f_MsgSize = ProtoField.new("Message Size", "sample.size", ftypes.UINT16)
f_Msg = ProtoField.new("Message", "sample.msg", ftypes.STRING)

-- 定義したプロトコルフィールドをプロトコルフィールド配列に登録
SampleProto.fields = {f_MsgSize, f_Msg}

-- dissector関数の定義
function SampleProto.dissector(buffer, pinfo, tree)
	
-- dissector関数内で使用するローカル変数の定義
    local MsgNum = 0    -- 1パケット内に含まれるSample Protocolのメッセージ数
    local BufLeft = buffer:len()    -- buffer内の未読込データの残りバイト数(初期値は引数bufferのデータ長)
    local BufPos = 0    -- bufferのデータ読込開始位置
    local r_BufRange    -- サンプルプロトコルメッセージのデータ範囲
    local r_MsgSize     -- Message Sizeのデータ範囲
    local r_Msg    -- Messageのデータ範囲

    ---- bufferからデータを読込み ----
    -- Message Sizeを読込み
    r_MsgSize = buffer(0, 2)

    -- Messageを読込み
    r_Msg = buffer(2, r_MsgSize:uint())

    -- Sample Protocolメッセージ全体を読込み
    r_BufRange = buffer(0, 2 + r_MsgSize:uint())
    
    ---- プロトコルツリーに情報を追加
    -- treeにサブツリーとして"Sample Protocol"を追加
    local subtree = tree:add( SampleProto, r_BufRange )
	
    -- subtreeに上記で定義した各プロトコルフィールドの値を追加
    subtree:add( f_MsgSize, r_MsgSize )  -- Message Size
    subtree:add( f_Msg, r_Msg, r_Msg:string(ENC_UTF_8), "Message : " .. r_Msg:string(ENC_UTF_8) )  -- Msg

    ---- プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Sample"
	
    -- "info"列の表示
    pinfo.cols.info = "Message Size:" .. r_MsgSize:uint() .. " Message:" .. r_Msg:string(ENC_UTF_8)
end

-- 定義したプロトコルをTCPポート番号を指定した紐づけ
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(20406, SampleProto)      


各行の細かい解説については今回は省略しますので、前回の記事を参照してください。

taekwongineer.hatenablog.jp

上記コードのプラグインでサンプルプロトコルの解析を行った場合、1つのIPパケットに複数のサンプルプロトコルメッセージが含まれる場合の解析結果は以下の図2-1のように表示されることになります。


f:id:taekwongineer:20200419171044p:plain
図2-1 複数メッセージを含むパケットの解析結果(複数メッセージ対応未考慮)

見てわかる通り、1番目のメッセージは解析され、Message SizeとMessageの内容が表示されますが、後に続くメッセージについては解析が行われず、何も表示されない状態となってしまっています。

そこで上記のコードに対して、以下の改良を行います。
(1)1つのIPパケットに複数のサンプルプロトコルメッセージが含まれていても、全て解析を実行できるようにする。
(2)各メッセージのMessage Size及びMesssageの値を、プロトコルツリーのSample Protocolの文字列の右側に表示できるようにする

(2)については、この記事の本筋からは外れますが、知っておくと便利なので、併せて解説します。

3.解析プラグインのサンプルコード(複数メッセージ対応考慮)

改良を行ったコードを以下に示します。

--独自プロトコルの定義
SampleProto = Proto.new("sample","SampleProtocol")

--プロトコルフィールドの定義
f_MsgSize = ProtoField.new("MessageSize","sample.size",ftypes.UINT16)
f_Msg = ProtoField.new("Message","sample.msg",ftypes.STRING)

--定義したプロトコルフィールドをプロトコルフィールド配列に登録
SampleProto.fields = {f_MsgSize,f_Msg}

-- dissector関数の定義
function SampleProto.dissector(buffer, pinfo, tree)

    -- dissector関数内で使用するローカル変数の定義
    local MsgNum = 0    -- 1パケット内に含まれるSampleProtocolのメッセージ数
    local BufLen = buffer:len()    -- 引数bufferのデータ長
    local BufLeft = buffer:len()    -- buffer内の未読込データの残りバイト数(初期値はBuflen)
    local BufPos = 0    -- bufferのデータ読込開始位置
    local r_BufRange    -- サンプルプロトコルメッセージのデータ範囲
    local r_MsgSize    -- MessageSizeのデータ範囲
    local r_Msg    -- Messageのデータ範囲
    
    -- BufLeftが3以上のとき、解析処理を継続
    while BufLeft >= 3 do
        ---- bufferからデータを読込み ----
        -- MessageSizeを読込み
        r_MsgSize = buffer(BufPos, 2)

        -- Messageを読込み
        r_Msg = buffer(BufPos + 2, r_MsgSize:uint())

        -- SampleProtocolメッセージ全体を読込み
        r_BufRange = buffer(BufPos, 2 + r_MsgSize:uint())

        ---- プロトコルツリーに情報を追加
        -- treeにサブツリーとして"SampleProtocol"を追加
        local subtree = tree:add(SampleProto, r_BufRange)

        -- subtreeに上記で定義した各プロトコルフィールドの値を追加
        subtree:add(f_MsgSize, r_MsgSize) -- MessageSize
        subtree:add(f_Msg, r_Msg, r_Msg:string(ENC_UTF_8), "Message:" .. r_Msg:string(ENC_UTF_8)) -- Msg

        -- subtreeのトップにMessageSizeとMessageの情報を追記する
       subtree:append_text(",MessasgeSize:" .. r_MsgSize:uint() .. ",Message:" .. r_Msg:string(ENC_UTF_8))

        ---- bufferのデータ読込開始位置を更新
        BufPos = BufPos + 2 + r_MsgSize:uint()

        ---- buffer内の未読込データ数を更新
        BufLeft = BufLeft - (2 + r_MsgSize:uint())

        -- SampleProtocolメッセージ数を更新
        MsgNum = MsgNum + 1
    end

    ---- プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Sample"

    -- "info"列の表示(MsgNumの値に応じて分岐)
    if MsgNum == 1 then
    -- メッセージ数が1個の場合は、MessageSizeとMessageの値を表示
    pinfo.cols.info = "MessageSize:" .. r_MsgSize:uint() .. "Message:" .. r_Msg:string(ENC_UTF_8)
    else
    -- メッセージ数が2個以上の場合は、メッセージ数を表示
    pinfo.cols.info = "Thispacketcontains" .. MsgNum .. "messages."
    end
end

-- 定義したプロトコルをTCPポート番号を指定した紐づけ
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(20406, SampleProto)

改良といってもそんなに大したことはしていません。
(1)1つのIPパケットに複数のサンプルプロトコルメッセージが含まれていても、全て解析を実行できるようにする。
↑に対しては、whileループを組んで、bufferのデータ長に対して、未読込のデータ数が3byte以上残っていれば、解析処理を継続するようにコードを修正しました。

(2)各メッセージのMessage Size及びMesssageの値を、プロトコルツリーのSample Protocolの文字列の右側に表示できるようにする
↑に対しては、subtree:append_text("追記文")の形式で、プロトコルツリーの表示に追記を行いました。

上記コードのプラグインでサンプルプロトコルの解析を行うと、1つのIPパケットに複数メッセージが含まれる場合の解析結果は図3-1のようになります。


f:id:taekwongineer:20200419174152p:plain
図3-1 複数メッセージを含むパケットの解析結果(複数メッセージ対応考慮)

見てわかる通り、TCPプロコルのヘッダツリーの後に、サンプルプロトコルのツリーが2つ追加されるようになりました。

また、各ツリーのSample Protocolのテキスト表示の右側に、Message Size及びMeesageの内容を表示できるようになりました。

4.まとめ

今回の記事では、1つのIPパケットに独自プロトコルのメッセージが複数含まれる場合の解析プラグインの作成方法について解説しました。
一工夫がいると書きましたが、特別なAPIを使うわけではなく、一般的なプログラムのアルゴリズムだけで実現可能です。(;^_^A

また、今回のサンプルコードで使用したsubtree:append_text("追記文")は、知っておくととても便利かと思います。

2020/05/24 追記
独自プロトコルのメッセージが複数のIPパケットに跨って配信される場合に再組立てして解析する方法について記事を書きました。
taekwongineer.hatenablog.jp