はじめに
特定のデータファイルなどを改変したい際、その構造を操作する方法が提供されていない場合、最終的にはバイナリ解析して改変することになるかと思います。
チェックサムなどがある場合、その方法も解析が必要になってくるのだと思いますが、ここでは単純なデータ構造のバイナリファイルを解析した際に使用した、C#のコードについての備忘録です。
なお、コードは実際のものではなく簡略化した内容としました。
解析について
きわめて原始的な方法で実施していました。
一部を変更し、出力されたファイルと変更前のファイルとをWinMargeで比較し、変わった部分の前後から判断して行くだけです。
今回扱ったデータはそのデータが何であるのかのID、そのデータの長さ、データといった構造になっていました。
単純な構造ですので、データの見方がわかってしまえば簡単です。
バイト配列の操作
バイト配列を操作する簡単なクラスを作成します。
このままだとあまりBinaryReader
と変わりませんが、必要に応じて改変していきます。
public class MyBytes
{
private List<byte> Bytes { get; set; } = new List<byte>();
private int Pos { get; set; }
public MyBytes(string filePath)
{
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs))
{
Bytes.AddRange(br.ReadBytes((int)fs.Length));
}
Pos = 0;
}
public byte[] ReadBytes(int count, int offsetPos = 0, bool readNext = true)
{
List<byte> result = Bytes.GetRange(Pos + offsetPos, count);
if (next)
readNext += offsetPos + count;
return result.ToArray();
}
public bool EOF { get { return Bytes.Count <= Pos; } }
}
よく使う型のクラス
例えば、データの長さを示す形式がint型なら、int型を操作するクラスを作成します。
[System.Diagnostics.DebuggerDisplay("{Value} ({System.BitConverter.ToString(ToBytes()).Replace(\"-\", \" \")})")]
public class MyInt : IEnumerable<byte>
{
public int Value { get; set; }
public MyInt(MyBytes bytes)
{
Value = BitConverter.ToInt32(bytes.ReadBytes(4), 0);
}
public MyInt(int value)
{
Value = value;
}
public byte[] ToBytes()
{
return BitConverter.GetBytes(Value);
}
public static implicit operator int(MyInt obj)
{
return obj.Value;
}
public static implicit operator byte[](MyInt obj)
{
return obj.ToBytes();
}
public static implicit operator MyInt(MyBytes bytes)
{
return new MyInt(bytes);
}
public static implicit operator MyInt(int value)
{
return new MyInt(value);
}
public IEnumerator<byte> GetEnumerator()
{
return ToBytes().ToList<byte>().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ToBytes().GetEnumerator();
}
}
ポイント
- デバッグ時にウォッチで見やすくするように
DebuggerDisplay
を定義 - 要求する型に合わせて動作するように
implicit
を定義 - 繰り返しの要求用に
IEnumerable
を定義 -
MyBytes
からデータを読み進めるため、MyBytes
での使用前提のクラス
解析用のクラス
解析した結果データ構造が判明した場合、各データ項目を操作しやすくするクラスを作成します。
public class MyData
{
public MyBytes_Int DataID { get; set; }
public MyBytes_Int Length { get; set; }
public byte[] Data { get; set; }
public EspB_Short DataSize { get; set; }
public MyData(MyBytes bytes)
{
DataID = bytes;
Length = bytes;
Data = bytes.ReadBytes(Length);
}
public byte[] ToBytes()
{
List<byte> result = new List<byte>();
b.AddRange(DataID);
b.AddRange(Length);
b.AddRange(Data);
return result.ToArray();
}
}
後は上記のクラスを基準にデータ構造に合わせたクラスを順次作成して行きます。
データの読み込みと書き出し
一通りデータの解析が済みましたら、データを読込み、必要な操作を行い、ファイル出力します。
MyBytes bytes = new MyBytes(@"C:\Hoge.xxx");
List<MyData> list = new List<MyData>();
while (bytes.EOF)
{
list.Add(new MyData(bytes));
}
/* (何らかの改変) */
using (FileStream fs = new FileStream(@"C:\Hoge2.xxx", FileMode.Create))
using (BinaryWriter bw = new BinaryWriter(fs))
{
foreach (var item in list)
{
bw.Write(item.ToBytes());
}
}
最後に
実際にはもう少し複雑ですが、このような形で進めていました。
やってみれば意外にバイナリ解析は簡単なもので、後は地道な作業です。
クラス設計的に、MyInt
はMyBytes
からデータを読み出すこと前提で作っていたりなど、設計思想的には好ましくない部分が多いのかもしれませんが、公式に公開するものではないので、とりあえずやりやすいように作ったものです。
以上