本稿では、Luaで独自プロトコルのDissectorを作る人のために、TCPとUDPの分割(フラグメント)パケットの再構築(リアセンブル)を説明します。
Dissectorって何?という人は、先にWiresharkのDissectorを使った独自プロトコル解析をやさしく解説してみましたを読んでください。
前々回は、符号なし整数、文字列を題材にDissectorを説明しました。 前回は、Bit列の解析や時刻の解析など様々なデータ型への対応や、エンディアン(Endian)の対応について説明しました。
これまでの内容を理解いただければ、1パケットの解析はできるようになったと思います。しかし、世の中にはデータサイズが大きく複数のパケットに分割されて送信されるプロトコルも存在します。今回、このような分割パケットのDissectorの作り方について解説します。
今回紹介する内容を理解いただくと、分割パケットに対応できるDissectorを作る力がつきます。
サンプルのキャプチャデータも添付するのでWiresharkを使って試してみてください。
技術部の安井です。長年、制御システムを開発した経験から、現在は制御システムセキュリティ向上に取り組んでいます。
前々回、 前回とも、継続してアクセスいただいており、独自プロトコルを解析するためにDissectorを作成しようしている方が参考にしてくれているようです。記事を読みDissectorを作成する方が増えてくれていれば嬉しい限りです。
最近、前回ブログで「記載していないこと」と書いた「データサイズが大きく複数パケットに分割された場合のDissector」についての問い合わせをいただく機会がありました。TCPとUDPにおける分割パケットの解析方法の資料を探したところ、TCPに関しては多くの資料が見つかりましたが、UDPに関しては理解しやすい資料をさくっと見つけることができませんでした。このためUDPに関して解説するブログを書いてみることにしました。
対象読者
前々回、前回の以下ブログ内容を理解している方。かつ、何らかのプログラム言語を使ったことがあり配列や連想配列が何かを理解している方。
できるようになること
複数のパケットに分割されたパケットの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のデータとする。
Size部は10進数の26,データ本体部はasciiコードのa〜zです。
UDPの1つのパケットのフォーマットは図2に示したとおりであり、データ部として最大16byteしか格納できないとする。
30byteのデータを図2のフォーマットで送信するには、パケットを分割する必要があるため、図3に示したように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 とマージされたデータが表示されているのがわかると思います。
図4:1パケット目と2パケット目のWiresharkでの表示
最後に、2パケット目の"Data"部をクリックしたときの表示を図5に示します。ソースコード上concats[pinfo.number]:tvb("merged data")とした部分が、画面の最下部の左側Frameバイト列の枠の右横にmerged data(30bytes)として別枠で表示されているのがわかると思います。
まとめ
本稿では、Luaを使ってUDPの分割パケットのDissectorを作る方法を説明しました。
本稿がUDPの分割パケットの解析に困っている方の助けになれば幸いです。
本稿を作成するにあたり参考とさせていただいた資料です。
補足
本記事を作成したところ、ある同僚の方より
「独自ヘッダーの情報だけでリアセンブリすると、クライアントが複数存在する場合バグりそうです。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)
-
様々な理由がありますがここでは理由の説明は割愛します。 ↩︎