DOCFILEを解析してみた Microsoft Compound File Binary File Format
2023/04/23 加筆/修正・序章バイナリファイルや、メモ帳で開くと半角カタカナで「ミマ」と表示されるファイル。Header Signatureと呼ばれる情報で、規格化さている。規格は、マイクロソフトにより規定され。[MS-CFB]:Compound File Binary File Formatというドキュメントが公開されています。詳しく見ると、ファイル先頭が下記の値が記録されてます。0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1.つなげるとD0CF11E0A1B11AE1となり、なんとなくDOCFILEと読めなくもないです。このファイル構造は、昔のExcelやWordなどに使用されており、複数のファイルを纏めて1つのファイルとして扱えるようにしたものです。この形式のファイルを分解できればアプリで保存されている情報をファイル単位に分解することができいろいろはかどる可能性があります。今回は、4月頭に発生したモヤモヤを、ファイル解析してスッキリするのが目的です。得られた知見を覚書として公開しておきます。当初OpenOfficeの団体が公開している「compdocfileformat.pdf」がわかりやすそうなので、読みましたが、結局はマイクロソフト出している規格書を読んだほうがスッキリします。書かれた内容は、規格書の焼き直しなので権利は一切ありませんが、文章自体は私が作成しましたので権利を有します。転載しないでね。一応Pythonで、スクリプトを書いて動作確認しながら内容を理解したつもりですので、概ねあっていると思いますが、間違いがありましたらご指摘ください。・基本 ファイルの構造は、ヘッダ情報(compound file header)1セクタ分とセクタ番号(sector number)0から始まるセクタの集合 セクタ番号からアドレスを求めるには、(セクタ番号+1)*セクタサイズで求めることができる。 ファイル構造自体にバージョンが有り、バージョン3と4がある。 主にセクタサイズが異なる。規格書自体がバージョン3に当て書きされているので余り気にしない。 バージョン3は512バイトのセクタサイズを基本に構成されています。・ディレクトリエントリ(directory entry) フォルダ名/ファイル名に当たるもの(Directory Entry Name)と、ファイルの開始セクタ、容量、作成日時等の情報を持っている。 大元には必ず「root storage object」がいる。 ディレクトリエントリもセクタ単位で管理されており、開始セクタはヘッダファイルに記載がある「First Directory Sector Location」。 128バイト単位の情報で構成されているので、1セクタあたり、4エントリずつ記載されています。 開始セクタに対応するFATを参照すると、次のセクタ番号が書かれているので、 それを辿ってセクタを繋いでいくと、ディレクトリエントリのリストが構成できる。 親子関係を示すIDは、「root storage object」が0番で、以下エントリに出現順にナンバリングされているようです。 ※「Number of Directory Sectors」というフィールドがありますが、バージョン3では、0なので使えません。 ・DIFAT(double-indirect file allocation table) FATセクタを管理するDIFATというものがあり、FATセクタの構成が記述されている。 DIFATは、既定でヘッダファイルの一部を構成しており、109個のFATセクタまで管理できる。 それ以上DIFATがある場合は、ヘッダ内に情報が書かれているので参照する。 ヘッダ部オフセット、0x44から4バイトの「First DIFAT Sector Location」がDIFATの最初のセクタ(ヘッダ部にある109個の続きにあたる。ない場合はEOC(0xFFFFFFFE)が書かれる)、 同、0x48からの4バイトが、「Number of DIFAT Sectors」で、DIFATセクタの数。(同じくヘッダ部のDIFATは含まれない) DIFATのセクタは、最後の4バイトが次DIFATのセクタ番号となるので注意。1セクタあたり、127個のFATエントリとなる。(セクタサイズ512/FATセクタのセクタ番号バイト-1) 最終のDIFATセクタの最後の4バイトにはEOC(0xFFFFFFFE)が書かれる。 ※余談だがDIFATが、通常DIFATは未使用との情報が定義されていたのでダイレクトにFATが展開されていると思い、 解析していたが、どうも合わないので苦労しました。 まずDIFATに書いてあるFATセクタを並べて、FATセクタの集合が構成される。さらにFATセクタのFATエントリをたどってセクタ並べると最終なセクタの集合となる。 ※余談2:DIFATのチェーンは、最後の4バイトで次のセクタが指示される。FATで管理していないので注意。これもハマった。・FAT (file allocation table) FATは、対応するセクタの次のセクタ番号が書かれている。 FATをたどり、セクタを並べるとファイルが再構成できる。 FATもセクタ単位で管理されている。1FATあたり、4バイト使用するので、FATセクタ内に512/4=128個のFATが管理できる。 ファイル(のようなもの=ストリーム)は、各ディレクトリエントリにかかれている開始セクタ番号「Starting Sector Location」から開始する。 スタートセクタに対応するFATを参照すると、次のセクタ番号が書かれているので、数珠つなぎに参照していけば、 順序付けられたセクタの集合(sector chain)がファイルとして得られる。ファイル(ストリームstream?)のサイズ情報は、 ディレクトリエントリに記載があるので、セクタ単位で構成したあまりは、そのサイズ情報でカットすれば良いと思います。 終点は特殊なセクタ番号「ENDOFCHAIN (0xFFFFFFFE)」が用意されているのでそれをみて数珠つなぎの終点とする。 また、使用していないセクタ(unallocated free sector)もFATに特殊のセクタ番号が書かれることで管理される。 ※FATなのか、FATセクタなのか、注意して規格書を読まないと混乱します(してました)・ミニFATとミニストリーム(mini FAT mini stream) FAT/FATセクタは、ミニセクタと標準セクタが用意されている。 ヘッダ情報に記載されているしきい値(Mini Stream Cutoff Size)が設定されており、ストリームの容量が4096バイトより小さいとミニセクタにより管理される。 ディレクトリエントリの容量情報「Stream Size」を確認して、開始セクタがセクタを参照しているのか、ミニセクタを参照しているのか処理を切り替える。 ミニセクタは、64バイト単位。 ミニセクタにもFATがある。ミニFATの情報は、ヘッダに位置情報とサイズ書かれている。 ミニセクタの開始セクタは、ディレクトリエントリのルートディレクトリエントリに記載がある「The root directory entry の Starting Sector Location」。 セクタ番号からアドレスを求めるには、ミニセクタ開始セクタ+ミニセクタ番号*セクタ(=64Byte)を用いる。 ミニセクタも基本セクタ単位で管理されている。基本セクタあたりのミニセクタの数は512/64バイト=8個単位で管理される。 ミニセクタの集合は、ミニFATの情報を参照し、該当セクタの次のセクタ番号を取得する。・フォルダ構造 今回は内部のファイルがベタで読めれば良かったので追求していない。 ストレージ(storage objects)とストリーム(stream objects)があり、ストリームがデータ情報を持っている。 ディレクトリエントリの内のタイプ情報「Object Type」で判別できる。 チルドレンは多分このストレージとストリームの関係を定義するの値。 赤黒木については、不明。ストレージ内のストリームリストをこれで保持しているかも。 エントリーの名前の長さで、まず左右に振り分け、文字数が同じなら文字コード順にまた左右に振り分けていきツリー構造のリストが作成されます。・日付 不明 1601/1/1 からの100ナノ秒単位 LibreOffice 0=1899/12/30 00:00 1日単位 → =(セル/10/1000/1000/60/60/24)-299*365-70+9/24 Excel 0=1900/1/0 12:00AM 1日単位(但し、1900/2/29が存在) ・class identifier (CLSID)とかglobally unique identifier (GUID)とか 不明