Wireshark Lua スクリプト徹底解説

Wireshark Luaスクリプト徹底解説

はじめに

ネットワークの解析やトラブルシューティングにおいて、Wiresharkはデファクトスタンダードとも言える強力なツールです。パケットキャプチャ、プロトコル解析、統計情報の表示など、その機能は多岐にわたります。しかし、標準でサポートされていないカスタムプロトコルを解析したい場合や、特定の解析作業を自動化したい場合、あるいは独自の統計情報を収集したい場合など、Wiresharkの標準機能だけでは対応できない場面も出てきます。

ここで登場するのが、WiresharkのLuaスクリプト機能です。Luaは軽量で拡張性の高いスクリプト言語であり、Wiresharkに組み込まれることで、ユーザーが独自のプロトコル解析モジュール(Dissector)を作成したり、既存の機能を拡張したり、特定の処理を自動化したりすることを可能にします。Luaスクリプトを利用することで、Wiresharkは単なるパケットビューアから、カスタマイズ可能な強力な解析プラットフォームへと進化します。

なぜLuaが選ばれたのでしょうか? LuaはC言語との連携が容易であり、実行速度が比較的速く、組み込みシステムやアプリケーションの拡張言語として広く利用されています。Wiresharkのようにパフォーマンスが要求されるツールにおいて、軽量かつ効率的に機能を追加できるLuaは理想的な選択肢でした。

この記事では、WiresharkのLuaスクリプト機能について、基本的な使い方からプロトコルDissectorの作成、さらには応用的な機能までを網羅的に解説します。対象読者は、Wiresharkを日常的に利用しているネットワークエンジニア、セキュリティアナリスト、プロトコル開発者、そしてWiresharkをさらに深く使いこなしたいと考えているすべての人です。Lua言語の基本的な知識があることが望ましいですが、必須ではありません。必要に応じてLuaの基本的な文法にも触れながら解説を進めます。

この記事を通じて、Wireshark Luaスクリプトの強力さと柔軟性を理解し、自身の解析作業や開発に活用できるようになることを目指します。さあ、Wiresharkのカスタマイズ可能な世界へ飛び込みましょう。

Luaスクリプトの基本

WiresharkにおけるLuaスクリプトは、主に以下の目的で使用されます。

  1. プロトコル解析 (Dissector): 標準でサポートされていないカスタムプロトコルや、既存プロトコルの特殊な拡張形式を解析し、Wiresharkのツリー表示やカラム表示に反映させます。
  2. 統計情報 (Tapper, Listener, Stats): パケットデータから特定の情報を抽出し、集計や統計表示を行います。
  3. GUI拡張: カスタムメニューやダイアログを追加するなど、ユーザーインターフェイスを拡張します。
  4. その他の処理: 特定の条件を満たすパケットに対してアクションを実行するなど、自動化された処理を行います。

これらの目的に応じて、Wiresharkは様々なLua API(Application Programming Interface)を提供しています。

スクリプトの実行方法

Wiresharkは起動時に特定のディレクトリにあるLuaスクリプトファイルを自動的に読み込み、実行します。スクリプトファイルを配置する主な場所は以下の通りです。

  • Wiresharkのpersonal configuration directory内のpluginsディレクトリ:
    このディレクトリはOSやWiresharkのバージョンによって異なります。Wiresharkのメニューから「ヘルプ」->「About Wireshark」->「Folders」タブを選択すると、「Personal Configuration」ディレクトリの場所を確認できます。その中にpluginsという名前のディレクトリを作成し、その中にLuaスクリプトファイル(.lua拡張子)を配置します。これが最も一般的な方法です。
  • Wiresharkインストールディレクトリ内のplugins/(version)ディレクトリ:
    Wiresharkのインストールディレクトリにもpluginsディレクトリがありますが、ここに配置したスクリプトは全てのユーザーに影響し、Wiresharkのアップデートで上書きされる可能性があるため、個人用のスクリプト配置には推奨されません。
  • init.luaファイル:
    Personal Configuration directoryの直下にはinit.luaという特別なファイルがあります。このファイルはWireshark起動時に最初に実行され、他のLuaスクリプトファイルを読み込んだり、初期設定を行ったりするのに使われます。例えば、dofile()関数を使って他のスクリプトを読み込むことができます。
    lua
    -- init.lua の例
    print("Loading custom Wireshark Lua scripts...")
    dofile(get_perscon_dir() .. "/plugins/my_custom_dissector.lua")
    dofile(get_perscon_dir() .. "/plugins/my_stats_script.lua")
    print("Custom scripts loaded.")

    get_perscon_dir()はWiresharkが提供する関数で、personal configuration directoryのパスを取得できます。

スクリプトファイルを配置したら、Wiresharkを再起動します。スクリプトにエラーがなければ、機能が追加されます。エラーがある場合は、Wiresharkのメニューから「ツール」->「Lua」->「Console」を開くと、エラーメッセージを確認できます。

また、コマンドラインから特定のスクリプトを実行することも可能です。例えば、tsharkコマンドでキャプチャファイルに対してスクリプトを実行する場合に使用します。

Lua言語の基本的な文法

Wiresharkスクリプトを書く上で最低限知っておくべきLuaの文法要素を簡単に説明します。

  • コメント: --で始まり行末までがコメントになります。複数行コメントは--[[ ... ]]で囲みます。
  • 変数: 変数の宣言にキーワードは不要です。代入によって型が決まります。デフォルトではグローバル変数になりますが、localキーワードを付けてローカル変数にすることが推奨されます。
    lua
    local my_number = 123
    local my_string = "hello"
    local my_boolean = true
    local my_nil = nil -- 値がないことを示す
  • データ型: nil, boolean, number, string, function, userdata, thread, table があります。userdataはWiresharkのオブジェクト(例: Proto, Tvb)などが該当します。
  • テーブル (Table): Luaの唯一の構造化データ型です。配列としても、連想配列(ハッシュマップ)としても使えます。
    “`lua
    local my_array = {10, 20, 30} — インデックスは1から始まる
    print(my_array[1]) — 出力: 10

    local my_map = {name = “Alice”, age = 30}
    print(my_map[“name”]) — 出力: Alice
    print(my_map.age) — 出力: 30 (ドット演算子も使える)

    local mixed_table = {
    “apple”,
    “banana”,
    count = 2,
    colors = {“red”, “yellow”}
    }
    * **制御構造:**
    * `if-then-else-end`:
    lua
    if score > 90 then
    print(“Excellent”)
    elseif score > 70 then
    print(“Good”)
    else
    print(“Needs improvement”)
    end
    * `for`ループ: 数値forとジェネリックforがあります。lua
    — 数値for
    for i = 1, 5 do
    print(i) — 1から5まで出力
    end
    for i = 5, 1, -1 do
    print(i) — 5から1まで逆順に出力
    end

    -- ジェネリックfor (テーブルの反復などに使用)
    for key, value in pairs(my_map) do
        print(key, value)
    end
    for index, value in ipairs(my_array) do -- 配列部分のみをインデックス順に反復
        print(index, value)
    end
    ```
    
    • whileループ:
      lua
      local i = 1
      while i <= 5 do
      print(i)
      i = i + 1
      end
    • 関数: functionキーワードで定義します。
      lua
      local function add(a, b)
      return a + b
      end
      print(add(3, 5)) -- 出力: 8

Wiresharkスクリプトでは、これらの基本要素とWiresharkが提供するAPIオブジェクトを組み合わせて記述していきます。

Wireshark Lua APIの概要

Wiresharkが提供するLua APIは、グローバル変数としてアクセスできる様々なクラス(メタテーブル)や関数で構成されています。主要なものには以下のようなものがあります。

  • Proto: プロトコルを定義するためのクラス。プロトコル名、略称、フィールドなどを定義します。
  • Field: プロトコルフィールドを定義するためのクラス。フィールド名、型、表示形式などを定義します。
  • Dissector: 特定のプロトコルやデータ形式を解析する関数を定義・登録するためのクラス。
  • Tvb: Packet data (tvbuff) を操作するためのオブジェクト。データの長さ、バイト列の読み取り、範囲指定などができます。
  • Pinfo: Packet information を保持するオブジェクト。パケット番号、時間、カラム表示情報などを扱います。
  • DissectorTable: プロトコルやポート番号に基づいてDissectorを検索・登録するためのテーブル。
  • Tapper: 特定のパケット情報を集計し、統計情報として表示するためのクラス。
  • Listener: パケットストリーム(TCPセッションなど)全体の状態を追跡し、イベントに応じた処理を行うためのクラス。
  • StatsTree: カスタムの統計ツリーを作成するためのクラス。
  • Preferences: スクリプトの設定項目をWiresharkのPreferencesダイアログに追加するためのクラス。

これらのAPIオブジェクトをインスタンス化したり、メソッドを呼び出したりすることで、Wiresharkの機能を拡張していきます。

プロトコル Dissector の作成

プロトコル Dissector は、Wireshark Luaスクリプトの最も一般的で強力な用途の一つです。ネットワークパケットの生データ(バイト列)を解析し、Wiresharkのパケット詳細ツリー表示やカラム表示に、プロトコルの構造やフィールドの値を分かりやすく表示させる役割を担います。

dissector の役割と仕組み

Wiresharkはキャプチャしたパケットを順次処理し、各レイヤーのプロトコルを特定して対応するDissectorを呼び出します。例えば、Ethernetフレーム内のIPパケットを見つけるとIP Dissectorを、IPパケット内のTCPセグメントを見つけるとTCP Dissectorを呼び出す、といった具合です。TCP Dissectorはさらにポート番号を見て、HTTP、FTPなどのアプリケーション層プロトコルを特定し、対応するDissectorを呼び出します。

LuaでカスタムDissectorを作成する場合、この既存のプロトコルスタックのどこかに組み込むことになります。一般的には、TCPやUDPの特定のポート番号に紐付けたり、既存プロトコルのペイロードとして指定したりします。

基本的なDissectorの作成手順は以下の通りです。

  1. Protoオブジェクトを作成し、プロトコルのメタ情報を定義します(名前、略称など)。
  2. プロトコル内で解析したいフィールドに対応するFieldオブジェクトを定義します。
  3. パケットデータを実際に解析し、定義したFieldオブジェクトに値を割り当てるためのdissector関数を実装します。
  4. 作成したDissectorを、既存のプロトコルスタック内の適切な場所(例: TCPポート8000番)に登録します。

基本構造:Protoオブジェクトとフィールド定義

まず、プロトコルを定義するProtoオブジェクトを作成します。これは、Luaのグローバル変数として扱うのが一般的です。

lua
-- myproto_dissector.lua
local myproto = Proto("My Custom Protocol", "MYPROTO")

Protoコンストラクタの第1引数はプロトコルのフルネーム、第2引数はWireshark内で使用される略称です。この略称はフィルタリングなどにも使用できます。

次に、このプロトコルが持つフィールドを定義します。フィールドはFieldオブジェクトとして作成し、これも通常はグローバル変数として定義します。Fieldコンストラクタは、フィールドのフィルタ名、表示名、データ型、表示形式、値と表示文字列のマッピングなどを引数に取ります。

“`lua
local myproto = Proto(“My Custom Protocol”, “MYPROTO”)

— フィールドの定義
— 形式: Field.new(filter_name, display_name, type, format, value_map, mask, description)
local myproto_version = Field.new(“myproto.version”, “Version”, ftypes.UINT8)
local myproto_type = Field.new(“myproto.type”, “Type”, ftypes.UINT8, nil, {
[1] = “Request”,
[2] = “Response”,
[3] = “Notification”
})
local myproto_length = Field.new(“myproto.length”, “Length”, ftypes.UINT16, base.LITTLEENDIAN) — リトルエンディアンの場合
local myproto_data = Field.new(“myproto.data”, “Data”, ftypes.BYTES)
“`

ftypesはフィールドのデータ型を定義する定数テーブルです。UINT8, UINT16, UINT32, INT8, INT16, INT32, BYTES, STRING, BOOLEANなど様々な型があります。
baseは数値の表示形式を定義する定数テーブルです。DEC, HEX, OCT, DEC_HEX, DECIMAL, HEXIDECIMALなど。また、バイトオーダーを指定するLITTLEENDIAN, BIGENDIANもあります。
value_mapは数値と文字列を対応付けるテーブルで、数値の表示を分かりやすくするために使用します。

定義したFieldオブジェクトをProtoオブジェクトに関連付けます。これは、Protoオブジェクトのテーブルにfieldsというキーでフィールドのテーブルを代入することで行います。

“`lua
local myproto = Proto(“My Custom Protocol”, “MYPROTO”)

local myproto_version = Field.new(“myproto.version”, “Version”, ftypes.UINT8)
local myproto_type = Field.new(“myproto.type”, “Type”, ftypes.UINT8, nil, {
[1] = “Request”,
[2] = “Response”,
[3] = “Notification”
})
local myproto_length = Field.new(“myproto.length”, “Length”, ftypes.UINT16, base.BIGENDIAN) — ここではビッグエンディアンに変更
local myproto_data = Field.new(“myproto.data”, “Data”, ftypes.BYTES)

— Protoオブジェクトにフィールドを関連付け
myproto.fields = {
myproto_version,
myproto_type,
myproto_length,
myproto_data
}
“`

dissector関数の実装

dissector関数は、実際のパケットデータを受け取り、それを解析してWiresharkの表示ツリーに追加する処理を行います。この関数は、WiresharkがこのDissectorを呼び出すべきパケットを見つけたときに実行されます。

dissector関数は通常、以下の3つの引数を取ります。

  1. tvb: Packet data buffer。解析対象の生データが含まれています。Tvbオブジェクトです。
  2. pinfo: Packet information。パケット番号、時間、プロトコルスタック、カラム情報など、パケットに関するメタデータが含まれています。Pinfoオブジェクトです。
  3. tree: Protocol tree。解析結果を表示するためのツリー構造です。TreeItemオブジェクトです。

dissector関数の実装は、Protoオブジェクトのdissectorプロパティに代入します。

“`lua
local myproto = Proto(“My Custom Protocol”, “MYPROTO”)

— フィールド定義 (上記参照)
local myproto_version = Field.new(“myproto.version”, “Version”, ftypes.UINT8)
local myproto_type = Field.new(“myproto.type”, “Type”, ftypes.UINT8, nil, {
[1] = “Request”,
[2] = “Response”,
[3] = “Notification”
})
local myproto_length = Field.new(“myproto.length”, “Length”, ftypes.UINT16, base.BIGENDIAN)
local myproto_data = Field.new(“myproto.data”, “Data”, ftypes.BYTES)

myproto.fields = {
myproto_version,
myproto_type,
myproto_length,
myproto_data
}

— dissector 関数の実装
function myproto.dissector(tvb, pinfo, tree)
— パケット情報カラムの更新
pinfo.cols.protocol:set(“MYPROTO”)
— 必要に応じて info カラムも更新
— pinfo.cols.info:set(“MYPROTO Packet”)

-- プロトコルツリーのルート要素を追加
-- tree:add(proto_object, tvb_range, display_text)
local subtree = tree:add(myproto, tvb(), "My Custom Protocol Data") -- tvb() はtvb全体の範囲を示す

-- tvb からデータを読み込み、ツリーに追加
-- tvb:read_uint(offset, length, endian)
-- treeitem:add(field_object, tvb_range, value)
-- treeitem:add(field_object, tvb_range) -- tvb_rangeから値を自動で読み込む

local offset = 0 -- 現在の読み取り位置

-- Version (1 byte)
local version = tvb:uint(offset, 1)
subtree:add(myproto_version, tvb(offset, 1)) -- 値の読み取りとツリーへの追加を同時に行う
offset = offset + 1

-- Type (1 byte)
subtree:add(myproto_type, tvb(offset, 1))
offset = offset + 1

-- Length (2 bytes)
-- Lengthフィールドの値は、後続のDataフィールドの長さを示すと仮定
local data_length = tvb:uint(offset, 2, base.BIGENDIAN) -- ビッグエンディアンで読み込み
subtree:add(myproto_length, tvb(offset, 2))
offset = offset + 2

-- Data (variable length)
-- データ長がtvbの残りの長さよりも大きい場合は調整する
if data_length > tvb:len() - offset then
    data_length = tvb:len() - offset
end
subtree:add(myproto_data, tvb(offset, data_length))
-- offset = offset + data_length -- 次のフィールドがあればoffsetを更新

end
“`

tvb(offset, length)は、元のtvbオブジェクトから指定されたオフセットと長さの部分範囲を示す新しいTvbRangeオブジェクトを生成します。これはツリー表示でハイライトされる部分を指定するのに非常に重要です。tvb(offset, length)のように長さを省略すると、オフセットからデータの最後までが範囲となります。

tvb:uint(offset, length, endian)tvb:bytes(offset, length)などのメソッドを使って、指定されたオフセットからデータを読み取ります。

subtree:add(...)メソッドを使って、定義したフィールドとそれに対応するTvbRangeをツリーに追加します。これにより、Wiresharkのパケット詳細ペインにプロトコル構造が表示されます。

サブ Dissector の呼び出しとプロトコルスタックへの登録

作成したDissectorをどのパケットに対して実行するかをWiresharkに知らせる必要があります。これは、既存のDissectorテーブルに登録することで行います。最も一般的なのは、TCPまたはUDPの特定のポート番号に紐付ける方法です。

DissectorテーブルはDissectorTableオブジェクトを通じてアクセスできます。TCPポート用のテーブルはDissectorTable.get("tcp.port")で取得できます。

“`lua
— dissector 関数定義の後…

— Dissector を TCP ポート 8000 に登録
— DissectorTable:add(table_name, key, dissector_object)
local tcp_port_table = DissectorTable.get(“tcp.port”)
tcp_port_table:add(8000, myproto) — ポート番号 8000 の TCP パケットに myproto.dissector を適用

print(“MYPROTO dissector loaded and registered on TCP port 8000.”)
“`

これで、TCPポート8000番を送受信するパケットは、標準のTCP DissectorによってMYPROTO Dissectorに引き渡され、解析されるようになります。

同様に、UDPポートに登録する場合はDissectorTable.get("udp.port")を使用します。他のDissectorのペイロードとして呼び出したい場合は、そのDissectorが提供するテーブル名を確認する必要があります(例えば、Ethernet Type, IP Protocolなど)。

また、特定の条件(マジックバイトなど)に基づいてDissectorを呼び分けたい場合は、上位プロトコルのDissector内で条件分岐を行い、DissectorTable:get_dissector(table_name, key)で取得したDissectorオブジェクトをdissector:call(tvb_range, pinfo, tree)メソッドで呼び出すことも可能です。

lua
-- 例: 上位プロトコル Dissector 内で MYPROTO を呼び出す
-- if (マジックバイトがMYPROTOを示す場合) then
-- local myproto_dis = DissectorTable.get("tcp.port"):get_dissector(8000) -- Dissector オブジェクトを取得
-- if myproto_dis then
-- myproto_dis:call(tvb(offset, remaining_length), pinfo, tree) -- 特定の範囲を渡して呼び出す
-- end
-- else
-- -- 他のプロトコルを呼び出すか、生データとして表示
-- end

dissector:call()を使用すると、呼び出されたDissectorは指定されたtvb_rangeを自身のtvb引数として受け取ります。

例:シンプルなカスタムプロトコル (固定長ヘッダー) の dissector 作成

ここまでの内容をまとめたシンプルなカスタムプロトコルのDissectorスクリプトの完全な例を示します。

“`lua
— File: simple_custom_proto.lua

— プロトコルの定義
local scp = Proto(“Simple Custom Protocol”, “SCP”)

— フィールドの定義
local scp_message_id = Field.new(“scp.message_id”, “Message ID”, ftypes.UINT16, base.BIGENDIAN)
local scp_command = Field.new(“scp.command”, “Command”, ftypes.UINT8, nil, {
[0x01] = “Hello”,
[0x02] = “Status Request”,
[0x03] = “Data”
})
local scp_status = Field.new(“scp.status”, “Status”, ftypes.UINT8, nil, {
[0x00] = “Success”,
[0x01] = “Error”
})
local scp_payload = Field.new(“scp.payload”, “Payload”, ftypes.BYTES)

— Protoオブジェクトにフィールドを関連付け
scp.fields = {
scp_message_id,
scp_command,
scp_status,
scp_payload
}

— dissector 関数の実装
function scp.dissector(tvb, pinfo, tree)
— パケット情報カラムの更新
pinfo.cols.protocol:set(“SCP”)

local offset = 0
local pkt_len = tvb:len() -- パケット全体の長さ

-- SCP プロトコルツリーのルートを追加
local subtree = tree:add(scp, tvb(), "Simple Custom Protocol Data")

-- Message ID (2 bytes)
if offset + 2 <= pkt_len then
    subtree:add(scp_message_id, tvb(offset, 2))
    offset = offset + 2
else
    -- データが足りない場合のエラー処理
    subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Packet too short for Message ID")
    return -- 解析を中断
end

-- Command (1 byte)
if offset + 1 <= pkt_len then
    subtree:add(scp_command, tvb(offset, 1))
    offset = offset + 1
else
    subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Packet too short for Command")
    return
end

-- Status (1 byte) - CommandがDataの場合にのみ存在すると仮定
local command_val = tvb:uint(2, 1) -- オフセット2のコマンド値を取得

if command_val == 0x03 then -- Dataコマンドの場合
    if offset + 1 <= pkt_len then
        subtree:add(scp_status, tvb(offset, 1))
        offset = offset + 1
    else
        subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Packet too short for Status in Data command")
        return
    end
else
    -- Statusフィールドが存在しない場合でも、解析オフセットは進めない
    -- Statusフィールドを省略した場合は、ツリーに追加しない
end


-- Payload (remaining bytes)
local payload_len = pkt_len - offset
if payload_len > 0 then
    subtree:add(scp_payload, tvb(offset, payload_len))
end

-- これ以降に他のプロトコルが続く場合、それを呼び出す
-- 例えば、SCPペイロードがさらに別のプロトコルデータを含む場合
-- local next_dissector = DissectorTable.get("...")
-- next_dissector:call(tvb(offset, payload_len), pinfo, tree)

end

— Dissector を TCP ポート 12345 に登録
local tcp_port_table = DissectorTable.get(“tcp.port”)
tcp_port_table:add(12345, scp)

print(“SCP dissector loaded and registered on TCP port 12345.”)
“`

この例では、tvb:uint(offset, length, endian)を使ってデータを読み込み、subtree:add(field, tvb_range)でツリーに追加しています。また、パケット長を確認してデータが足りない場合にエラーを表示 (add_expert_info) して解析を中断しています。これは頑健なDissectorを作成する上で重要なステップです。PI_MALFORMEDPI_ERRORは、Expert Informationとして表示される情報の重要度と分類を示す定数です。

可変長フィールド、TLV形式などの解析

実際のプロトコルでは、固定長フィールドだけでなく、長さフィールドの値によって後続フィールドの長さが決まる可変長フィールドや、Type-Length-Value (TLV) 形式などがよく使われます。

可変長フィールドの解析は、直前の長さフィールドの値を読み取り、その値を使って次のフィールドのtvb_rangeや読み取り長さを決定します。

“`lua
— 例: Length フィールド (2バイト) が後続の Data フィールドの長さを示す場合
local offset = 0
local data_length = tvb:uint(offset, 2, base.BIGENDIAN) — 長さフィールドを読む
offset = offset + 2

— Data フィールドを読む (長さは data_length)
local data_tvb_range = tvb(offset, data_length)
subtree:add(myproto_data, data_tvb_range)
offset = offset + data_length
“`

TLV形式の解析は、ループを使って要素を順次処理します。

“`lua
— 例: TLV 形式 (Type: 1 byte, Length: 1 byte, Value: Length bytes)
local offset = 0
local pkt_len = tvb:len()

while offset < pkt_len do
— Type (1 byte)
if offset + 1 > pkt_len then break end — データ不足
local type = tvb:uint(offset, 1)
local type_tvb_range = tvb(offset, 1)
offset = offset + 1

-- Length (1 byte)
if offset + 1 > pkt_len then break end -- データ不足
local length = tvb:uint(offset, 1)
local length_tvb_range = tvb(offset, 1)
offset = offset + 1

-- Value (Length bytes)
if offset + length > pkt_len then
    -- データ不足、おそらく不正なパケット
    subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "TLV value length exceeds remaining data")
    break
end
local value_tvb_range = tvb(offset, length)
offset = offset + length

-- TLV 要素をツリーに追加(Type に応じてフィールドを切り替えることも可能)
local tlv_subtree = subtree:add("TLV (Type: " .. type .. ", Length: " .. length .. ")", tvb(type_tvb_range.offset, type_tvb_range.len + length_tvb_range.len + value_tvb_range.len))
tlv_subtree:add("Type: " .. type, type_tvb_range) -- Type を表示
tlv_subtree:add("Length: " .. length, length_tvb_range) -- Length を表示
tlv_subtree:add(myproto_tlv_value, value_tvb_range) -- 定義済みの Field を使うか、新しい Field を動的に作成

end
“`

TLVの解析では、各要素の範囲を正確に計算し、サブツリーとして追加することで、構造を分かりやすく表示できます。Typeに応じたフィールド定義やサブDissectorの呼び出しを行うことで、より詳細な解析が可能です。

TCP/UDP 上でのプロトコル解析 (ポート番号によるディスパッチ)

前述の例でも示しましたが、多くのカスタムプロトコルはTCPやUDPの上で動作します。これらのプロトコル層からのディスパッチは、Dissectorテーブルを使って行います。

Dissectorテーブルへの登録は通常、スクリプトファイルの末尾で行います。

“`lua
— スクリプトファイル simple_custom_proto.lua の末尾

— SCP Dissector を TCP ポート 12345 に登録
local tcp_port_table = DissectorTable.get(“tcp.port”)
tcp_port_table:add(12345, scp)

— もし UDP ポート 12345 でも同じプロトコルが動作するなら、UDP テーブルにも登録
— local udp_port_table = DissectorTable.get(“udp.port”)
— udp_port_table:add(12345, scp)

print(“SCP dissector loaded and registered on TCP port 12345.”)
“`

これにより、WiresharkはTCPポート12345のパケットを受信すると、自動的に標準のTCP DissectorからSCP Dissectorに処理を引き渡します。

プロトコル Dissector の高度なトピック

より複雑なプロトコルを解析する場合や、既存のDissector機能を活用したい場合には、いくつかの高度なAPIや概念が必要になります。

状態管理(TCPセッションなど)

ステートフルなプロトコル(TCPなど)を解析する場合、同じセッション内のパケット間で状態(state)を共有したり、前のパケットの情報に基づいて現在のパケットを解析したりする必要が出てきます。Wireshark Luaでは、Pinfoオブジェクトのuserdata機能を使って、パケット間で任意のLuaデータを共有できます。

pinfo.userdataはテーブルであり、Dissectorは任意のキーを使ってデータを保存・取得できます。このテーブルはTCPストリームやUDP会話などの論理的なフローごとに維持されます。

“`lua
function myproto.dissector(tvb, pinfo, tree)
pinfo.cols.protocol:set(“MYPROTO”)
local subtree = tree:add(myproto, tvb(), “My Custom Protocol Data”)

-- セッションの状態を取得または初期化
local session_state = pinfo.userdata["myproto_state"]
if not session_state then
    -- 最初のパケットの場合、状態を初期化
    session_state = {
        sequence_counter = 0,
        is_authenticated = false
    }
    pinfo.userdata["myproto_state"] = session_state -- 状態を保存
    print("New MYPROTO session initialized.")
end

-- パケットからシーケンス番号を読み込むと仮定
local current_sequence = tvb:uint(0, 4, base.BIGENDIAN) -- 例として先頭4バイトがシーケンス番号
subtree:add(myproto_sequence, tvb(0, 4))

-- 前のパケットのシーケンス番号と比較するなど、状態を利用した処理
if current_sequence ~= session_state.sequence_counter + 1 then
    -- シーケンス番号の不一致を検出
    subtree:add_expert_info(PI_SEQUENCE, PI_WARN, "Sequence number mismatch")
end

-- 状態の更新
session_state.sequence_counter = current_sequence

-- パケットの内容に基づいて状態を更新する例
local command_type = tvb:uint(4, 1) -- 例としてオフセット4にコマンドタイプがある
if command_type == 0x10 then -- 認証コマンドと仮定
    -- 認証成功を判定し、状態を更新
    session_state.is_authenticated = true
end

-- 認証状態に基づいて解析方法を変える例
if session_state.is_authenticated then
    subtree:add("Authenticated Data", tvb(5, -1)) -- オフセット5以降を認証済みデータとして扱う
else
    subtree:add("Unauthenticated Data", tvb(5, -1))
end

-- 状態は自動的に pinfo.userdata に保存されている

end
“`

pinfo.userdataはDissectorが返す際にWiresharkによって自動的に保存され、同じフロー(TCPセッションなど)の次のパケットが来たときに再度利用可能になります。これにより、複雑なプロトコルの状態管理が可能になります。

Fragment / Reassembly

TCPのようなストリーム指向プロトコルでは、アプリケーション層のメッセージが複数のTCPセグメントに分割(フラグメント)されたり、一つのセグメントに複数のメッセージが含まれたりすることがあります。Wiresharkにはこれらのフラグメントを再構成(Reassembly)して、元の完全なメッセージをDissectorに渡す機能があります。

Lua Dissectorでこの機能を利用するには、Protoオブジェクトにinit関数とreassembly_tableプロパティを追加し、Dissector内でtvb:get_string(offset, length)ではなくtvb:range(offset, length)を使い、必要に応じてDissector.get("data"):call(...)を呼び出します。ただし、LuaでのReassemblyの実装はC言語のDissectorほど直接的ではなく、pinfo.desegment_lenpinfo.desegment_offsetを設定してWiresharkの再構成エンジンを制御する必要があります。

ReassemblyはLua Dissectorの機能の中でも特に複雑な部類に入ります。基本的な考え方としては、Dissectorはパケットヘッダーを解析して、そのパケットがアプリケーションメッセージのどの部分を含んでいるか(またはそれが完全なメッセージか)を判断し、Wiresharkに次のパケットでさらにデータが必要か(pinfo.desegment_len > 0)、または現在のパケットの一部だけを再構成に回すか(pinfo.desegment_offset)を伝えます。

完全なReassembly対応Dissectorの実装はここでは割愛しますが、Wiresharkの公式Lua examplesやC言語Dissectorの実装(特にepan/dissectors/packet-tcp.cなどのストリームベースのもの)が参考になります。

Preferences の利用

作成したDissectorに、ユーザーが設定可能な項目(例: デフォルトポート番号、特定のオプションの有効/無効など)を追加したい場合があります。これはPreferencesオブジェクトを使用して行います。

まず、Protoオブジェクトにprefsプロパティを追加し、その中に設定項目を定義します。

“`lua
— プロトコル定義の後
local myproto = Proto(“My Custom Protocol”, “MYPROTO”)

— フィールド定義 (上記参照)
— …

— Preferences の定義
myproto.prefs = {
— Key = Preference.new(“display_name”, default_value, “description”, type, range, callback)
default_port = Preference.new(“Default TCP Port”, 8000, “Default TCP port for MYPROTO”, pref_types.UINT16),
enable_checksum_validation = Preference.new(“Enable Checksum Validation”, true, “Validate checksum field”, pref_types.BOOLEAN),
debug_level = Preference.new(“Debug Level”, 0, “Debug message level (0=none, 1=info, 2=debug)”, pref_types.UINT8, {0, 1, 2})
}

— dissector 関数の実装
function myproto.dissector(tvb, pinfo, tree)
pinfo.cols.protocol:set(“MYPROTO”)
local subtree = tree:add(myproto, tvb(), “My Custom Protocol Data”)

-- 設定値の読み込み
local default_port = myproto.prefs.default_port.value
local validate_checksum = myproto.prefs.enable_checksum_validation.value
local debug_level = myproto.prefs.debug_level.value

-- 設定値を利用した処理
if debug_level > 0 then
    print("MYPROTO Dissector: Processing packet " .. pinfo.number .. " with debug level " .. debug_level)
end

-- チェックサム検証が有効なら実行
if validate_checksum then
    -- チェックサム検証ロジック...
    if not checksum_valid then
        subtree:add_expert_info(PI_CHECKSUM, PI_ERROR, "Checksum invalid")
    end
end

-- ... 以降の解析ロジック ...

end

— ポート登録は、もしユーザー設定ポートを使いたいなら、init.lua などで設定値を読み取ってから行う必要がある
— シンプルな例では固定ポートに登録することが多い
local tcp_port_table = DissectorTable.get(“tcp.port”)
tcp_port_table:add(myproto.prefs.default_port.value, myproto) — ★注意: この方法だとスクリプト読み込み時の値が使われる

— より柔軟なポート設定には、ユーザーが Preferences を変更した後に DissecorTable の登録を更新する仕組みが必要だが、
— Lua Dissector では動的な DissectorTable 更新は難しい場合があるため、
— 通常は DissectorTable 登録は固定で行い、Dissector関数内で設定値を読み込んで動作を変える。
— もしくは init.lua で設定を読み込み、登録ポートを決定する。
“`

Preference.newの第4引数typepref_types.BOOLEAN, pref_types.UINT8, pref_types.UINT16, pref_types.UINT32, pref_types.INT8, pref_types.INT16, pref_types.INT32, pref_types.FLOAT, pref_types.STRING, pref_types.ENUM, pref_types.RANGEなどがあります。valueプロパティで設定値を取得できます。

Wiresharkの「編集」->「設定」(Edit -> Preferences) メニューを開くと、プロトコルツリーの中に作成したプロトコル名(”My Custom Protocol”)が表示され、設定項目を編集できるようになります。

プロトコル階層の操作

Dissectorは、現在のパケットがどのプロトコルに属しているか、その上位や下位のプロトコルは何か、といった階層情報を参照したり変更したりできます。これはPinfoオブジェクトを通じて行います。

  • pinfo.cols: Wiresharkのパケットリストペインのカラムに表示される情報を操作します。pinfo.cols.protocol:set(...)pinfo.cols.info:set(...)などで値を設定できます。
  • pinfo.cinfo: カラム情報に関するより詳細な構造体。
  • pinfo.layers: パケットの各レイヤーに関する情報を持つテーブル。
  • pinfo.private: 各Dissectorが一時的な情報を保存できる場所。
  • pinfo.protocol: 現在解析中のプロトコルの略称。
  • pinfo.parent_proto: 上位プロトコル (Protoオブジェクト)。

また、Protoオブジェクト自身もparentプロパティを持ち、上位プロトコルへの参照を格納できますが、これはあまりLua Dissectorで直接操作する機会は少ないかもしれません。

主な用途は、pinfo.colsを使ってパケットリストに表示されるプロトコル名や情報文字列をカスタムメッセージにすることです。

“`lua
function myproto.dissector(tvb, pinfo, tree)
— プロトコルカラムを更新
pinfo.cols.protocol:set(“MYPROTO”)

-- Infoカラムにパケットの種類を表示
local command_type = tvb:uint(4, 1) -- 例
local command_name = myproto_command.value_map[command_type] or "Unknown"
pinfo.cols.info:set("MYPROTO: " .. command_name .. " message")

-- ... 解析処理 ...

end
“`

エラーハンドリングとデバッグ

Luaスクリプトの開発において、エラーはつきものです。Wiresharkはスクリプトのエラーメッセージを「Tools」->「Lua」->「Console」に表示します。Syntax Errorやランタイムエラーはこのコンソールを確認することで特定できます。

スクリプト内でデバッグ情報を出力するには、組み込みのprint()関数を使用します。print()の出力もLua Consoleに表示されます。

“`lua
function myproto.dissector(tvb, pinfo, tree)
print(“Processing packet #” .. pinfo.number .. “, length: ” .. tvb:len())

local offset = 0
-- デバッグ出力
print("Reading version at offset " .. offset)
local version = tvb:uint(offset, 1)
print("Version read: " .. version)

-- ...

end
“`

より高度なデバッグが必要な場合は、Lua標準ライブラリのdebugモジュール(ただしWireshark環境で制限がある可能性あり)や、エラーが発生した際に自動的にデバッガーを起動する設定(ただしGUI版では非推奨か困難な場合がある)を検討します。通常はprintデバッグが最も手軽で実用的です。

解析エラー(例: パケットデータが期待する構造と異なる、長さが足りないなど)をWiresharkのExpert Informationとして表示するには、tree:add_expert_info(group, severity, text)を使用します。

  • group: エキスパート情報のカテゴリ(PI_PROTOCOL, PI_CHECKSUM, PI_SEQUENCE, PI_RESPONSE_CODE, PI_MALFORMED, PI_UNDECODED, PI_DEBUG, PI_NOTE, etc.)
  • severity: 重要度(PI_ERROR, PI_WARN, PI_NOTE, PI_CHAT
  • text: 表示するメッセージ文字列

“`lua
function myproto.dissector(tvb, pinfo, tree)
— …
local offset = 0
local pkt_len = tvb:len()

if offset + 4 > pkt_len then
    -- データが4バイト未満しかない
    subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Packet too short for header")
    return
end

-- ... 解析を続ける ...

end
“`

これらのデバッグおよびエラーハンドリングの手法を組み合わせることで、堅牢で開発しやすいDissectorを作成できます。

Tapper と Listener

Dissectorが個々のパケットの構造を解析するのに対し、TapperとListenerはパケットストリームや複数のパケットから情報を収集・集計するために使用されます。

Tapper の役割と仕組み

Tapperは、Wiresharkの統計情報メニューにカスタム統計情報を追加するために使用されます。特定のフィールドの値の出現回数をカウントしたり、平均値を計算したり、独自のヒストグラムを作成したりといった用途に適しています。

Tapperの基本構造は以下の通りです。

  1. Tapper.new()でTapperオブジェクトを作成します。表示名や対象となるプロトコルフィルタなどを指定します。
  2. Tapperオブジェクトに以下のメソッドを実装します。
    • reset(): 統計情報をリセットする際に呼び出されます。
    • packet(pinfo, tvb, column_info): パケットごとに呼び出され、統計情報を更新します。
    • draw(box): 統計情報ウィンドウに結果を表示する際に呼び出されます。boxは描画領域を提供します。

“`lua
— 例: HTTPリクエストメソッドの出現回数をカウントする Tapper
local http_method_counter = Tapper.new(“HTTP Method Counter”, “http.request.method”)

— 統計情報を格納するテーブル
local stats = {}

— reset メソッド
function http_method_counter.reset()
stats = {} — テーブルをクリア
print(“HTTP Method Counter: Stats reset.”)
end

— packet メソッド (パケットごとに呼び出される)
— filter (“http.request.method”) にマッチするパケットでのみ呼び出される
function http_method_counter.packet(pinfo, tvb, column_info)
— tvb にはフィルタされたフィールドの値 (バイト列) が含まれる
— http.request.method は文字列フィールドなので、tvb:string() で読み取る
local method_bytes = tvb:bytes(0, tvb:len())
local method_string = method_bytes:string() — TvbBytes オブジェクトの string() メソッド

-- 統計情報を更新
stats[method_string] = (stats[method_string] or 0) + 1 -- 出現回数をインクリメント

-- 必要に応じて、pinfo や column_info を利用することも可能
-- print("Packet #" .. pinfo.number .. ": Method = " .. method_string) -- デバッグ用

end

— draw メソッド (統計情報ウィンドウに表示される際に呼び出される)
function http_method_counter.draw(box)
box:text(“HTTP Request Method Counts:”)
box:text(“————————–“)

-- 統計情報をソートして表示
local methods = {}
for method, count in pairs(stats) do
    table.insert(methods, {method = method, count = count})
end

-- カウントが多い順にソート
table.sort(methods, function(a, b) return a.count > b.count end)

for _, item in ipairs(methods) do
    box:text(string.format("%-10s: %d", item.method, item.count))
end

box:text("--------------------------")
box:text("Total requests: " .. #methods) -- これはメソッドの種類数。合計リクエスト数とは異なる。
-- 合計リクエスト数をカウントするには、packetメソッドで別途カウンタを持つ必要がある。

end

— Tapperを登録
— Tapperオブジェクトをグローバル変数として定義するだけで、Wiresharkが自動的に検出して登録する
— (ただし、ファイルが plugins ディレクトリにあるか、init.lua で読み込まれている必要がある)

print(“HTTP Method Counter Tapper loaded.”)
“`

このスクリプトをpluginsディレクトリに配置してWiresharkを再起動すると、「統計」メニューに「HTTP Method Counter」という項目が追加されます。これを選択すると、キャプチャファイル内のHTTPリクエストメソッドの出現回数が表示されます。

Tapper.new()の第2引数でフィルタ文字列を指定することで、特定の条件にマッチするパケットのみを処理対象とすることができます。packetメソッドには、フィルタにマッチしたフィールドの値がtvbとして渡されます。

drawメソッド内で、box:text()を使ってテキストを統計ウィンドウに表示します。より複雑な描画が必要な場合は、boxオブジェクトの他のメソッド(例: box:add_lineなど)や、後述のStatsTreeなどを検討します。

Listener の役割と仕組み

Listenerは、Tapperよりもさらに高度な、ストリーム指向またはイベント駆動型の処理に使用されます。TCPセッションのような複数のパケットにまたがるデータの再構成や、アプリケーション層の対話(リクエスト/レスポンス)を追跡するのに適しています。

Listenerは、特定のプロトコルストリーム(例: TCPストリーム)に関連付けて使用されることが多く、ストリームの開始、データの受信、ストリームの終了といったイベントに対してコールバック関数を実行します。

Listenerの基本構造は以下の通りです。

  1. Listener.new()でListenerオブジェクトを作成します。対象となるプロトコル(通常はTCPやUDPなど)を指定します。
  2. Listenerオブジェクトに以下のメソッドを実装します。
    • reset(): 新しいストリームや会話が開始される際に呼び出されます。ストリームごとの状態を初期化します。
    • packet(pinfo, tvb, column_info): 個々のパケットがListenerに関連付けられたプロトコルに属している場合に呼び出されます。
    • stream_data(tvb_range, pinfo): ストリームデータが利用可能になったときに呼び出されます。再構成された連続したデータを受け取れます。
    • stream_end(pinfo): ストリームが終了したときに呼び出されます。

Listenerは通常、対象となるプロトコルのDissector内で登録されます。

“`lua
— 例: TCP ストリームを追跡するシンプルな Listener
— この Listener は TCP Dissector 内で登録されることを想定

local tcp_stream_listener = Listener.new(“tcp”) — TCP プロトコルに関連付ける

— ストリームごとの状態を保持するテーブル(Listener インスタンス自体に状態を持たせることも多い)
tcp_stream_listener.streams = {}

function tcp_stream_listener.reset(self)
— 新しいストリーム/会話が始まるたびに呼び出される
— ここでストリームごとの状態を初期化する
self.stream_state = {
packets_seen = 0,
total_bytes = 0
}
print(“Listener: New TCP stream/conversation started.”)
end

function tcp_stream_listener.packet(self, pinfo, tvb, column_info)
— ストリームに属する個々のパケットが来たときに呼び出される
— 必要に応じて、ここでパケットレベルの情報を見る
self.stream_state.packets_seen = self.stream_state.packets_seen + 1
self.stream_state.total_bytes = self.stream_state.total_bytes + tvb:len()
— print(“Listener: Packet #” .. pinfo.number .. ” in stream.”)
end

function tcp_stream_listener.stream_data(self, tvb_range, pinfo)
— 再構成されたストリームデータの一部が利用可能になったときに呼び出される
— tvb_range は連続したデータ部分を指す
print(“Listener: Received ” .. tvb_range:len() .. ” bytes of stream data.”)
— ここでアプリケーション層のメッセージ境界を検出したり、データを解析したりする
— 例: HTTP リクエスト/レスポンスの解析
end

function tcp_stream_listener.stream_end(self, pinfo)
— ストリームが終了したときに呼び出される
print(“Listener: TCP stream ended. Total packets: ” .. self.stream_state.packets_seen .. “, Total bytes: ” .. self.stream_state.total_bytes)
— ストリーム全体の統計や解析結果をここでまとめる
end

— Listener の登録は、通常、対象プロトコル (例: TCP) の Dissector の初期化部分で行われる
— ただし、Lua の場合は Listener オブジェクトをグローバルに定義するだけで Wireshark が自動的に検出・登録することが多い
— (Tapper と同様の検出メカニズム)

print(“TCP Stream Listener loaded.”)
“`

Listenerは個々のパケットだけでなく、stream_dataメソッドを通じて再構成された連続したデータストリーム全体を扱うことができる点が特徴です。これにより、複数パケットに分割されたアプリケーション層メッセージの解析などが容易になります。

統計スクリプトと GUI 拡張

Wiresharkは、パケットデータから様々な統計情報を収集・表示する機能を持っています(例: プロトコル階層、会話リスト、エンドポイントリストなど)。Luaスクリプトを使って、これらの標準統計機能に独自の項目を追加したり、カスタムの統計ウィンドウを作成したりすることができます。

Stats Tree (StatsTree)

StatsTreeは、Wiresharkの「統計」メニューにある「Protocol Hierarchy」や「Conversations」のようなツリー形式の統計表示に、独自の項目を追加するために使用します。

  1. StatsTree.new()StatsTreeオブジェクトを作成します。表示名などを指定します。
  2. packet(pinfo, tvb, column_info)メソッドを実装し、パケットごとに呼び出される際に統計情報を集計します。
  3. draw(tree)メソッドを実装し、統計ツリーに結果を追加します。

“`lua
— 例: パケット長ごとのカウントを StatsTree で表示
local pkt_length_stats = StatsTree.new(“Packet Length Distribution”)

— 集計用テーブル
local length_counts = {}

function pkt_length_stats.packet(self, pinfo, tvb, column_info)
local pkt_len = tvb:len()
length_counts[pkt_len] = (length_counts[pkt_len] or 0) + 1
end

function pkt_length_stats.draw(self, tree)
— tree は統計ツリーのルート
local root_node = tree:add(“Packet Length Counts”)

-- 長さでソートして表示
local lengths = {}
for len in pairs(length_counts) do
    table.insert(lengths, len)
end
table.sort(lengths)

local total_packets = 0
for _, len in ipairs(lengths) do
    local count = length_counts[len]
    root_node:add(string.format("Length %d: %d packets", len, count))
    total_packets = total_packets + count
end

root_node:add("Total packets counted: " .. total_packets)

end

— StatsTree オブジェクトをグローバルに定義することで自動的に登録される
print(“Packet Length Distribution StatsTree loaded.”)
“`

StatsTreeオブジェクトをグローバル変数として定義するだけで、Wireshark起動時に「統計」メニューに項目が追加されます。drawメソッドにはStatsTreeItemオブジェクト(ここではtreeという引数名)が渡され、そのadd()メソッドを使ってツリー構造を作成していきます。

Stats Window

StatsTreeが既存の統計ツリーに追加する形式であるのに対し、独自の統計ウィンドウを作成することも可能です。これはTapperdrawメソッドでより複雑な表示を行ったり、独自のGUI要素を組み込んだりする場合に利用できます。

基本的なTapperはテキストベースの表示に限定されますが、よりリッチなGUIを持つ統計ウィンドウは通常、C言語で実装されることが多く、Luaで完全にカスタマイズされたウィンドウを作成するのは高度なプログラミングが必要になる可能性があります。Tapperdrawメソッドで提供されるboxオブジェクトの機能範囲内でテキストベースの表示を行うのが一般的です。

GUI 拡張 (UiCustomPanel)

LuaスクリプトでWiresharkのGUI自体を拡張する機能も存在しますが、これは比較的限定的です。例えば、新しいメニュー項目を追加したり、簡単なダイアログを表示したりといったことができます。UiCustomPanelクラスなどが存在しますが、公式ドキュメントでの説明が手薄だったり、バージョンによって利用できる機能が異なったりすることがあります。

本格的なGUI拡張や複雑なユーザーインタラクションが必要な場合は、C言語によるプラグイン開発の方が適していることが多いです。LuaによるGUI拡張は、簡単なスクリプト実行のためのメニュー追加などに限定して考えるのが現実的です。

Wireshark Lua API リファレンス (概要)

これまでに登場した主要なWireshark Lua APIオブジェクトをまとめます。詳細なメソッドやプロパティについては、Wiresharkの公式ドキュメントを参照することを強く推奨します。Wiresharkのソースコードリポジトリ内のdoc/wsug_html/wsluarm.htmlまたはepan/wslua/wslua_api.cが参考になります。

  • Proto: プロトコル定義。
    • Proto.new(name, short_name): 新しいProtoオブジェクトを作成。
    • proto.fields: Fieldオブジェクトのテーブル。
    • proto.dissector: パケット解析関数 (function(tvb, pinfo, tree))。
    • proto.prefs: Preferenceオブジェクトのテーブル。
    • proto.parent: 上位プロトコル。
  • Field: プロトコルフィールド定義。
    • Field.new(filter_name, display_name, type, format, value_map, mask, description)
    • ftypes: フィールド型の定数テーブル。
    • base: 表示形式の定数テーブル。
  • Dissector: Dissectorオブジェクト。DissectorTableから取得。
    • dissector:call(tvb_range, pinfo, tree): 他のDissectorを呼び出す。
  • Tvb: パケットデータバッファ。Dissector関数に渡される。
    • tvb:len(): バッファの全長。
    • tvb:uint(offset, length, endian): 整数値を読み取り。
    • tvb:bytes(offset, length): バイト列を読み取り (TvbBytesオブジェクトを返す)。
    • tvb:string(offset, length): 文字列を読み取り。
    • tvb(offset, length): 部分範囲を示すTvbRangeを作成。
  • TvbRange: Tvbの部分範囲を示す。ツリー表示に追加する際に使用。
    • tvb_range.offset, tvb_range.len: 元のTvbからのオフセットと長さ。
    • tvb_range:uint(...), tvb_range:string(...), etc.: 元のTvbと同様の読み取りメソッド。
  • Pinfo: パケット情報。Dissector関数に渡される。
    • pinfo.number: パケット番号。
    • pinfo.time: パケットのタイムスタンプ。
    • pinfo.cols: カラム情報 (pinfo.cols.protocol, pinfo.cols.infoなど)。
    • pinfo.userdata: セッション/会話ごとの状態保存テーブル。
    • pinfo.desegment_len, pinfo.desegment_offset: Reassembly制御用プロパティ。
    • pinfo.src, pinfo.dst: 送信元/送信先アドレス。
    • pinfo.src_port, pinfo.dst_port: 送信元/送信先ポート番号。
  • TreeItem: パケット詳細ツリーのノード。Dissector関数に渡されるtree引数や、tree:add(...)の戻り値。
    • treeitem:add(field, tvb_range, value): フィールドと値をツリーに追加。
    • treeitem:add(field, tvb_range): tvb_rangeから値を読み取って追加。
    • treeitem:add(display_text, tvb_range): テキストノードをツリーに追加。
    • treeitem:add_expert_info(group, severity, text): Expert Informationを追加。
    • treeitem:append_text(text): ノードの表示テキストに追記。
    • treeitem:set_text(text): ノードの表示テキストを設定。
  • DissectorTable: Dissectorの登録・検索用テーブル。
    • DissectorTable.get(table_name): 指定した名前のテーブルを取得(例: “tcp.port”, “udp.port”, “ethertype”など)。
    • table:add(key, dissector): キーに関連付けてDissectorを登録。
    • table:get_dissector(key): キーに対応するDissectorオブジェクトを取得。
  • Tapper: カスタム統計情報の集計・表示。
    • Tapper.new(display_name, filter_string): 新しいTapperを作成。
    • tapper.reset, tapper.packet, tapper.draw: メソッドを実装。
  • Listener: ストリーム指向/イベント駆動処理。
    • Listener.new(proto_name): 新しいListenerを作成。
    • listener.reset, listener.packet, listener.stream_data, listener.stream_end: メソッドを実装。
  • StatsTree: カスタム統計ツリーの作成。
    • StatsTree.new(display_name): 新しいStatsTreeを作成。
    • statstree.packet, statstree.draw: メソッドを実装。
  • Preference: 設定項目定義。
    • Preference.new(display_name, default_value, description, type, range, callback): 新しいPreferenceを作成。
    • pref.value: 設定値を取得。
    • pref_types: Preference型の定数テーブル。

開発のヒントとベストプラクティス

Wireshark Luaスクリプトを効率的かつ堅牢に開発するためのヒントとベストプラクティスをいくつか紹介します。

  • print() デバッグを最大限に活用する: Lua Consoleは最も手軽なデバッグツールです。変数の値、処理のフロー、条件分岐の通過などをprint()で出力して確認しましょう。
  • Lua Console を活用する: Lua Consoleでは、実行中のスクリプトのグローバル変数にアクセスしたり、Luaコードをインタラクティブに実行したりできます。スクリプトを読み込んだ後、print(myproto.prefs.default_port.value)のようにして設定値を確認したり、簡単な関数を試したりできます。
  • 小さなステップで開発する: 一度に多くの機能を追加せず、まずは基本的なヘッダー解析とフィールド表示から始め、次に可変長フィールド、TLVs、状態管理など、少しずつ機能を拡張していきましょう。
  • 頑健な解析を心がける: パケットデータが想定通りでない場合(例: 長さが足りない、予約済みのフィールドに予期しない値が入っているなど)に、スクリプトがクラッシュせず、適切なエラーや警告(Expert Information)を表示するようにしましょう。データ長チェックは非常に重要です。
  • 既存の Dissector を参考にする: Wiresharkには多数の標準Dissectorが含まれており、その中にはLuaで実装されているものもあります(例: epan/dissectors/lua/ディレクトリ内)。これらを参考にすることで、具体的なAPIの使い方や実装パターンを学ぶことができます。特に似たようなプロトコルやデータ構造を解析するDissectorは良いお手本になります。
  • 公式ドキュメントを参照する: Lua APIに関する最新かつ正確な情報は、Wiresharkの公式開発者向けドキュメント(通常は英語)にあります。疑問が生じたり、特定の機能が必要になったりした場合は、まずは公式ドキュメントを確認しましょう。
  • パフォーマンスに関する考慮: Dissectorは大量のパケットに対して頻繁に実行されるため、パフォーマンスが重要です。不要なデータコピーを避け、効率的なデータ読み取り(特にtvb関連のメソッド)を心がけましょう。複雑な計算や処理は避けるか、最適化を検討してください。ただし、多くのLua Dissectorでは、パフォーマンスがボトルネックになる前にC言語での実装を検討することが多いです。
  • コードの可読性と保守性: ローカル変数を適切に使用し、関数やテーブルでコードを整理し、分かりやすい変数名やコメントを使用することで、スクリプトの可読性と保守性を高めましょう。
  • エラーメッセージの解釈: Wireshark Lua Consoleに表示されるエラーメッセージは、問題の特定に役立ちます。エラーの種類(Syntax Error, Runtime Error)、発生したファイル名、行番号、エラー内容を注意深く確認しましょう。特にbad argument #X to 'method' (expected type, got nil)のようなメッセージは、予期しないnil値が関数に渡されたことを示唆しており、変数の初期化や値の取得に問題がある可能性が高いです。

Luaスクリプトの応用例

Wireshark Luaスクリプトは、様々なシナリオで活用できます。

  • カスタムプロトコルの解析: 社内開発されたプロトコル、特定の組み込みシステムで使用されるプロトコルなど、Wiresharkが標準で対応していないプロトコルを解析できます。
  • 既存プロトコルの拡張/修正: 既存プロトコルに独自のヘッダーが追加されている場合や、標準Dissectorが対応していない特殊なオプションやフィールドを解析したい場合に、既存Dissectorを上書きしたり、そのペイロードとしてカスタムDissectorを呼び出したりできます。
  • 特定のセキュリティイベントの検出: 不審なパケットパターン(例: 特殊なフラグの組み合わせ、予期しないシーケンス番号、異常なペイロードサイズなど)をDissector内で検出してExpert Informationとしてマークしたり、TapperやListenerを使って統計的に分析したりできます。
  • ネットワークパフォーマンスの分析に特化した統計情報: Round Trip Time (RTT)、スループット、特定のメッセージ間の遅延など、標準統計にはない独自のパフォーマンス指標を計算し、TapperやStatsTreeで表示できます。
  • 自動化された処理: 特定のパケットが見つかったらログを出力する、特定の条件を満たすパケットをマークするなど、簡単な自動処理を実装できます。

これらの応用例は、Wireshark Luaスクリプトが単なるプロトコル解析にとどまらず、ネットワークの監視、トラブルシューティング、セキュリティ分析、性能評価など、幅広い分野で役立つツールであることを示しています。

まとめ

この記事では、WiresharkのLuaスクリプト機能について、その基本的な概念から、最も重要な機能であるプロトコルDissectorの作成方法、さらにはTapper、Listener、StatsTreeといった応用的な機能までを詳細に解説しました。

Wireshark Luaスクリプトは、標準機能だけでは対応できない様々な要求に応えるための強力な拡張メカニズムです。カスタムプロトコルの解析、独自の統計情報の収集、解析作業の自動化など、ネットワーク解析の可能性を大きく広げることができます。

Luaは学習しやすい言語であり、Wiresharkが提供する豊富なAPIを活用することで、比較的少ないコード量で強力な機能を実現できます。はじめはシンプルなDissectorから開発を始め、慣れてきたら状態管理やTLV解析、さらにはTapperやListenerといった機能にも挑戦してみてください。

この記事が、あなたがWireshark Luaスクリプトの世界へ踏み出し、ネットワーク解析のスキルをさらに高めるための一助となれば幸いです。Lua Consoleと公式ドキュメントを味方につけ、ぜひあなたのアイデアをスクリプトとして形にしてみてください。ネットワークの世界は常に変化しており、新しいプロトコルや技術が登場します。Wireshark Luaスクリプトは、その変化に対応し、常に最前線で解析を行うための強力な武器となるでしょう。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール