NHN Cloud NHN Cloud Meetup!

Wiresharkでのプロトコル分析(Wireshark Custom Dissectorの製作)

なぜCustom Dissectorが必要か?

プロジェクトを進行する上で、直接プロトコルを定義、実装して使うことがあります。
HTTP、JSON、XMLなどのように、よく知られているプロトコルを基盤にしたり、あるいはText基盤で作成されたプロトコルであれば、パケットをキャプチャして解析するのは非常に簡単です。ところが、下記のようにbyte基盤で、さらにbit単位まで分けて使用するプロトコルであれば、Wiresharkでパケットをキャプチャしても分析が難しいでしょう。


上記のプロトコルフォーマットはWebSocketパケットのフォーマットです。もちろんWiresharkはWebSocketのDissectorが基本的に含まれていますが、ない場合はパケットを取得しても、下図のようにTCP PSHで表示されるだけで、確認したいPSHが検索しづらく、検索できてもHexで表記されたbyte値を取得して、都度、計算しながらパケットの内容を確認することになります。

たとえば、最初のbyte値が0x81なので、これをbitで紐解けば1000 0001になります。プロトコル構造でみると、FIN flagは1、残りのflagは0、opcodeは1 ..というように確認する必要があります。
WiresharkのWebSocket Dissectorを適用すると、以下のように整然と出力され、より簡単にパケットを分析できます。

直接作成して使用するプロトコルを、WebSocketのようにWiresharkでParsingされた形態ですぐに確認できれば、パケット解析の労力を大幅に削減できそうですね。

この記事で扱う内容

この記事では、Dissectorの作成方法を基本から一つずつ説明するのではなく、TOAST PCからストリーミングデータを送信するのに使用するプロトコル用のDissectorを作成する方法をサンプルとともに紹介しようと思います。私たちが作るプロトコルは案外簡単な場合が多いので、非常に複雑な形状のプロトコルでない限り、本文のサンプルを応用するだけで簡単に作成できます。
Wireshark用のDissectorを作成する方法はいくつかありますが、本文ではLuaを用いて作成します。

サンプルを作成してみよう

サンプルとして、TOAST PCでストリーミングに用いるプロトコルを使用します。
TOAST PCでストリーミングに使用するパケットは、次のような構造です。

  • STR(start)bit値が1のとき

  • STR(start)bit値が0のとき


上で表示したWebSocketに比べるとかなり単純な形になっています。本格的に作ってみよう。

1. Protocol Filterと名前を指定して、プロトコルオブジェクトを作成する

-- Create ToastPC Streaming protocol
--  "tpcstream" : プロトコルの名前。Filterウィンドウなどで使用
--  "TPCSTREAM" : Packet Detail, ListのProtocolカラムに表示されるプロトコルDescription
p_tpcstream = Proto ("tpcstream", "TPCSTREAM")

2. 上記、p_tpcstreamオブジェクトのFieldを定義する

  • Field名で使用する値は、今後、WiresharkのFilterウィンドウでパケットをフィルタリングする際にキーワードとして使用できる
  • (例 : tpcstream.encrypted==1)
local f = p_tpcstream.fields

-- Field 定義
--   HEADER 共通
--     STARTCODE Field 定義
f.startcode = ProtoField.uint32("tpcstream.startcode", "STARTCODE", base.HEX)

--     FLAGS Field 定義
--     bit dataを扱うには、unit8()関数の最後にbits mask値を記載する
f.ver = ProtoField.uint8("tpcstream.ver", "VERSION", base.DEC, nil, 0xC0)
f.reserved = ProtoField.uint8("tpcstream.reserved", "RESERVED", base.HEX, nil, 0x30)
f.encrypted = ProtoField.uint8("tpcstream.encrypted", "ENCRYPTED", base.DEC, nil, 0x08)
f.iframe = ProtoField.uint8("tpcstream.iframe", "I-FRAME", base.DEC, nil, 0x04)
f.startframe = ProtoField.uint8("tpcstream.start", "START", base.DEC, nil, 0x02)
f.endframe = ProtoField.uint8("tpcstream.end", "END", base.DEC, nil, 0x01)

--   HEADER Fields for START BIT == 1 
--        Frame Size Field 定義
f.frame_size = ProtoField.uint32("tpcstream.frame_size", "FRAME_SIZE", base.DEC)

--     Frame Count Field 定義
f.frame_count = ProtoField.uint32("tpcstream.frame_count", "FRAME_COUNT", base.DEC)

--   HEADER Fields for START BIT == 0
--       Packet Count Field 定義
f.packet_count = ProtoField.uint16("tpcstream.packet_count", "PACKET_COUNT", base.DEC)

--   BODY : Frame Data
f.frame_data = ProtoField.bytes("tpcstream.frame_data", "FRAME_DATA")

3. p_tpcstreamオブジェクトのdissector()関数を定義する

  • Wiresharkで当該Dissectorを使用する条件(Layer4 Protocol、Port、番号など)に合うパケットを検索したとき、呼び出される関数
  • Parameterに次の3つの値を伝達する
    • buffer:Packet raw bytes
    • pinfo:Packet Information
    • tree:Packet Detail tree
  • Wiresharkウィンドウでは、それぞれ次の部分を表している

  • つまり、この関数では、bufferの値を持って下記を作成する
    • Packet詳細ウィンドウに表示するParsed値(tree)
    • Packetリストウィンドウに表示するPacket情報値(pinfo)
-- tpcstream dissector function
function p_tpcstream.dissector (buffer, pinfo, tree)
  -- validate packet length is adequate, otherwise quit
  if buffer:len() == 0 then return end

  ---------------------------------------------------------
  -- パケットの詳細ウィンドウに SubTree を追加
  ---------------------------------------------------------
  subtree = tree:add(p_tpcstream, buffer(0))

  -- STARTCODE値 Parsing
  -- bufferの最初のbyteから4byteをstartcode fieldに適用して追加する。
  subtree:add(f.startcode, buffer(0, 4))
  -- FLAGS値 Parsing
  -- bufferの5番目のbyteから1byteだけ読み、これをver, reserved, encrypted.. など
  -- 上で定義した各bit fieldに適用して追加する。
  local flags = buffer(4, 1)
  -- add flags bit
  subtree:add(f.ver, flags)
  subtree:add(f.reserved, flags)
  subtree:add(f.encrypted, flags)
  subtree:add(f.iframe, flags)
  subtree:add(f.startframe, flags)
  subtree:add(f.endframe, flags)

  -- "start" bit flag値によってパケット形態が異なるので、まずstart bit flag値を読む。
  local startbit = buffer(4, 1):bitfield(6, 1)

  -- start bit値が1か0かによって追加が必要なsubtree項目が異なる。
  if startbit == 1 then
    -- start bit値が1なら、frame size, frame count, frame data項目を追加する。
    subtree:add(f.frame_size, buffer(5, 3))
    subtree:add(f.frame_count, buffer(8, 4))
    --subtree:add(f.frame_data, buffer(16))
    -- 次のような方法で他のDissectorを呼び出し使用できる。
    -- TOAST PC Streaming Protocolのframe dataは、H.264パケットがあるので
    -- 下記のようにH.264 Protocol Dissectorを呼び出してframe dataをParsingする。
    h264_table = Dissector.get("h264")
    tvb=buffer(16)
    h264_table:call(tvb:tvb(), pinfo, tree)
  else 
    -- start bit値が0なら、frame count, packet count, frame data項目を追加する。
    subtree:add(f.frame_count, buffer(5, 4))
    subtree:add(f.packet_count, buffer(9, 2))
    --subtree:add(f.frame_data, buffer(12))
    h264_table = Dissector.get("h264")
    tvb=buffer(12)
    h264_table:call(tvb:tvb(), pinfo, tree)
  end
  ---------------------------------------------------------

  ---------------------------------------------------------
  -- パケットリスト表示ウィンドウ info カラムに表示される情報
  ---------------------------------------------------------
  -- Protocolカラムに表示されるプロトコル名を指定
  -- "TPCSTREAM" に設定
  pinfo.cols.protocol = p_tpcstream.name

  -- Infoカラムに表示されるプロトコル情報文字列を生成
  -- 本サンプルでは以下のような形で出力するように作成する。
  -- * start bitが1パケット
  --    [フレームタイプ] frame count #フレームカウント start
  -- * end bitが1パケット
  --     frame count #フレームカウント end seq=#パケットカウント
  -- * start/end bitがすべて1パケット
  --    [フレームタイプ] frame count #フレームカウント start, end
  -- * 残り
  --     frame count #フレームカウント cont. seq=#パケットカウント
  local info_str = "";
  -- バージョン情報出力
  info_str = info_str.."VER="..version.." "

  local endbit = buffer(4, 1):bitfield(7, 1)
  if startbit == 1 then
    -- start bitが1なら、I/P Frameとframe count値、start/end可否を出力
    local iframe = buffer(4, 1):bitfield(5, 1)
    if iframe == 1 then
      info_str = info_str.."[I-FRAME]"
    else 
     info_str = info_str.."[P-FRAME]"
    end

    local frame_count = buffer(8, 4):uint()
    info_str = info_str.." frame count "..frame_count.." start"
    if endbit == 1 then
      info_str = info_str..", end"
    end
  else
    -- start bitが0なら、frame count, packet count値とcontinue/end可否を出力
    local frame_count = buffer(5, 4):uint()
    local packet_count = buffer(9, 2):uint()
    info_str = info_str.." frame count "..frame_count
    if endbit == 1 then 
      info_str = info_str.." end "
    else
      info_str = info_str.." cont. "
    end

    info_str = info_str.."seq="..packet_count
  end

  -- 生成した文字列をInfoカラムの値に設定
  pinfo.cols.info = info_str
  --------------------------------------------------------
end

4. p_tpcstreamのinit()関数を定義する

  • 初期化時に呼び出される関数
  • 本サンプルでは、特別に処理することがないので、空白のままにしておく
-- Initialization routine
function p_tpcstream.init()
end

5. こうして作られたDissectorをWiresharkのDissector Tableに追加すると完成する

  • UDP Port 7010、8010番を通じて送信されるパケットは、このDissectorをのせて設定する
local udp_dissector_table = DissectorTable.get("udp.port")
udp_dissector_table:add(7010, p_tpcstream)
udp_dissector_table:add(8010, p_tpcstream)

Wiresharkに適用してみよう

以上の内容を作成した後、Luaの拡張子を持つファイル名で保存します。

  • Windowsでは、D:\tools\tpcstream.luaに保存する。

Wiresharkがインストールされたパスで

init.lua

ファイルを管理者権限で実行したエディタで開き、ファイルの末尾に次のようにtpcstream.luaファイルのパスを指定して保存する。

dofile("D:\\tools\\tpc_stream.lua")

その後、Wiresharkを実行したら完了です。
実際のパケットをキャプチャした後、Wiresharkで開いてみると、次のように表示されます。

おわりに

優れた汎用プロトコルやプロトコルを扱うライブラリがすでに多く存在しているので、直接プロトコルを定義して使用することは、実際にはあまりないでしょう。プロトコルの特性によっては、WiresharkでDissectorからパケットを分析する必要がない場合や、または必要があっても、すでに作られたDissectorが存在するため、このようにDissectorまで作成するケースはほとんどないかもしれません。

しかし、自分でProtocol Dissectorを作り、いろいろ試してみたりするのは、プロトコルの理解を深める上で、有用な方法だと思います。プロトコルの構造を把握できたり、各Field値に応じた処理ロジックが理解できるようになったり、たくさんの情報の中から、重要な情報を選別して出力する作業が学習できるからです。この記事が少しでも役立つと嬉しいです。

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop