bin_parser_to_be_tested.py 技術ドキュメント

プログラムの動作

bin_parser_to_be_tested.py は、未知のバイナリファイルの探索を支援するためのPythonスクリプトです。このプログラムの主な目的は、バイナリデータの中から特定の数値(整数や浮動小数点数)や文字列のパターンを検出し、その周辺のデータを調査することによって、ファイルの構造を解明する手助けをすることです。

主な機能は以下の通りです。

  • 値の検索 (search): 指定されたデータ型、値、エンディアン、および検索ステップ(アライメント)に基づいて、バイナリファイル内で数値または文字列を検索します。浮動小数点数の比較には許容誤差を設定できます。

  • 文字列のリストアップ (strings): バイナリデータの中から表示可能なASCII文字列を抽出し、指定された最小長以上のものをリストアップします。特定のサブ文字列を含むものでフィルタリングすることも可能です。

  • オフセットの検査 (inspect): 特定のオフセットに存在するバイナリデータを、複数の異なるデータ型(例: float32, int16, strなど)として解釈し、それぞれの値を表示します。これにより、同じバイト列がどのように異なるデータとして読み取られるかを確認できます。

  • 配列の読み込み (array): 指定されたオフセットから、連続する数値型のデータを配列として読み込み、各要素のオフセットと値を表示します。要素間のステップ(ストライド)も指定可能です。

  • データのダンプ (dump): 指定されたオフセットから指定された長さのバイナリデータを、16進数とASCII文字の両方で表示するヘックスダンプを行います。

このプログラムは、ファイルフォーマットが不明なデータセットや、逆アセンブルされたバイナリコードのスライスを解析する際に特に有用です。

原理

bin_parser_to_be_tested.py は、Pythonの標準ライブラリである struct モジュールと正規表現 (re) を核として、バイナリデータの読み取り、解析、および検索を行います。

  1. バイナリデータの読み込み: load_bin_file 関数は、指定されたファイルパスからバイナリデータをバイト列 (bytes オブジェクト) としてメモリに読み込みます。これは open(file_path, "rb") を使用して行われます。

  2. エンディアンの指定: endian_prefix 関数は、ユーザーが little (リトルエンディアン) または big (ビッグエンディアン) のどちらを指定したかに応じて、struct モジュールで使用するフォーマット文字列のプレフィックスを決定します。リトルエンディアンの場合は <、ビッグエンディアンの場合は > が使われます。

  3. データ型のマッピング: TYPE_MAP ディクショナリは、サポートされる各データ型(例: float32, int16, uint8 など)に対して、struct モジュールが認識するフォーマット文字と、その型のバイトサイズをマッピングします。例えば、"float32"("f", 4) に対応し、これは単精度浮動小数点数を意味します。

  4. バイナリデータの解釈 (get_data, read_array): get_data 関数は、指定されたオフセットから、指定されたサイズとデータ型、エンディアンに従ってバイナリデータを読み込み、Pythonの対応する型(int, float, str)に変換します。これは struct.unpack_from(prefix + fmt, bin_data, offset)[0] を使用して実現されます。 文字列の読み込みに関しては、ascii_prefix (表示可能なASCII文字のみを読み込む)、c_string (NULL文字で終端される文字列を読み込む)、fixed (固定長で文字列を読み込む) の3つのモードがあります。 read_array 関数は、get_data を繰り返し呼び出すことで、特定の型の配列データを連続して読み取ります。step 引数により、要素間のバイトオフセット(ストライド)を制御できます。

  5. 値の検索 (search_data):

    • 数値の検索: search_data 関数は、指定されたオフセット範囲内で、step バイトごとにデータを読み込み、ターゲット値と比較します。浮動小数点数の比較には、numeric_match 関数が絶対許容誤差 (atol) および相対許容誤差 (rtol) を用いて行われます。数式は以下の通りです。 $\(|val - target| \le (atol + rtol \cdot |target|)\)\( ここで \)val\( はバイナリから読み取った値、\)target$ は検索対象の値です。

    • 文字列の検索: search_raw_string 関数は、Pythonのバイト列オブジェクトの find() メソッドを使用して、エンコードされたターゲット文字列(バイト列)を効率的に検索します。

  6. ASCII文字列の抽出 (find_ascii_strings): find_ascii_strings 関数は、正規表現 rb"[\x20-\x7E]{%d,}" % min_length を使用して、ASCIIの表示可能文字(スペース 0x20 からチルダ 0x7E まで)が min_length 以上連続するパターンを検索します。re.finditer を使用してすべてのマッチを見つけ、そのオフセットとデコードされた文字列を返します。

  7. ヘックスダンプ (hex_dump): hex_dump 関数は、バイナリデータを読みやすくするために、指定されたオフセットと長さのデータを、16進数表記とASCII文字表記の両方で整形して出力します。これは一般的なバイナリエディタやデバッガの機能と同様です。

必要な非標準ライブラリとインストール方法

bin_parser_to_be_tested.py は、Pythonの標準ライブラリのみを使用しており、argparse, math, os, re, struct, sys, typing 以外の追加の非標準ライブラリは必要ありません。

したがって、特別なインストール手順は不要です。Pythonがインストールされていれば、そのまま実行できます。

必要な入力ファイル

プログラムは、バイナリファイルを読み込むことを想定しています。 期待されるファイル形式やデータ構造は特に指定されません。プログラムは、任意のバイナリデータを生のバイト列として読み込み、ユーザーの指定に基づいてその内容を解析しようとします。

入力ファイルのパスは、コマンドライン引数として常に必要となります。

例: test.raw のような任意のバイナリファイル。

生成される出力ファイル

bin_parser_to_be_tested.py は、ファイルシステム上に新しいファイルを生成することはありません。すべての処理結果は標準出力 (stdout) にテキスト形式で表示されます。

ユーザーは、シェルのリダイレクト機能(例: > output.txt)を使用して、標準出力をファイルに保存することができます。

コマンドラインでの使用例 (Usage)

bin_parser_to_be_tested.py は、サブコマンド形式のコマンドラインインターフェースを提供します。基本的な実行方法は以下の通りです。

python bin_parser_to_be_tested.py <command> [arguments]

利用可能な主なコマンドと引数は以下の通りです。

strings コマンド

ファイルから表示可能なASCII文字列をリストアップします。

python bin_parser_to_be_tested.py strings <infile> [options]
  • <infile>: 入力バイナリファイルのパス。

  • --min-length <int>: 検索する文字列の最小長 (デフォルト: 4)。

  • --contains <substring>: 特定のサブ文字列を含む文字列のみをフィルタリングします。

  • --max-hits <int>: 表示する最大ヒット数 (デフォルト: 200)。

inspect コマンド

特定のオフセットのデータを複数の型で解釈します。

python bin_parser_to_be_tested.py inspect <infile> --offset <offset> [options]
  • <infile>: 入力バイナリファイルのパス。

  • --offset <offset>: データを読み込む開始オフセット (16進数は 0x プレフィックスで指定可能)。

  • --types <type1,type2,...>: 解釈するデータ型のカンマ区切りリスト (デフォルト: float32,int16,uint16,str)。

  • --sizes <size1,size2,...>: --types に対応するサイズのカンマ区切りリスト。指定しない場合、文字列型は --str-size を、その他の型はデフォルトサイズを使用。

  • --str-size <bytes>: 文字列型解釈時のデフォルトサイズ (デフォルト: 32)。

  • --str-mode <ascii_prefix|c_string|fixed>: 文字列の解釈モード (デフォルト: ascii_prefix)。

  • --endian <little|big>: エンディアンの指定 (デフォルト: little)。

array コマンド

特定のオフセットから数値型の配列を読み込みます。

python bin_parser_to_be_tested.py array <infile> --offset <offset> --type <type> --count <count> [options]
  • <infile>: 入力バイナリファイルのパス。

  • --offset <offset>: 配列の開始オフセット。

  • --type <type>: 配列要素のデータ型。

  • --count <int>: 読み込む要素の数。

  • --step <bytes>: 要素間のバイトオフセット(ストライド) (デフォルト: 型サイズ)。

  • --endian <little|big>: エンディアンの指定 (デフォルト: little)。

dump コマンド

特定のオフセットからデータをヘックスダンプします。

python bin_parser_to_be_tested.py dump <infile> --offset <offset> [options]
  • <infile>: 入力バイナリファイルのパス。

  • --offset <offset>: ダンプを開始するオフセット。

  • --length <bytes>: ダンプするバイト数 (デフォルト: 128)。

  • --width <int>: 各行に表示するバイト数 (デフォルト: 16)。

コマンドラインでの具体的な使用例

ここでは、test.raw という架空のバイナリファイルを前提とした実行例とその説明を示します。

例1: 浮動小数点数 120.0 をリトルエンディアンで4バイトアライメント検索

python bin_parser_to_be_tested.py search test.raw --type float32 --value 120.0 --endian little --step 4 --tolerance 1e-3

説明: test.raw ファイル内で、単精度浮動小数点数 (float32) の 120.0 をリトルエンディアンで検索します。検索は4バイトステップ(典型的な float32 のアライメント)で行われ、許容誤差は \(10^{-3}\) です。ヒットしたオフセットと、その周辺64バイト(ヒット地点の16バイト前からの)のヘックスダンプが表示されます。

実行結果の例:

Found 1 match(es).
offset 0x00000100 (256)
000000F0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000100  00 00 F0 42 00 00 00 00 00 00 00 00 00 00 00 00  |...B............|
00000110  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000120  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|

0x00000100 のオフセットに float32120.0 (リトルエンディアンで 00 00 F0 42) が見つかったことを示します。)

例2: 同じ値をフルブルートフォーススキャン

python bin_parser_to_be_tested.py search test.raw --type float32 --value 120.0 --step 1

説明: test.raw ファイル内で、単精度浮動小数点数 120.0 をリトルエンディアンで検索します。今度は1バイトステップ(フルブルートフォーススキャン)で行われます。これにより、アライメントされていない可能性のあるデータも検出できますが、処理時間は長くなり、誤検出も増える可能性があります。ヒットしたオフセットと周辺のダンプが表示されます。

実行結果の例:

Found 2 match(es).
offset 0x00000100 (256)
000000F0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000100  00 00 F0 42 00 00 00 00 00 00 00 00 00 00 00 00  |...B............|
00000110  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000120  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|

offset 0x00000201 (513)
000001F1  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000201  00 00 F0 42 00 00 00 00 00 00 00 00 00 00 00 00  |...B............|
00000211  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000221  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|

0x000001000x00000201 の2つのオフセットでヒットが見つかり、2つ目のオフセットは4バイトアライメントではないことを示します。)

例3: テキストフラグメント DEVICE を直接検索

python bin_parser_to_be_tested.py search test.raw --type str --value DEVICE

説明: test.raw ファイル内で、文字列 "DEVICE" を検索します。文字列検索はデフォルトで1バイトステップで行われます。ヒットしたオフセットと、その周辺のヘックスダンプが表示されます。

実行結果の例:

Found 1 match(es).
offset 0x00000040 (64)
00000030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000040  44 45 56 49 43 45 00 00 00 00 00 00 00 00 00 00  |DEVICE..........|
00000050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|

0x00000040 のオフセットに "DEVICE" という文字列が見つかったことを示します。)

例4: 表示可能なASCII文字列をリストアップ

python bin_parser_to_be_tested.py strings test.raw --min-length 4

説明: test.raw ファイルから、長さが4文字以上の表示可能なASCII文字列をすべて抽出してリストアップします。各文字列はそのオフセットとともに表示されます。

実行結果の例:

0x00000040 (64): DEVICE
0x00000080 (128): FIRMWARE_V1.2
0x000003A0 (928): LOG_DATA_START

(ファイル内の複数のASCII文字列とそれらのオフセットが表示されます。)

例5: 特定のオフセット 0x120 のデータを複数型で検査

python bin_parser_to_be_tested.py inspect test.raw --offset 0x120 --types float32,int16,uint16,str --sizes 4,2,2,32

説明: test.raw ファイルのオフセット 0x120 にあるバイナリデータを、float32 (4バイト)、int16 (2バイト)、uint16 (2バイト)、str (32バイト) の各型として解釈し、その結果を表示します。--sizes 引数で各型に適用する明示的なサイズを指定しています。解釈結果と、そのオフセット周辺の64バイトのヘックスダンプが表示されます。

実行結果の例:

File: test.raw
Offset: 0x00000120 (288)
- float32  size=4   value=3.14159265359
- int16    size=2   value=-1024
- uint16   size=2   value=64512
- str      size=32  value=Sample String at Offset 0x120

00000100  00 00 F0 42 00 00 00 00 00 00 00 00 00 00 00 00  |...B............|
00000110  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000120  DB 0F 49 40 00 FC 00 FC 53 61 6D 70 6C 65 20 53  |..I@.. ..Sample S|
00000130  74 72 69 6E 67 20 61 74 20 4F 66 66 73 65 74 20  |tring at Offset |
00000140  30 78 31 32 30 00 00 00 00 00 00 00 00 00 00 00  |0x120...........|
00000150  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|

(指定されたオフセット 0x120 のバイト列が、各型でどのように解釈されるかが表示されます。例では DB 0F 49 40float32 として 3.14159265359 に、00 FCint16 として -1024 に、00 FCuint16 として 64512 に、53 61 6D ... が文字列に解釈されています。)

例6: オフセット 0x200 から float32 の配列を16要素読み込み

python bin_parser_to_be_tested.py array test.raw --offset 0x200 --type float32 --count 16 --step 4

説明: test.raw ファイルのオフセット 0x200 から、float32 型のデータを16要素分読み込みます。各要素は4バイトステップで配置されていると仮定されます。読み込まれた各要素のインデックス、オフセット、および値が表示されます。

実行結果の例:

[   0] offset=0x00000200 (   512) value=1.0
[   1] offset=0x00000204 (   516) value=2.0
[   2] offset=0x00000208 (   520) value=3.0
[   3] offset=0x0000020C (   524) value=4.0
[   4] offset=0x00000210 (   528) value=5.0
[   5] offset=0x00000214 (   532) value=6.0
[   6] offset=0x00000218 (   536) value=7.0
[   7] offset=0x0000021C (   540) value=8.0
[   8] offset=0x00000220 (   544) value=9.0
[   9] offset=0x00000224 (   548) value=10.0
[  10] offset=0x00000228 (   552) value=11.0
[  11] offset=0x0000022C (   556) value=12.0
[  12] offset=0x00000230 (   560) value=13.0
[  13] offset=0x00000234 (   564) value=14.0
[  14] offset=0x00000238 (   568) value=15.0
[  15] offset=0x0000023C (   572) value=16.0

(インデックス 0 から 15 までの float32 値が、それぞれのオフセットとともに表示されます。)