DARK MATTER

CDI Engineer's Technical Blog

(続)WiresharkのDissectorを使った独自プロトコル解析をやさしく解説してみました

本稿では、初めて実際に独自プロトコルのDissectorを作る人が最初にぶつかるであろう壁を乗り越える方法を紹介します。

Dissectorって何?という人は、先に↓こちらを読んでください。
io.cyberdefense.jp


前回は、符号なし整数、文字列を題材にDissectorを説明しました。しかし、実際にDissectorを作ろうとすると、Bit列の解析や時刻の解析など様々なデータ型への対応や、エンディアン(Endian)の解析の壁にぶつかったりします。
今回紹介する内容を理解いただくと、これらの壁を乗り超えてDissectorを作る力がつきます。
サンプルのキャプチャデータも添付するのでWiresharkを使って試してみてください。

サンプルを探す前に、あと一歩力をつけて

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

前回、予想以上に多くの人に読んでいただきありがとうございました。Dissector を作ろうとされている方が大勢いることが分かり嬉しい限りです。

多くの方は、初めて独自プロトコルのDissectorを作ろうとしたときに、サンプルコードを検索して読解できず混乱する人が多いのではないでしょうか?私がそうでした。そうならないために、あと一歩、今回記載する3つのポイントを理解して力をつけてから、検索に出発することをお勧めします。

  • 1. 記法の違いによる混乱に陥らない方法
  • 2. 様々なデータ型の記法の見つけ方
  • 3. エンディアンの理解と対応方法

これを理解すると、サンプルコードを探し回らなくてもDissectorが作成できますし、サンプルコードを見たときに、だいぶストレスなく読めるようになると思います。

できるようになること

様々なデータ型と、エンディアンの違いを含む独自プロトコルを解析するためのDissector作成

記載していないこと

フォーマットが条件により変化するプロトコルのDissectorの作り方。
・例1:パケット内部の値の条件によりフォーマットが変わる内容を表示する。
・例2:データサイズが大きく可変であり、パケット内部のデータサイズ値の条件により複数パケットに分割(フラグメント)された内容をもとに戻して(リアセンブル)表示する。

Dissectorを作る際の3つのポイント

この章は、本ページ内に記載したサンプルコードと見比べて読んでください。

1. 記法の違いによる混乱に陥らない方法

参考サイト毎の異なるコーディングについて

初めて実際にDissectorを作成しようとする人がネット上でサンプルコードを探すと、同じ事をしているようなのに記法が異なるコードが見つかり、どれを参考としてよいか混乱することがあります。

代表的なものが、「ProtoField」と「treeitem:add」の書き方です。「ProtoField」に関しては、例えばProtoField.new("サイズ","origin.uint",ftypes.UINT32)と書いている場合と、ProtoField.uint32("origin.uint","サイズ") と書いている場合があります。両者は同じことを意味しており、前者がデータ型を引数(ftypes.UINT32)で指定しているのに対し、後者はメソッド(uint32)で指定しています。

結局わかりやすいのは公式サイトのマニュアル

コードの記法の違いで混乱しないためには、コードの仕様を確認する方法を知っておく必要があります。コードの仕様を確認するにはWiresharkの公式サイトの「Wireshark Developer’s Guide」が便利です。「ProtoField」の章を見ると、引数で指定することもメソッドで指定することもできることが分かります。筆者はデータ型を引数渡しする方が好みですのでProtoField.newの記法を使っています。treeItem:addについても公式サイトに記載されています。

2. 様々なデータ型の記法の見つけ方

実際にDissectorを書こうと思うと、bit列や、時刻など、様々なデータ型に対応したい場面が出てきます。例えば、ネットワーク上で時刻の情報がUNIX型のバイト列「5e 71 7b 3f」で流れているものを人間が分かる形式の「Mar 18, 2020 10:37:03」と表示したい場合などです。 ネット上でコードを探すのもよいですが、ここでもWireshark の公式サイトの「Wireshark Developer’s Guide」を読むと記法を見つけられるでしょう。「ProtoField」の章を見ると、以下のとおり利用できるデータ型が記載されています。上記の例の時刻はftypes.ABSOLUTE_TIMEが使えます。

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.

出展:Wireshark Developer's Guide Version 3.3.0

3. エンディアンの理解と対応方法

独自プロトコルの通信データを解析する際は、エンディアンについて理解する必要があります。

エンディアンについて

エンディアンとは、複数のバイトを並べる順序のことで、ビッグエンディアンとリトルエンディアンがあります。プロセッサの違いにより、ビッグエンディアンのプロセッサとリトルエンディアンのプロセッサが存在し、Intelはリトルエンディアンで、SPARCはビッグエンディアンです。
例えば、10進数で51814は16進数で表すと「ca 66」となりますが、この値を4バイトのメモリに格納すると、ビッグエンディアンのプロセッサでは「00 00 ca 66」と格納されるのに対し、リトルエンディアンのプロセッサでは、「66 ca 00 00」と格納され、16進数表記と反対の並びになります。
ネットワーク上にデータを送る場合に、どちらで送るかを意識して通信する必要があります。
なお、ネットワーク通信において、TCP/IPでは、パケットのヘッダ部はビッグエンディアンが用いられていますが、ヘッダ以降の部分(ペイロード)は、通信を行うアプリケーションの仕様で決められます。

Wiresharkでリトルエンディアンのデータを表示する場合は、リトルエンディアン専用のメソッドを利用して変換する必要があります。ビッグエンディアンとリトルエンディアンの表示イメージを図1に示します。

f:id:yasuikj:20200318003938p:plain
図1:ビッグエンディアンとリトルエンディアンの表示

現実世界のエンディアンの実情の例

現在、Intel系のプロセッサが多いためか、ネットワーク上に流れるパケットのペイロード部分が、リトルエンディアンとなっている事も多いです。通信相手がお互いリトルエンディアンであれば、誰もエンディアンを意識しなくてよいので都合が良いというのが理由の1つでしょう。

では、ペイロードをビッグエンディアンとするのはどのような場合があるかというと、Javaアプリケーションはプロセッサによらずビッグエンディアンです。また、制御システムでは現在でもビッグエンディアンを用いている場合が比較的多いようです。

昔は、サーバといえばビッグエンディアンであるSun MicrosystemsのSPARCのサーバが多く、TCP/IPがパケットのヘッダ部がビッグエンディアンであることもあり、ネットワーク上のパケットは全てビッグエンディアンという仕様が多くなったという事もあるのではないかと思います。パケットのバイト列を見て脳内解析する際に、ビッグエンディアンだと16進数と同じ並びなので人間が理解しやすいというのも理由だったのかもしれません。制御システムはライフサイクルが長いですし多くの装置間で通信しているので、一度通信仕様がビッグエンディアンと決まると、装置の一つがリトルエンディアンの新しい装置に変わったとしても、相手がビッグエンディアンのままであれば、通信仕様をリトルエンディアンに変えるわけにはいきません。このような経緯も制御システムでビッグエンディアンを用いているものが比較的多い理由の1つでしょう。

昔よくあったエンディアン問題

SPARC勢が減りだしIntel勢が増えてきたころ、Intlの計算機どおしでエンディアンなど何も考えずにリトルエンディアンで通信しているシステムにて、あるとき、熟練さんがSPARCの装置をネットワークに繋げて通信すると繋がらず、「なんでリトルで流すんだ!昔からネットワークはビッグが普通だろ!」と怒り出すというような場面が見られていたものです。最初に作った方は運用してしまっているので変更できず、後から作った方は、泣く泣く、リトルエンディアンにあわすとかあったものです。
こんなことにならないよう、エンディアンの仕様は明確にしましょう。

リトルエンディアンのデータの表示について

パケット上の各データのエンディアンは、通信仕様によるので、リトルエンディアンのデータは、Dissectorでリトルエンディアンであることを意識して表示する必要があります。ここでは2種類の記法を説明します。
1つ目は、ツリーにaddする際に、 addメソッドの代わりにadd_leメソッドを使う方法です。
2つ目は、ツリーにaddする際に、バッファの値をbuffer(0,4):le_uintなどのメソッドで変換したものを、addメソッドの引数として渡す方法です。具体的なコードは、以下に示すサンプルコードを参照してください。

様々なデータ型(Uint32型、Bit型、bool型、時刻型、byte型)および リトルエンディアンのサンプル

参考として、ここまでの説明した内容のうち、各データ型の使い方、および、add_leメソッドを使ったエンディアン変換のサンプルコードを記します。

-----------------------------------------------------------
-- プロトコルの定義
proto = Proto("originalCS","独自制御プロトコル")

-- プロトコルフィールド定義
-- ProtoField.new(name,abbr,type,[valuestring],[base],[mask],[desc])
-- 引数:
--      name: プロトコルフィールド名、treeに表示される
--      abbr: プロトコルフィールドのフィルタ名
--      type: データ型
--      valuestring(省略可): (本サンプルでは使わないので説明省略) (本サンプルではbit表示時のみnilとして使用)
--      base(省略可): 表示オプション  例: base.DEC 10進表示, base.HEX 16進表示 (本サンプルではbit表示時のみ使用)
--      mask(省略可): データ型のうち、表示対象とするbit位置を指定する (本サンプルではbit表示時のみ使用)
--      desc(省略可): プロトコルフィールドの説明 (本サンプルでは未使用)
--
uint_F  = ProtoField.new("uint符号なし4byte整数","originalCS.uint",ftypes.UINT32)
-- uint_F  = ProtoField.uint32("originalCS.uint","uint符号なし4byte整数") --参考:メソッド記法 上の行と同じ意味
flag1_F  = ProtoField.new("bitフラグ1","originalCS.flag1",ftypes.UINT8,nil,base.DEC,0x80)
-- flag1_F  = ProtoField.uint8("originalCS.flag1","bitフラグ1",base.DEC,nil,0x80) --参考:メソッド記法 上の行と同じ意味
flag2_F  = ProtoField.new("bitフラグ2","originalCS.flag2",ftypes.UINT8,nil,base.DEC,0x40)
-- flag2_F  = ProtoField.uint8("originalCS.flag2","bitフラグ2",base.DEC,nil,0x40) --参考:メソッド記法 上の行と同じ意味
bool_F  = ProtoField.new("bool真/偽","originalCS.bool",ftypes.BOOLEAN)
-- bool_F  = ProtoField.bool("originalCS.bool","bool真/偽") --参考:メソッド記法 上の行と同じ意味
time_F  = ProtoField.new("日時分秒","originalCS.time",ftypes.ABSOLUTE_TIME,nil,base.LOCAL)
-- time_F  = ProtoField.absolute_time("originalCS.time","日時分秒",base.LOCAL) --参考:メソッド記法 上の行と同じ意味
value_F = ProtoField.new("BCD制御値","originalCS.value",ftypes.BYTES)
-- value_F = ProtoField.bytes("originalCS.value","BCD制御値") --参考:メソッド記法 上の行と同じ意味

uint_LittleEndian_F  = ProtoField.new("uint符号なし4byte整数(LittleEndian)","originalCS.uint_LittleEndian",ftypes.UINT32)

-- プロトコルフィールド定義をプロトコルフィールド配列へ登録
proto.fields = {uint_F, flag1_F, flag2_F, bool_F, time_F, value_F, uint_LittleEndian_F}

-- Dissector
function proto.dissector(buffer, pinfo, tree)

    -- パケットインフォメーション情報のprotocolヘッダに表示する名称を設定
    pinfo.cols.protocol = "ORIGINAL_CS"

    -- パケット詳細部のツリーの登録
    local subtree = tree:add(proto,buffer())
    subtree:add(uint_F, buffer(0,4))
    subtree:add(flag1_F, buffer(4,1))
    subtree:add(flag2_F, buffer(4,1))
    subtree:add(bool_F, buffer(5,1))
    subtree:add(time_F, buffer(8,4))
    subtree:add(value_F, buffer(12,2))

    subtree:add("------エンディアン変換の例-----")
    subtree:add_le(uint_F,buffer(14,4))
    --参考:表示内容を、第3引数に直接渡す記法。 表示内容は 14から4byte分をエンディアン変換したもの 上の行と同じ表示となる
    -- subtree:add(uint_F,buffer(14,4),buffer(14,4):le_uint())
end

--  定義したプロトコル(Proto:original)をTCPポート番号を指定して既存のTCPのDissectorに紐づける
tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(3334, proto)

このDissectorを使って「00 00 00 12 80 01 00 00 60 00 40 47 07 53 12 00 00 00」というデータを表示したものを図2に示します。

f:id:yasuikj:20200318164557p:plain
図2:独自プロトコルツリー
図の中のbyte型で表示しているBCDとは、二進化十進数です。
リトルエンディアンの表示は、add_le メソッドで表示しています。

上記の説明で用いた[ORIGINAL_CS]行のキャプチャデータをテキスト形式で以下に記載します。
記載内容をsample.txtというファイル名で空のテキストファイルに転記し保存した上、Wiresharkのメニューの[ファイル]-[開く]からsample.txtを選択してください。
上記の内容と同じ内容がWireshark上で表示されます。テキストファイルの内容を変更してどのような表示になるか確認してみてください。

+---------+---------------+----------+
10:38:17,642,540   ETHER
|0   |08|00|27|c8|48|c2|0a|00|27|00|00|00|08|00|45|02|00|46|00|00|40|00|40|06|49|52|c0|a8|38|01|c0|a8|38|0a|c8|82|0d|06|f0|28|34|e9|84|a0|e3|b5|80|18|08|0a|9c|59|00|00|01|01|08|0a|10|13|88|fe|00|f1|5b|3c|00|00|00|12|80|01|00|00|60|00|40|47|07|53|12|00|00|00|

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

まとめ

本稿では、実際の独自プロトコルのDissectorを作る際に必要となる3つのポイントを説明しました。

  • 1. 記法の違いによる混乱に陥らない方法
  • 2. 様々なデータ型の記法の見つけ方
  • 3. エンディアンの理解と対応方法

本稿を理解することで、実際に独自プロトコルのDissectorを作れるようになっていただけたら幸いです。

 制御システムセキュリティに関わる者として、このようなノウハウを使って独自プロトコルの平文通信が解析されうることを手を動かして実体験として体感した上で、暗号化の要否などセキュリティ対策を検討いただくことを願います。わからない敵は怖いですが、一つ一つ仕組みを理解し基礎体力をつけていくことで、不安を一つ一つ減らしていくことができるのではないでしょうか?

この続きとして、今回「記載していないこと」としてある内容であるTCPとUDPの分割パケットのDissectorの解説が以下に記載してあります。よろしければこちらも読んでみてください。
io.cyberdefense.jp


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