WiresharkでUSBパケットを解析するときのニッチな要求に応える

WiresharkでUSBパケットを解析するときのニッチな要求に応えたときのメモ

概要 & 背景

WiresharkでUSBパケットをキャプチャしたとき、実際に転送されてるデータの中身が見たいというニッチな要求があった。
(送ったデータがちゃんとUSBバスに出てるよね~、という確認がしたいなどの用途)

Wiresharkのセーブデータ渡して「Wiresharkで見てね~」と言ったら「見方が分からん!」と…
で、Excelで一覧表にしてあげようと思い、エクスポートできんかな~と探してみるも、適当な機能が見つからず…
しかたなく、変換スクリプトかましてCSVに出力してExcelで読み込んでみよう、と試した時のメモ。

今回はisochronous転送のデータの中身をダンプしている。

準備

  1. 最初に解析したいパケットをキャプチャしておく(これをやらなきゃ始まらん…)。
  2. 必要なら表示フィルタで解析したいパケットだけ選び出す。
  3. それらのパケットの一部だけ(最初の1秒だけ など)解析したい場合はそのパケット群を選択する。
    RaspberryPiでのやり方が分からん…Windows版なら他のアプリ同様、先頭で左クリック→最後でshift押しながら左クリックでOK。
    どうしても出来なかったら、RaspberryPiでキャプチャしたのを保存して、そのファイル(pcapngファイル)をWindowsにコピーして、 Windows上のWiresharkで読み込んでくだされ…😅
  4. メニューの「ファイル」→「パケット解析をエクスポート」→「JSONとして」を選択
  5. ファイル操作ダイアログが表示される
    • 真ん中の「ファイル名」を入力
    • 左下の「パケットの範囲」で
      • 「表示されたパケット」を選択
      • 「すべてのパケット」または「選択されたパケットのみ」を選択
        (ここで「範囲」を選んで入力すれば選択しなくてもいいのかな?試してないので不明)
    • 「保存」をクリック

これでJSONファイルが保存される。

CSVファイルに変換

さぁ、このJSONファイルを読み込んで必要な部分を取り出すスクリプトを書けばOKじゃん?と思ったが、
そうは問屋がおろしてくれない😢
じつはWiresharkがエクスポートするJSONファイルはJSONファイルの文法からはずれた部分があるのだ…
具体的には、/_source/layers/usb の配下に複数のキーusbがあって、すべてのUSBデータを読み込めない。
(JSONの仕様では同一階層に複数の同じキーの存在を許さない。pythonのJSONモジュールでは複数あるデータのうち、一つだけが読み込まれる)
ここが配列になってればOKだと気が付いて、チカラワザで変換する処理を追加してみた。

で、pyshonで書いたスクリプトがこちら。
Windows/RaspberryPi どっちでも大丈夫と思うけど、Windowsでしか試してない。
pythonは3.6以降が必要。3.7.7で動作確認。

json_read.py

import sys
import os

import json
import datetime

# テンポラリファイル名
tmp_file = 'tmp.json'

def usage() :
    print( '==== USAGE =========================')
    print(f'    {sys.argv[0]} <JSON file> <CSV file>')
    print( '====================================')

if len(sys.argv) == 3 :
    json_file = sys.argv[1]
    csv_file  = sys.argv[2]
else :
    usage()
    sys.exit(1)

if not os.path.isfile(json_file) :
    print( 'Error: JSON file not exist!!')
    usage()
    sys.exit(1)

# "usb" キーが複数あるので、これをリストに変換したJSONファイルを作成する
# かなり力技...
def modify_json(json_file, tmp_file) :
    with open(json_file) as f_json, open(tmp_file, 'w') as f_tmp:
        line_number = 0;
        line = f_json.readline()
        
        find_flag = False
        
        while line:
            line_number = line_number + 1           # 行番号
            # line = line.rstrip('\r\n')              # CRLFを削除
            if find_flag :
                line = line.replace('"usb": ', '')      # key名称 "usb"を削除
                if first_flag :
                    # print(f'START: {line_number} {line}')
                    f_tmp.write(f'          "usb_data": [\n')
                    first_flag = False
                brackets = brackets + line.count('{')
                brackets = brackets - line.count('}')
                if brackets < 0 :
                    # print(f'END: {line_number} {line}')
                    f_tmp.write(f'          ]\n')
                    find_flag = False
            else :
                if line.startswith('          "usb.iso.numdesc":') :
                    f_pos = f_json.tell()
                    next_line = f_json.readline()        # 次の行を読み込んで
                    f_json.seek(f_pos)                   # ファイル位置を戻す
                    if next_line.startswith('          "usb":') :
                        find_flag = True
                        first_flag = True
                        brackets = 0                    # 括弧の数
            f_tmp.write(line)
            line = f_json.readline()

# JSONファイルの修正
modify_json(json_file, tmp_file)

# パケット解析用変数
stream_number = 0
frame_number = 0

# JSONファイルの読み込み
with open(tmp_file) as f:
    json_load = json.load(f)
    # print(json_load)

with open(csv_file, 'w') as f_csv :
    # ヘッダの出力
    f_csv.write('PacketNo,Date,Relative_time,Delta_time,Packet Size,Video Stream Size,Video Stream offset,Frame No,Stream No,Stream Data\n')
    for json_data in json_load :
        # フレームデータ
        frame_data          = json_data["_source"]["layers"]["frame"]
        frame_index         = frame_data["frame.number"]
        frame_time_epoc     = frame_data["frame.time_epoch"]
        frame_time_delta    = frame_data["frame.time_delta_displayed"]
        frame_time_relative = frame_data["frame.time_relative"]
        frame_time_dt       = datetime.datetime.fromtimestamp(float(frame_time_epoc))
        frame_time          = str(frame_time_dt)
        frame_len           = frame_data["frame.len"]
        # print(f'{frame_index},"\'{frame_time}",{frame_len},', end='')
        f_csv.write(f'{frame_index},"\'{frame_time}",{frame_time_relative},{frame_time_delta},{frame_len},')
        
        # USBデータ
        usb_datas  = json_data["_source"]["layers"]["usb"]["usb_data"]
        first_flag = True
        for usb_data in usb_datas :
            if first_flag :
                first_flag = False
            else :
                # print(f',,,,,', end='')
                f_csv.write(f',,,,,')
            iso_len  = int(usb_data['usb.iso.iso_len'])
            iso_off  = usb_data['usb.iso.iso_off']
            if (iso_len > 0) :
                iso_data = usb_data["usb.iso.data"]
                iso_data = '0x'+iso_data.replace(':', ',0x')
                if stream_number == 0 :
                    frame_number = frame_number + 1
                    frame_number_str = str(frame_number)
                else :
                    frame_number_str = ''
                
                # print(f'{iso_len},{iso_off},{stream_number},{iso_data}')
                f_csv.write(f'{iso_len},{iso_off},{frame_number_str},{stream_number},{iso_data}\n')
                if iso_data.startswith('0x02,0x02') or iso_data.startswith('0x02,0x03') :
                    stream_number = 0
                else :
                    stream_number = stream_number + 1
            else :
                # print(f'{iso_len},{iso_off},,')
                f_csv.write(f'{iso_len},{iso_off},,\n')


# テンポラリファイルの削除
os.remove(tmp_file)

実行方法

以下のコマンドで実行する。(RaspberryPiだとpython3にしてちょ)

python json_read.py «入力JSONファイル» «出力CSVファイル»

ちょっと説明

やっつけスクリプトなので、エラーチェックはかなりいい加減…

関数modify_json() が 前述のJSONファイルの不具合をチカラワザで修正する処理。

テンポラリファイルとしてカレントディレクトリにtmp.jsonを作成するので、注意。 ファイル名を変更したければ、8行目のtmp_fileを変更。
このファイルはスクリプトの最後で削除している。
作成したテンポラリファイルを残しておきたければ最後のos.remove(tmp_file)をコメントアウト。

71~72行目で修正したJSONファイルを読み込み。

75行目で書き出すCSVファイルをオープン。

78行目~のforループで各JSONレコードを読み込みながら処理。

82,85~86行目でframe.time_epochから時刻文字列を作成。
時刻はframe.timeを使用する手もあるが、ここはプラットフォームによって変化するらしいので同じ表示にするためにエポックタイムから生成している。
その他時刻関連データでは、frame.time_delta_displayedで「前のパケットからの相対時間」、 frame.time_relativeで「最初のパケットからの相対時間」を取得している。

94行目~のforループがデータを取り出す部分。
isochronous転送のデータではないデータを取り出したい場合は、所望のデータのキーに置き換えて取り出せば良い。
frame_numberstream_numberは私の解析用の補助データなので気にしないでネ。

あとは、エクスポートされたJSONファイルとスクリプトを見比べてちゃぶだい。(^^ゞ

おしまい

あとは、csvファイルをExcelで読み込むなり、pandasとかを使った別のスクリプトで加工するなりしてちょ。