科学・技術
Appleのスパースイメージフォーマット(ASIF)を分析する
Dissecting Apple's Sparse Image Format (ASIF) (schamper.dev)
要約
この記事では、AppleがmacOS 26 Tahoeで発表した新しいディスクイメージフォーマットであるASIF(Apple Sparse Image Format)の解析プロセスを詳細に解説しています。ASIFは仮想マシンでの使用を目的としたスパース仮想ディスクフォーマットで、既存のVMDKやVHDX、QCOW2ファイルに似ています。筆者は、このフォーマットをリバースエンジニアリングしてパーサーを記述する過程を、テストファイルの作成、ヘックスダンプの分析、バイナリの探索といった具体的な手順を追って説明しており、ファイルフォーマットのリバースエンジニアリングに不慣れな読者にも役立つ洞察を提供しています。
全文翻訳
Appleのスパースイメージフォーマット(ASIF)を分析する
2026-06-18 · 18分間の読書 · Erik Schamper
WWDC 2025で、AppleはmacOS 26 Tahoeを発表しました。macOS Tahoeの新機能の一つは、新しいディスクイメージフォーマットであるASIFです。仮想マシンでの使用を目的として設計されており(そのドキュメントは仮想化フレームワークにあります)、ASIFは既存の仮想ディスクフォーマットから多くのインスピレーションを得ています。実用的に言えば、これは別のスパース仮想ディスクフォーマットであり、スパースVMDK、VHDX、QCOW2ファイルと非常によく似た機能を持っています(慣れていない人のために説明すると、大きなディスクやファイルを、より小さく「スパースな」方法で保存することを可能にします)。
macOS Tahoeのリリース(2025年後半)の少し前に、ASIFファイルのパーサーを書いてみるのは楽しい練習になるだろうと思いました。それからしばらく経ちましたが、私がこのような問題にどのようにアプローチするか、そのプロセスを振り返って示したいと思いました。ファイルフォーマットのリバースエンジニアリングに不慣れな人でも、何かを学べるかもしれません。そのため、この記事には追加の洞察を伴う「調査ノート」が散りばめられています。
Appleのドキュメントに記載されているコマンドでテストファイルを作成し、テストパターンを書き込んで始めましょう。
調査ノート
テスト目的で、私は通常、コンテンツが「オフセット」と一致することを確認できるテストパターンを作成するのが好きです。この場合、基本的に1MiBのブロックに番号を振ったバイトです。確かに、より良いテストパターンはありますが、ファイルフォーマットの最初の段階では、何かでファイルを埋めることも重要です。予測可能で検証可能なパターンを持つことで、後のステップが容易になります。
```bash
❯ diskutil image create blank --fs none --format ASIF --size 1GiB file file.asif created
❯ diskutil image attach -nomount file.asif /dev/disk4
❯ python3
Python 3.14.0 (main, Oct 7 2025, 09:34:52) [Clang 17.0.0 (clang-1700.3.19.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> fh = open("/dev/disk4", "wb")
>>> for i in range(255):
... fh.write(bytes([i] * 1024 * 1024))
>>> fh.close()
❯ hdiutil detach disk4
"disk4" ejected.
```
ヘックスダンプの目視確認
いつものように、いくつかのヘックスダンプを目視確認することから始め、詳細を把握できるかどうか見てみましょう。
```bash
❯ xxd file.asif | head -5
000102030405060708090A0B0C0D0E0F
00000000 73686477000000010000020000000000 shdw············
00000010 00000000000002000000000000041400 ················
00000020 8af9ead2cf3849c08eec0095cf5c7899 ·····8I······\x·
00000030 00000000001dcd650000080000000000 ·······e········
00000040 001000000200000000000000ffffffff ················
```
すぐに、何らかのファイルマジックに続いて、ビッグエンディアンらしき整数がいくつか見つかります。
調査ノート
ファイルフォーマットをリバースエンジニアリングしていてマジックバイトを見つけたら、オンラインで利用可能な情報を検索するのが常に良いアイデアです。私は通常、マジックの文字列/バイト表現、ビッグエンディアンの16進数、リトルエンディアンの16進数の組み合わせを、様々な検索エンジン(Google、GitHub、VirusTotal Retrohunt)で検索します。
この場合、あまり有用な情報は見つかりませんでした。「エンディアンネス」や整数フィールドを見つけることに関しては、しばらくすると自転車に乗るようなものです。ヒントとしては、まず4バイト(uint32)、次に8バイト(uint64)のチャンクで左から右にスキャンし、次に可能であればより小さなチャンク(uint16またはuint8)に分割して、合理的に見える整数(16進数で丸められたもの、またはファイルのオフセットと他の見つけた値を掛けて相互参照したもの)を解析できるようになるまで続けます。「自然な順序」に見える整数があれば、それはビッグエンディアンです。逆に見える場合は、リトルエンディアンです。
迅速に大まかな構造を書き起こし、整数の幅を推測して、dissect.cstructでさらに詳しく調べてみましょう。
```python
# /// script
# requires-python = ">=3.10"
# dependencies = ["dissect.cstruct"]
# ///
import sys
from dissect.cstruct import cstruct, dumpstruct
asif_def = """
struct header {
char magic[4];
uint32 field4;
uint32 field8;
uint32 fieldC;
uint64 field10;
uint64 field18;
char field20[16];
uint64 field30;
uint64 field38;
uint32 field40;
uint32 field44;
uint32 field48;
uint32 field4C;
};
"""
c_asif = cstruct(asif_def, endian=">")
with open(sys.argv[1], "rb") as fh:
header = c_asif.header(fh)
dumpstruct(header)
```
```
000102030405060708090A0B0C0D0E0F
00000000 73686477000000010000020000000000 shdw············
00000010 00000000000002000000000000041400 ················
00000020 8af9ead2cf3849c08eec0095cf5c7899 ·····8I······\x·
00000030 00000000001dcd650000080000000000 ·······e········
00000040 001000000200000000000000ffffffff ················
header
Field Offset Size Value
magic[4] 0x0000 4 bytes b'shdw'
field4 0x0004 4 bytes 0x1
field8 0x0008 4 bytes 0x200
fieldC 0x000c 4 bytes 0x0
field10 0x0010 8 bytes 0x200
field18 0x0018 8 bytes 0x41400
field20[16] 0x0020 16 bytes b'\x8a\xf9\xea\xd2\xcf8I\xc0\x8e\xec\x00\x95\xcf\\x\x99'
field30 0x0030 8 bytes 0x1dcd65
field38 0x0038 8 bytes 0x80000000000
field40 0x0040 4 bytes 0x100000
field44 0x0044 4 bytes 0x2000000
field48 0x0048 4 bytes 0x0
field4C 0x004c 4 bytes 0xffffffff
```
いくつかの興味深い数字が見られます。すぐに0x200のインスタンスが2つ見つかりますが、これは一般的なセクターサイズ(512バイト)です。いくつかの値を掛けたり割ったりして試行錯誤した結果、field30がセクター数での仮想ディスクサイズ(1GiB // 512 = 0x200000)である可能性も考えられます。field8がセクターサイズだと仮定すると、field10とfield18はファイル内のオフセットかもしれません。
もう少し大きなヘックスダンプを見てみましょう。
```bash
❯ xxd -a file.asif | head -32
000102030405060708090A0B0C0D0E0F
00000000 73686477000000010000020000000000 shdw············
00000010 00000000000002000000000000041400 ················
00000020 8af9ead2cf3849c08eec0095cf5c7899 ·····8I······\x·
00000030 00000000001dcd650000080000000000 ·······e········
00000040 001000000200000000000000ffffffff ················
00000050 00000000000000000000000000000000 ················
*
00000200 00000000000000020000000000000004 ················
00000210 00000000000000000000000000000000 ················
*
00041240 00000000000000000000000000000001 ················
00041250 00000000000000000000000000000000 ················
*
00041400 00000000000000010000000000000000 ················
00041410 00000000000000000000000000000000 ················
*
00082440 00000000000000000000000000000001 ················
00082450 00000000000000000000000000000000 ················
*
00120030 c0000000000000020000000000000003 ················
00120040 00000000000000000000000000000000 ················
*
00200000 6d657461000000010000020000000000 meta············
00200010 00000200000000000000000000000000 ················
00200020 00000000000000000000000000000000 ················
*
00200200 3c3f786d6c2076657273696f6e3d2231 <?xml version="1
00200210 2e302220656e636f64696e673d225554 .0" encoding="UT
00200220 462d38223f3e0a3c21444f4354595045 F-8"?>·<!DOCTYPE
00200230 20706c697374205055424c494320222d plist PUBLIC "-
00200240 2f2f4170706c652f2f44544420504c49 //Apple//DTD PLI
00200250 535420312e302f2f454e222022687474 ST 1.0//EN" "htt
```
さらに明らかになりました。0x00000200と0x00041400にいくつかのデータがあることがわかりますが、まだその意味を完全に理解することはできません。0x00200000には「meta」が見え、その後に0x00200200にXML plistが続いていることもわかります。field30が仮想ディスクサイズであるという私たちの推測は間違っていたかもしれません。変数が多すぎ、明確な構造が少なすぎるため、ヘックスダンプの目視確認だけではこれ以上続けることはできません。そこで、リバースエンジニアリングするバイナリを探してみましょう。
バイナリを探す:
バイナリを探すにはいくつかの方法がありますが、私は特に急いでいなかったので、かなり怠惰なアプローチを取りました。
```bash
❯ grep -r "ASIF" /System/Library/Frameworks /System/Library/PrivateFrameworks
[...]
Binary file /System/Library/PrivateFrameworks/DiskImages2.framework/Versions/A/XPCServices/diskimagescontroller.xpc/Contents/MacOS/diskimagescontroller matches
[...]
```
調査ノート
私はこの目的のためにgrepの代わりにYARAを使うことが多いですが、通常はYARAの方が少し速いです。別の方法として、バイナリファイル内で特定の文字列やバイト列(「ASIF」など)を検索する「ニードルサーチ」を行うこともあります。