DARK MATTER

CDI Engineer's Technical Blog

続々 WiresharkのDissectorを使った独自プロトコル解析(TCP,UDP分割パケットの場合)

本稿では、Luaで独自プロトコルのDissectorを作る人のために、TCPとUDPの分割(フラグメント)パケットの再構築(リアセンブル)を説明します。

f:id:yasuikj:20200825183801p:plain

Dissectorって何?という人は、先にWiresharkのDissectorを使った独自プロトコル解析をやさしく解説してみましたを読んでください。

前々回は、符号なし整数、文字列を題材にDissectorを説明しました。 前回は、Bit列の解析や時刻の解析など様々なデータ型への対応や、エンディアン(Endian)の対応について説明しました。

これまでの内容を理解いただければ、1パケットの解析はできるようになったと思います。しかし、世の中にはデータサイズが大きく複数のパケットに分割されて送信されるプロトコルも存在します。今回、このような分割パケットのDissectorの作り方について解説します。

今回紹介する内容を理解いただくと、分割パケットに対応できるDissectorを作る力がつきます。
サンプルのキャプチャデータも添付するのでWiresharkを使って試してみてください。

技術部の安井です。長年、制御システムを開発した経験から、現在は制御システムセキュリティ向上に取り組んでいます。

前々回 前回とも、継続してアクセスいただいており、独自プロトコルを解析するためにDissectorを作成しようしている方が参考にしてくれているようです。記事を読みDissectorを作成する方が増えてくれていれば嬉しい限りです。
最近、前回ブログで「記載していないこと」と書いた「データサイズが大きく複数パケットに分割された場合のDissector」についての問い合わせをいただく機会がありました。TCPとUDPにおける分割パケットの解析方法の資料を探したところ、TCPに関しては多くの資料が見つかりましたが、UDPに関しては理解しやすい資料をさくっと見つけることができませんでした。このためUDPに関して解説するブログを書いてみることにしました。

対象読者

前々回、前回の以下ブログ内容を理解している方。かつ、何らかのプログラム言語を使ったことがあり配列や連想配列が何かを理解している方。
io.cyberdefense.jp
io.cyberdefense.jp

できるようになること

複数のパケットに分割されたパケットのDissectorの作成

TCPのパケット分割について(いちおう書いておきます)

TCPはストリーム型の通信であり、送信サイズや通信環境によりTCPの仕組みでパケットが分割されて送信される場合があります。このため一般に公開するDissectorでは分割パケットの対応はお作法として実施しておいた方がよいと思います。

TCPの分割パケットの解析については、pinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT というキーワードを覚えておけば、必要な際にサンプルを探せばいろいろな解説資料がみつかると思います。わかりやすく親切に解説してくれていると思った2つのURLを紹介しておきます。
Wiresharkで独自プロトコルの解析 - Qiita
Wiresharkの解析プラグインを作る ssmjp 201409 P24,25

UDPのパケット分割について(ここから本題です)

制御システムではTCPよりオーバヘッドの小さなUDPを使う場面が多くみられ、大きなデータをUDPで送信する場合、送信プログラムにて1パケットで送信可能なサイズで分割して送信することが多いです*1

本稿では以下の例を用いてUDPの分割パケットの解析について解説します。

  • UDPで2つのパケットに分割(フラグメント)された内容を再構築(リアセンブル)して表示する。

送信したいデータは、図1に示した30byteのデータとする。


f:id:yasuikj:20200826171526p:plain
図1:UDPを使って送信するデータのイメージ
Size部は10進数の26,データ本体部はasciiコードのa〜zです。

UDPの1つのパケットのフォーマットは図2に示したとおりであり、データ部として最大16byteしか格納できないとする。

f:id:yasuikj:20200825172927p:plain
図2:UDPの1パケットのフォーマット

30byteのデータを図2のフォーマットで送信するには、パケットを分割する必要があるため、図3に示したように2つのパケットに分割して送信する。

f:id:yasuikj:20200825172939p:plain
図3:送信したいデータをUDPの2つのパケットに分割したイメージ

UDP分割パケットのDissector

この章は、本ページ内に記載したDissectorのサンプルコードをudp_div.luaという名称で保存し、udp_div.txtのパケットを読み込んでみてください。

proto = Proto("udp_div","udp_div")

Ident_F  = ProtoField.new("Ident","Ident",ftypes.UINT16)
DivNo_F  = ProtoField.new("DivNo","DivNo",ftypes.UINT16)
DivMax_F = ProtoField.new("DivMax","DivMax",ftypes.UINT16)
DataSize_F  = ProtoField.new("DataSize","DataSize",ftypes.UINT32)
Data_F   = ProtoField.new("Data","Data",ftypes.STRING)

proto.fields = { Ident_F, DivNo_F, DivMax_F, Size_F ,DataSize_F, Data_F}

-- 初期化処理
function proto.init()
  fragments = {} -- 1パケットずつに分割されたパケットのデータ部を保存するバッファ。各パケット毎に保存する
  concats = {}   -- 分割パケットをマージした本体データを保存するバッファ。DivMax目のパケット処理時に保存する
end

-- Dissector  解析のメイン部分
function proto.dissector(buffer, pinfo, tree)

  local ident_no   = buffer(0,2):uint()    -- Ident識別子
  local division_no  = buffer(2,2):uint()  -- DivNo
  local division_max = buffer(4,2):uint()  -- DivMax

  if pinfo.visited == false then    -- 当該パケットを処理するのが初めての場合
    --注意:このpinfo.visitedで判定しないとWiresharkの画面でパケットをクリックする度処理が動いてしまう
    if division_no == 1 then
      local data_size = buffer(6,4):uint() -- データ本体のトータルサイズ
    end
    local reassembly_byte     -- 組み立て後のデータバイト列

    -- fragmentsを保存するテーブルの枠を生成
    if fragments[ident_no] == nil then
      fragments[ident_no] = {}
    end
    if fragments[ident_no][division_no] == nil then
      fragments[ident_no][division_no] = {}
    end

    -- パケットのUDPデータ部のサイズから先頭の独自ヘッダサイズ(6byte)を除いた部分をfragmentsに記憶
    fragments[ident_no][division_no] = buffer:bytes(6,buffer:len()-6)

    -- 分割パケットの最後なら、分割パケットをまとめたバイト列を生成しconcatsに記憶
    if (division_no == division_max ) then
      -- 保存用バイト列を生成
      reassembly_bytearray = ByteArray.new()
      -- 分割数分処理
      for key, value in ipairs(fragments[ident_no]) do
        -- バイト列をマージ
        reassembly_bytearray:append(value)
      end
      -- 分割数分マージしたバイト列をconcatsに記憶
      concats[pinfo.number] = reassembly_bytearray
      -- 各分割パケットのバイト列を保存したエリアは解放
      fragments[ident_no] = nil
    end
  end

  tree:add(Ident_F,buffer(0,2))    -- Ident表示
  tree:add(DivNo_F,buffer(2,2))    -- DivNo表示
  tree:add(DivMax_F,buffer(4,2))   -- DivMax表示

  -- 分割パケットのまとめがある場合
  if concats[pinfo.number] ~= nil then
    pinfo.cols.info = "merged data"
    buffer1 = concats[pinfo.number]:tvb("merged data")
    tree:add(DataSize_F,buffer1(0,4))     -- Size部分表示
    tree:add(Data_F,buffer1(4,data_size)) -- Size部分以降のデータ本体表示
  end
end

--  Dissectorを使用するUDPポート番号を指定する。
udp_table = DissectorTable.get("udp.port")
udp_table:add(4352, proto)

テスト用データのパケットキャプチャデータをテキスト形式としたものを以下に記載します。
記載内容をudp_sample.txtというファイル名で空のテキストファイルに転記し保存した上、Wiresharkのメニューの[ファイル]-[開く]からudp_sample.txtを選択してください。

+---------+---------------+----------+
01:03:35,089,675   ETHER
|0   |08|00|27|db|63|e0|08|00|27|63|e1|01|08|00|45|00|00|32|5f|41|40|00|40|11|94|50|c0|a8|01|01|c0|a8|01|02|f7|51|11|00|00|1e|47|59|00|33|00|01|00|02|00|00|00|1a|61|62|63|64|65|66|67|68|69|6a|6b|6c|

+---------+---------------+----------+
01:03:35,146,884   ETHER
|0   |08|00|27|db|63|e0|08|00|27|63|e1|01|08|00|45|00|00|30|5f|41|40|00|40|11|94|50|c0|a8|01|01|c0|a8|01|02|f7|51|11|00|00|1c|47|33|00|33|00|02|00|02|6d|6e|6f|70|71|72|73|74|75|76|77|78|79|7a|

+---------+---------------+----------+

このDissectorでテスト用データをWiresharkで表示したものを図4に示します。2パケット目の"Data" 部にabcd〜xyz とマージされたデータが表示されているのがわかると思います。

f:id:yasuikj:20200825174646p:plain:w290f:id:yasuikj:20200825174703p:plain:w290
          図4:1パケット目と2パケット目のWiresharkでの表示

最後に、2パケット目の"Data"部をクリックしたときの表示を図5に示します。ソースコード上concats[pinfo.number]:tvb("merged data")とした部分が、画面の最下部の左側Frameバイト列の枠の右横にmerged data(30bytes)として別枠で表示されているのがわかると思います。

f:id:yasuikj:20200825174719p:plain
図5:リアセンブルしたデータ部を表示したもの

まとめ

本稿では、Luaを使ってUDPの分割パケットのDissectorを作る方法を説明しました。

本稿がUDPの分割パケットの解析に困っている方の助けになれば幸いです。



本稿を作成するにあたり参考とさせていただいた資料です。
osqa-ask.wireshark.org

補足

本記事を作成したところ、ある同僚の方より
「独自ヘッダーの情報だけでリアセンブリすると、クライアントが複数存在する場合バグりそうです。1対1を基本とする独自プロトコルや運用はよくありますが、dissectorとしては多対多で正しく動作できるよう、サーバー/クライアントのIP/ポートの組み合わせを使うとよさそうだと思います。」
とのアドバイスとともに、対策したサンプルソースをいただけました。上記以外の元ソースとの相違としては、pinfo.cols.infoの書き換えはやめており分割全パケットでマージ結果を表示するようにしている点と、パケットの到着順序逆転にも対応するため分割パケット組み立て用に確保したエリアの解放やめています。こちらの方が便利な場合も多々あると思いますので合わせて公開しておきます。ソースの解説は載せてませんのが興味ある方は読み解いてみてください。

proto = Proto("udp_div","udp_div")

Ident_F  = ProtoField.new("Ident","Ident",ftypes.UINT16)
DivNo_F  = ProtoField.new("DivNo","DivNo",ftypes.UINT16)
DivMax_F = ProtoField.new("DivMax","DivMax",ftypes.UINT16)
DataSize_F  = ProtoField.new("DataSize","DataSize",ftypes.UINT32)
Data_F   = ProtoField.new("Data","Data",ftypes.STRING)

proto.fields = { Ident_F, DivNo_F, DivMax_F, Size_F ,DataSize_F, Data_F}

-- 初期化処理
function proto.init()
  fragments = {} -- 1パケットずつに分割されたパケットのデータ部を保存するバッファ。各パケット毎に保存する
end

function newtable(table, key)
  if table[key] == nil then
    table[key] = {}
  end
  return table[key]
end

-- Dissector  解析のメイン部分
function proto.dissector(buffer, pinfo, tree)
  local ident_no   = buffer(0,2):uint()    -- Ident識別子
  local division_no  = buffer(2,2):uint()  -- DivNo
  local division_max = buffer(4,2):uint()  -- DivMax
  local s = tostring(pinfo.src) .. ":" .. tostring(pinfo.src_port) .. "@" .. tostring(pinfo.dst) .. ":" .. tostring(pinfo.dst_port)

  tree:add(Ident_F, buffer(0,2))    -- Ident表示
  tree:add(DivNo_F, buffer(2,2))    -- DivNo表示
  tree:add(DivMax_F, buffer(4,2))   -- DivMax表示

  fragment = newtable(fragments, s)
  ident = newtable(fragment, ident_no)
  ident[division_no] = {tree=tree, data=buffer:bytes(6,buffer:len()-6)}

  if #ident == division_max then
    local data = ByteArray.new()
    for i, division in pairs(ident) do
      data:append( division["data"] )
    end
    buffer1 = data:tvb("merged data")
    tree:add(DataSize_F,buffer1(0,4))     -- Size部分表示
    tree:add(Data_F,buffer1(4,data_size)) -- Size部分以降のデータ本体表示
  end
end

--  Dissectorを使用するUDPポート番号を指定する。
udp_table = DissectorTable.get("udp.port")
udp_table:add(4352, proto)

*1:様々な理由がありますがここでは理由の説明は割愛します。

株式会社サイバーディフェンス研究所 / Cyber Defense Institute Inc.