← ブログに戻る

MFT レコードの内部、バイト単位で

· 読了 2 分

すべての MFT レコードは 1,024 バイトです。どのレコードも同じ方法でレイアウトされます。ヘックスエディタで 1 つ読めれば、すべて読めるようになり、損傷したレコードの回復で脆い商用ツールを打ち負かすパーサが書けるようになります。レイアウトは小さく、数時間と 010 Editor があれば習熟できます。

これは MFT パーサをブラックボックスとして扱うのをやめたいアナリスト向けに私が行うバイトレベルのツアーです。

FILE シグネチャ

46 49 4C 45  ("FILE")

最初の 4 バイト。ASCII で FILE です。$MFT 自身が読み取れないときに生クラスタから MFT レコードをカービングできるマジックです。ディスクイメージを 512 バイト境界(古い NTFS)または 4096 バイト境界(4K レコードの Win10、稀ですが存在します)で走査し、46 49 4C 45 のパターンマッチを行えば、テーブル自身のヘッダが破壊されていても通常 MFT のほとんどを回復できます。

実環境で目にするもう 1 つのシグネチャは BAAD42 41 41 44)です。NTFS は chkdsk がレコードを修復不能と判定したときにこれを書き込みます。スロットは保持され、シーケンス番号は保たれますが、それ以外のバイトは信用すべきではありません。BAAD レコードは chkdsk が実行された証拠として扱い、周辺コンテキスト分析の対象にしてください。その属性を素朴に解析しようとはしないでください。

レコードヘッダ(オフセット 0x00 から 0x37 前後まで)

シグネチャの 4 バイトの後、NTFS は NTFS バージョンによって若干異なるヘッダをレイアウトします。実際に使用するフィールドを、レコード先頭からのオフセットで示します。

0x00  4 bytes  Signature ("FILE" または "BAAD")
0x04  2 bytes  update sequence array(USA)へのオフセット
0x06  2 bytes  16 ビットワード単位の USA サイズ(USN + fixup_entries の数)
0x08  8 bytes  $LogFile シーケンス番号(LSN)
0x10  2 bytes  シーケンス番号
0x12  2 bytes  ハードリンクカウント
0x14  2 bytes  最初の属性へのオフセット
0x16  2 bytes  フラグ(ビット 0 = IN_USE、ビット 1 = DIRECTORY、
                     ビット 2 = QUOTA_CHARGED、ビット 3 = HAS_VIEW_INDEX)
0x18  4 bytes  レコードの使用サイズ
0x1C  4 bytes  アロケート済みサイズ(標準ボリュームでは常に 1024)
0x20  8 bytes  ベースファイルレコード参照(拡張レコードでは非ゼロ)
0x28  2 bytes  次の属性 ID
0x2A  2 bytes  (NTFS 3.0 のパディング/アラインメント)
0x2C  4 bytes  レコード番号(NTFS 3.1+、自己参照)

いくつかは特別に注目する価値があります。

0x16 のフラグ。 ビット 0 がクリアであれば削除済みです。ビット 1 がセットであればディレクトリです(レコードは $DATA ではなくインデックス属性を保持します)。両方のフラグが意味を持つ組み合わせがあるため、1 バイトが多くを物語ります。

0x10 のシーケンス番号。 スロットが再利用されるたびに増加します。64 ビットの ファイル参照(下位 48 ビットにレコード番号、上位 16 ビットにシーケンス番号)が、特定のファイルの存在に対する実際の一意識別子です。他の属性内の参照($FILE_NAME の親参照、$ATTRIBUTE_LIST のエントリ)はこの 64 ビット形式を使います。シーケンスが現在のレコードと一致しない参照は、以前の住人、通常は削除済みファイルを指します。これが Sleuth Kit が削除されたディレクトリチェーンを走査する方法です。

0x20 のベースファイルレコード参照。 ベースレコードではゼロ、拡張レコード(1 つのファイルの属性が単一スロットからあふれた場合)では非ゼロです。非ゼロの値は、この拡張が属するベースレコードの 64 ビット参照です。パーサは完全なファイルを組み立てるため $ATTRIBUTE_LIST チェーンをたどらなければなりません。

0x08 の $LogFile シーケンス番号。 $LogFile を指します。トランザクションレベルの回復に有用です。日常的な分析にはあまり有用ではありません。存在を知っておく価値はあります。

fixup(update sequence)配列

NTFS は torn write に対して小さな仕掛けで保護します。各 1,024 バイトのレコードは 2 つの 512 バイトセクタに分かれます。書き込み前に NTFS は次を行います。

  1. 16 ビットの update sequence number(USN、共通の頭字語にもかかわらず $UsnJrnl の USN とは無関係です)を選びます。
  2. 各セクタの末尾 2 バイトの元の値を、ヘッダ直後にある配列に格納します。
  3. 各セクタの末尾 2 バイトを USN 自体で置き換えます。

読み取り時に NTFS は、各 512 バイトセクタの末尾 2 バイトが選ばれた USN と等しいことを確認します。等しければ書き込みはアトミックでした。fixup 配列から元のバイトを取り出して戻します。いずれかのセクタ末尾が一致しなければ書き込みは破れており、レコードは疑わしい状態です。

配列は 1 つの USN ワードに続いて N 個の fixup ワードがあるレイアウトで、N はセクタ数です。512 バイトセクタのボリューム上で 1,024 バイトのレコードの場合、N = 2 です。したがって配列は合計 6 バイト(USNfixup_for_sector_0fixup_for_sector_1)を占めます。そのオフセットはレコードの 0x04 にある 2 バイトの値です(通常は NTFS バージョンによって 0x2A または 0x30)。

実務的な帰結。fixup を適用せずに $MFT の生 1,024 バイトのチャンクを読み取ると、すべてのレコードのオフセット 510 と 1022 にゴミが入ります。これらのオフセットを跨ぐ resident $DATA は破損します。MFTECmd、omerbenamram/mftanalyzeMFT、Sleuth Kit の fls/istat のようなパーサは最初のステップとして fixup を適用します。自前のパーサを書いている場合(良い練習です。Python での parse-mft を参照)、これを何より先に行ってください。

属性ストリーム

ヘッダと fixup 配列の後、すべてのレコードは型付き属性の系列を含みます。属性は 8 バイトアラインで連続して詰め込まれ、次の属性の型コードがあるべき位置に 0xFFFFFFFF のセンチネル値が置かれて終端されます。

各属性は小さな標準化されたヘッダで始まります。

0x00  4 bytes  属性タイプコード(0x10 = $STANDARD_INFORMATION 等)
0x04  4 bytes  この属性の長さ(ヘッダ + データ)
0x08  1 byte   非レジデントフラグ(0 = レジデント、1 = 非レジデント)
0x09  1 byte   名前の長さ(文字数。名前なしなら 0)
0x0A  2 bytes  名前へのオフセット(属性先頭からの文字数)
0x0C  2 bytes  フラグ(圧縮/暗号化/スパース)
0x0E  2 bytes  属性 ID

レジデント属性の場合、次のフィールドはコンテンツ長とコンテンツオフセットです。非レジデントの場合は開始/終了 VCN、ランリストオフセット、圧縮/アロケート/実サイズです。それらすべての後に実際のデータが続きます。名前(ある場合)は Unicode でアラインされていません。実装は手間がかかると思ってください。

最小のレコードには 3 つの属性が含まれます。

  • $STANDARD_INFORMATION0x10。タイムスタンプ、DOS フラグ、セキュリティ ID。
  • $FILE_NAME0x30。名前、親参照、2 つ目の 4 つのタイムスタンプの組、名前が設定されたときのアロケート済み/実サイズ。レコードは複数の $FILE_NAME を持てます(ハードリンクごとに 1 つ、加えて 8.3 生成が有効なボリュームでは 8.3 形式の短縮名)。
  • $DATA0x80。ファイルの内容。収まればレジデント、そうでなければランリストです。レコードは複数の $DATA 属性を持てます。名前のないものがプライマリストリーム、名前付きが 代替データストリーム です。

完全な属性カタログと各属性の所在については Master File Table リファレンス を参照してください。

レコードが損傷したときこれが重要な理由

安定した 1,024 バイトのレイアウト、FILE シグネチャ、fixup メカニズム、自己記述的な属性ヘッダの組み合わせが、削除された NTFS レコードのカービングを可能にしています。$MFT 自体が失われていても(破損、ランサムウェアの落書き、部分的なワイプ)、生ボリュームに対する 46 49 4C 45 境界のシグネチャスキャンは、基盤クラスタが上書きされていない限りレコードを回復します。fixup 検証はセクタごとの整合性チェックを提供します。これに失敗するレコードはフラグを立てるべきですが、その属性データは部分的に読み取れることもあります。

これが Sleuth Kit の mmls+fls、R-Studio の「ディープスキャン」、各種商用回復スイートの基盤です。それらはすべて同じバイトレイアウトを走査しています。自分でそれを知ることで、彼らの出力を健全性チェックできるようになります。

自分で 1 つ読む

抽出した $MFT を 010 Editor で開き、NTFS MFT Record テンプレートを適用します。レコード 5(ルートディレクトリ、ファイル先頭から 5 * 1024 = 5120 のオフセット)を選びます。次を確認します。

  • バイト 0x00 から 0x03。46 49 4C 45
  • 0x16 のフラグ。0x03(IN_USE と DIRECTORY の両方がセット)。
  • 0x14 のオフセットで指定された位置にある最初の属性タイプ。0x10$STANDARD_INFORMATION)。

これらが一致していれば、レコードはきれいに解析されています。一致しなければ、ファイルが破損しているか、fixup 配列が適用されていません(010 Editor のテンプレートが自動で適用してくれます)。

これを一度やれば、MFT の残りは読めるようになります。フィールドは難解なものではなくなり、助けなしで読めるレコードになります。

参考資料

外部リソース