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

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

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

TCP/IP通信を扱う仕事をしていると、Wiresharkを使ってネットワーク内を流れるパケットのプロトコル解析を行うという機会は多々あります。

解析対象のプロトコルは、ARP、IP、TCPUDPなど標準的なプロトコルから、システム毎に定義されている独自プロトコルまで様々です。

このとき、システム毎に独自に定義されているプロトコルのデータを解析するには、デフォルトのWiresharkの環境ですと、単なるバイト列のデータとして表示されてしまい、解析にとても手間がかかります。

しかし実は、Wiresharkには、luaという言語で書かれたプラグインを追加することで、独自プロトコルを解析できるようになる機能があるのです!

まぁでも実際には「実は」というほどのことでもなく、ネット上でググればサンプルコードはたくさん出てくるし、Wiresharkの公式ページの「Wireshark Developer's Guide」(以降、「開発ガイド」と記載)にも情報はたくさん書かれています(;^_^A

ただ、ネット上のluaのサンプルコードは記述方法が何通りかあるし、公式ページの開発ガイドは全部英語な上、サンプルコードも少なくとても分かりづらいのです・・・

そこであれこれ調べまわった結果、自分だったらこう書く!!というお作法を備忘録として書き留めます。

この記事の目次

1.プラグイン追加でできることの概要

この記事で扱う解析対象の独自プロトコルの例として、以下の図1-1のメッセージフォーマットを持つUDPパケットを扱います。


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

4種類の制御データ(ControlData1~ControlData4)とユーザデータ(Payload)で構成されるメッセージフォーマットを想定します。
(この記事のために定義したメッセージフォーマットであり、実在するものではありません。)

Wiresharkにこのメッセージフォーマットに対応したプラグインを追加すると、追加前と追加後でそれぞれ図1-2及び図1-3のように解析結果の表示が変わります。


f:id:taekwongineer:20200329171329p:plain
図1-2 プラグイン追加前の画面表示


f:id:taekwongineer:20200329171413p:plain
図1-3 プラグイン追加後の画面表示

図1-3の方がメッセージフォーマットの内容が分かりやすいのは一目瞭然です。

以後、独自プロトコルの解析プラグインの作成手順について説明していきます。

この記事で記載する解析プラグインのサンプルコードの説明には、一部以下の記事を参考にさせて頂いています。

io.cyberdefense.jp

2.解析プラグインの作成手順

まず、図1-1のプロトコルの解析プラグインのサンプルコードを以下に示します。

-- original.lua
-- (1)独自プロトコルの定義
OrgProto = Proto.new("original", "Original Protocol")

-- (2)プロトコルフィールドの定義
f_ContData1 = ProtoField.new("Control Data1" , "original.cont1"  , ftypes.UINT16 , nil, base.DEC_HEX)
f_ContData2 = ProtoField.new("Control Data2" , "original.cont2"  , ftypes.UINT8  , nil, base.DEC_HEX)
f_ContData3 = ProtoField.new("Control Data3" , "original.cont3"  , ftypes.UINT8  , nil, base.DEC_HEX, 0xF0)
f_ContData4 = ProtoField.new("Control Data4" , "original.cont4"  , ftypes.UINT8  , nil, base.DEC_HEX, 0x0F)
f_Payload   = ProtoField.new("Payload"       , "original.payload", ftypes.STRING)

-- (3)定義したプロトコルフィールドをプロトコルフィールド配列に登録
OrgProto.fields = {f_ContData1, f_ContData2, f_ContData3, f_ContData4, f_Payload}

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

    -- (5)dissector関数内で使用するローカル変数の定義及びbufferからの値の取得
    local ContData1 = buffer(0, 2):uint()
    local ContData2 = buffer(2, 1):uint()
    local ContData3 = bit32.rshift(buffer(3, 1):uint(), 4)
    local ContData4 = bit32.band(buffer(3, 1):uint(), 0x0F)
    local Payload = buffer(4, buffer:len()-4):string()
    
    -- (6)プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Original"
    -- "info"列の表示
    pinfo.cols.info = "Cont1:" .. ContData1 .. " Cont2:" .. ContData2 .. " Cont3:" .. ContData3 .. " Cont4:" .. ContData4 .. " Payload:" .. Payload
    
    -- (7)treeに"Original Protocol"の情報を追加 
    -- treeにサブツリーとして"Original Protocol"を追加
    local subtree = tree:add( OrgProto, buffer() )

    -- サブツリー内に上記で定義した各プロトコルフィールドの値を追加
    subtree:add( f_ContData1, buffer(0, 2) )
    subtree:add( f_ContData2, buffer(2, 1) )
    subtree:add( f_ContData3, buffer(3, 1) )
    subtree:add( f_ContData4, buffer(3, 1) )
    subtree:add( f_Payload, buffer(4, buffer:len()-4 ) )
    
end

-- (8)定義したプロトコルをUDPポート番号を指定した紐づけ
udp_table = DissectorTable.get("udp.port")
udp_table:add(20329, OrgProto)

「--」で始まる行はコメント部分で、実質24行のコードとなります。

作成の手順としては、コメント部分に示した(1)~(8)の手順に沿ってコードの記述を進めていきます。

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

(1)独自プロトコルの定義

初めに、独自プロトコルの定義を記述します。

-- (1)独自プロトコルの定義
OrgProto = Proto.new("original", "Original Protocol")

Proto.new(name, desc)
name:表示フィルタ使用時のフィルタ名称
desc:プロトコルフィールド

の形式で記述します。

筆者はlua言語の知識は皆無に等しいのですが、オブジェクト指向言語的には、Protoクラスのオブジェクトを引数name、descで初期化するといったイメージでしょうか。

上記の引数の内容は、以下の図2-1の箇所に反映されます。


f:id:taekwongineer:20200329215103p:plain
図2-1 Proto.new()の引数の反映箇所

ちなみに、開発ガイドの「11.6. Functions For New Protocols And Dissectors」では、Proto.new()の使用法について、以下のように定義されています。

11.6.5.1. Proto.new(name, desc)
Arguments
name
The name of the protocol.
desc
A Long Text description of the protocol (usually lowercase).
Returns
The newly created protocol.

引数の説明を和訳すると、
name:プロトコルの名称
desc:プロトコルの概要

と訳せますが、descがプロトコルの概要というのはちょっと違う気が・・・(;^_^A

(2)プロトコルフィールドの定義

2番目に、プロトコルフィールドの定義を記述します。

-- (2)プロトコルフィールドの定義
f_ContData1 = ProtoField.new("Control Data1" , "original.cont1"  , ftypes.UINT16 , nil, base.DEC_HEX)
f_ContData2 = ProtoField.new("Control Data2" , "original.cont2"  , ftypes.UINT8  , nil, base.DEC_HEX)
f_ContData3 = ProtoField.new("Control Data3" , "original.cont3"  , ftypes.UINT8  , nil, base.DEC_HEX, 0xF0)
f_ContData4 = ProtoField.new("Control Data4" , "original.cont4"  , ftypes.UINT8  , nil, base.DEC_HEX, 0x0F)
f_Payload   = ProtoField.new("Payload"       , "original.payload", ftypes.STRING)

プロトコルフィールドとは、以下の図2-2で示す箇所を指します。


f:id:taekwongineer:20200329181754p:plain
図2-2 プロトコルフィールド

プロトコルフィールドの定義は、各プロトコルフィールド毎に以下の形式で記述します。
ProtoField.new(name, abbr, type, [valuestring], [base], [mask], [desc])
name:プロトコルフィールド名
abbr:表示フィルタ使用時のプロトコルフィールドのフィルタ名称
type:データ型
valuestring(省略可):本サンプルでは不使用のため、説明省略。base以降の引数を使用時に"nil"を設定
base(省略可):表示オプション
mask(省略可):データ型のうち、表示対象とするbit位置を指定する (本サンプルではControlData3、ControlData4に使用)
desc(省略可):プロトコルフィールドの説明。本サンプルでは不使用。


第3引数のtypeに設定可能な値については、開発ガイドで以下のように定義されています。
本サンプルでは、ftypes.UINT16、ftypes.UINT8、ftypes.STRINGの3種類を使用しています。

type
Field Type: one of: ftypes.BOOLEAN, ftypes.CHAR, ftypes.UINT8, ftypes.UINT16, ftypes.UINT24, ftypes.UINT32, ftypes.UINT64, ftypes.INT8, ftypes.INT16, ftypes.INT24, ftypes.INT32, ftypes.INT64, ftypes.FLOAT, ftypes.DOUBLE , ftypes.ABSOLUTE_TIME, ftypes.RELATIVE_TIME, ftypes.STRING, ftypes.STRINGZ, ftypes.UINT_STRING, ftypes.ETHER, ftypes.BYTES, ftypes.UINT_BYTES, ftypes.IPv4, ftypes.IPv6, ftypes.IPXNET, ftypes.FRAMENUM, ftypes.PCRE, ftypes.GUID, ftypes.OID, ftypes.PROTOCOL, ftypes.REL_OID, ftypes.SYSTEM_ID, ftypes.EUI64 or ftypes.NONE.


出展:11.6. Functions For New Protocols And Dissectors

次に第5引数のbaseに設定可能な値については、開発ガイドで以下のように定義されています。

base (optional)
The representation, one of: base.NONE, base.DEC, base.HEX, base.OCT, base.DEC_HEX, base.HEX_DEC, base.UNIT_STRING or base.RANGE_STRING.

出展:11.6. Functions For New Protocols And Dissectors

筆者はbase.DEC_HEXが個人的にお気に入りです。
base.DEC_HEXを選択すると、以下の図2-3にように10進数表示の後に、()付きで16進数が表示されます。


f:id:taekwongineer:20200329231815p:plain
図2-3 base.DEC_HEXによるデータ表示形式

(3)定義したプロトコルフィールドをプロトコルフィールド配列に登録

(2)で定義したProtoFieldクラスの各オブジェクトを(1)で定義したProtoクラスのオブジェクトOrgProtoのメンバ変数fieldsに登録します。
ここは定型文として覚えておけばよいでしょう。

-- (3)定義したプロトコルフィールドをプロトコルフィールド配列に登録
OrgProto.fields = {f_ContData1, f_ContData2, f_ContData3, f_ContData4, f_Payload}

(4)dissector関数の定義

(1)で定義したOrgProtoのメンバ関数にdissector(buffer, pinfo, tree)を定義します。
こちらも定型文として覚えてけば良いでしょう。

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

--  この中に解析処理を記述していきます

end

ちなみにdissector関数の引数buffer、pinfo、treeはそれぞれ

buffer:パケット全体イメージのうち、独自プロトコル以降のデータが格納されたもの
pinfo:パケットインフォメーション情報
tree:パケット詳細部の情報

に相当します。

(5)dissector関数内で使用するローカル変数の定義及びbufferからの値の取得

    -- (5)dissector関数内で使用するローカル変数の定義及びbufferからの値の取得
    local ContData1 = buffer(0, 2):uint()
    local ContData2 = buffer(2, 1):uint()
    local ContData3 = bit32.rshift(buffer(3, 1):uint(), 4)
    local ContData4 = bit32.band(buffer(3, 1):uint(), 0x0F)
    local Payload = buffer(4, buffer:len()-4):string()

この部分は解析プラグインを作成する上で必須ではないので、サラッと流します。

まず、dissector関数の第1引数bufferですが、開発ガイドによると、「Tvb」というクラスのオブジェクトになります。

そして、

buffer(offset, length)

の形式で、bufferのoffsetバイト目から、lengthバイトを切り出し、「TvbRange」というクラスのオブジェクトを返します。

さらに、このTvbRangeクラスのオブジェクトに対し、

buffer(offset, length):uint()

の形式で、符号なし整数の型に変換します。

:uint()の符号なし整数以外にも種類はたくさんありますが、詳細は開発ガイドを参照してください。
https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Tvb.html#lua_class_TvbRange

その他には・・・

bit32.rshift(A, B)
AをBビット右にシフトします。

bit32.band(A, B)
AとBのANDを計算します。

buffer:len()
Tvbクラスのオブジェクトbufferのデータ長(バイト)を取得します。

(6)プロトコル情報を設定

ここではdissector関数の第2引数であるpinfoを使用します。

    -- (6)プロトコル情報を設定
    -- "Protocol"列の表示
    pinfo.cols.protocol = "Original"
    -- "info"列の表示
    pinfo.cols.info = "Cont1:" .. ContData1 .. " Cont2:" .. ContData2 .. " Cont3:" .. ContData3 .. " Cont4:" .. ContData4 .. " Payload:" .. Payload

上記のように記述することで、以下の図2-4のようにGUIプロトコル情報が反映されます。


f:id:taekwongineer:20200405232700p:plain
図2-4 pinfoへのプロトコル情報設定

(7)treeに"Original Protocol"の情報を追加

ここでは、dissector関数の第3引数であるtreeを使用します。

    -- (7)treeに"Original Protocol"の情報を追加 
    -- treeにサブツリーとして"Original Protocol"を追加
    local subtree = tree:add( OrgProto, buffer() )

    -- サブツリー内に上記で定義した各プロトコルフィールドの値を追加
    subtree:add( f_ContData1, buffer(0, 2) )
    subtree:add( f_ContData2, buffer(2, 1) )
    subtree:add( f_ContData3, buffer(3, 1) )
    subtree:add( f_ContData4, buffer(3, 1) )
    subtree:add( f_Payload, buffer(4, buffer:len()-4 ) )

まず、

local subtree = tree:add( OrgProto, buffer() )

で、IPv4プロトコルUDPプロトコルの後に、"Original Protocol"を表示させます。

そして、

subtree:add( ProtoField, TvbRange )

の形式で、(2)で定義したプロトコルフィールドと、buffer内のデータの紐づけをします。



※ 2020/4/19 追記
サブツリーへのプロトコル情報の追加方法には、上記の記述以外にもいくつか方法があります。
他の記事等で紹介されているサンプルコードの記述方法も一通りではないため、混乱を招くモトになっているかと思います。
(筆者も初めはかなり混乱しました・・・)

開発ガイドによると、サブツリーへのプロトコル情報の追加方法は、以下のように規定されています。

11.7.1.2. treeitem:add([protofield], [tvbrange], [value], [label])
Adds a child item to this tree item, returning the new child TreeItem.
If the ProtoField represents a numeric value (int, uint or float), then it’s treated as a Big Endian (network order) value.
This function has a complicated form: 'treeitem:add([protofield,] [tvbrange,] value], label)', such that if the first argument is a ProtoField or a Proto, the second argument is a TvbRange, and a third argument is given, it’s a value; but if the second argument is a non-TvbRange, then it’s the value (as opposed to filling that argument with 'nil', which is invalid for this function). If the first argument is a non-ProtoField and a non-Proto then this argument can be either a TvbRange or a label, and the value is not in use.


うん・・・全部英語でよくわかんねw(T_T)

protofield、tvrange、value、labelの4つの引数は全てオプション扱いで必須ではないらしい・・・

というわけで、「こうしたら、こうなる」の結果をいくつか書きます。

ケース①:treeitem:add([protofield], [tvbrange])
これはこの記事のサンプルコードで使用している記述方法です。
筆者はこの記述方法が一番シンプルで分かりやすいと思います。
以降のケースは、ケース①を基準として差分を説明していきます。

ケース②:treeitem:add([tvbrange], [label])
ケース①のadd関数の引数から[protofiled]を省略し、代わりに[label]を追加したケースです。

サンプルコードのControl Data1の 記述方法を本ケースの記述方法に修正した例を以下に示します。

    -- (7)treeに"Original Protocol"の情報を追加 
    -- treeにサブツリーとして"Original Protocol"を追加
    local subtree = tree:add( OrgProto, buffer() )

    -- サブツリー内に上記で定義した各プロトコルフィールドの値を追加
    subtree:add( buffer(0, 2), "Control Data1: " .. buffer(0, 2):uint() ..  string.format("(0x%04x)", buffer(0,2):uint()) .. "  ←引数[label]による記述")
    subtree:add( f_ContData2, buffer(2, 1) )
    subtree:add( f_ContData3, buffer(3, 1) )
    subtree:add( f_ContData4, buffer(3, 1) )
    subtree:add( f_Payload, buffer(4, buffer:len()-4 ) )

この記述方法により、GUIの表示は以下の図2-5のようになります。


f:id:taekwongineer:20200419124507p:plain
図2-5 ケース②適用後のGUI表示

引数[label]の内容がそのままGUIに反映されます。

この記述方法を使用した場合は、(2)で定義したプロトコルフィールドとの紐づけが行われないため、表示フィルタによる検索ができなくなります。

・表示フィルタによる検索が不要
プロトコル情報の表示内容を独自の形式で記述したい

という場合に使用するのがよい記述方法かと思います。


ケース③:treeitem:add([protofield], [tvbrange], [value], [label])
全ての引数を使用したケースになります。

ケース②と比較し、

・表示フィルタによる検索が必要
プロトコル情報の表示内容を独自の形式で記述したい

という場合に使用するのがよい記述方法かと思います。

ケース②と同様、サンプルコードのControl Data1の 記述方法を本ケースの記述方法に修正した例を以下に示します。

    -- (7)treeに"Original Protocol"の情報を追加 
    -- treeにサブツリーとして"Original Protocol"を追加
    local subtree = tree:add( OrgProto, buffer() )

    -- サブツリー内に上記で定義した各プロトコルフィールドの値を追加
    subtree:add( f_ContData1, buffer(0, 2), buffer(0, 2):uint(),  "Control Data1: " .. buffer(0, 2):uint() ..  string.format("(0x%04x)", buffer(0,2):uint()) .. "  ←引数[label]による記述")
    subtree:add( f_ContData2, buffer(2, 1) )
    subtree:add( f_ContData3, buffer(3, 1) )
    subtree:add( f_ContData4, buffer(3, 1) )
    subtree:add( f_Payload, buffer(4, buffer:len()-4 ) )

第3引数の[value]には、第2引数の[tvbrange]の値を代入します。
表示フィルタによる検索は、valueの値を対象に検索が行われるようです。

GUIの表示はケース②と同じなので、省略します。

(8)定義したプロトコルUDPポート番号を指定した紐づけ

最後に、定義したプロトコルUDPポート番号を指定した紐づけを行います。
今回は例として、UDPポート番号20329を"Original Protocol"として紐づけています。

-- (8)定義したプロトコルをUDPポート番号を指定した紐づけ
udp_table = DissectorTable.get("udp.port")
udp_table:add(20329, OrgProto)

ここの書き方は定型文として覚えてしまう方が良いでしょう。

3.解析プラグインの適用方法

2.で作成した解析プラグイン(original.lua)の適用は以下の手順で実施します。
とても簡単です。

Wiresharkのインストールデータの中にあるinit.luaファイルを開き、最下行にdofile("original.lua")を追記する。
②init.luaと同じディレクトリに作成したoriginal.luaファイルを格納する。

4. まとめ

ここまでUDPで伝送される独自プロトコルデータの解析プラグインの作成手順について説明しました。

UDPパケットで、制御情報の構成が固定的なものであれば、上記の(1)~(8)の手順で、luaのコードを記述していけば、割と簡単に解析プラグインを作成することができるかと思います。

ただ、これがTCPパケットなると、扱うプロトコルの内容にもよりますが、もう少し色々と考慮してプラグインのコードを書かないと、思うように解析ができないケースが出てくるので、次回はTCPパケットの独自解析プラグインの作成方法についてまとめたいと思います。

taekwongineer.hatenablog.jp