解剖苹果的稀疏映像格式(ASIF)
摘要
一篇技术博文解剖了苹果在macOS Tahoe中用于虚拟磁盘的新稀疏映像格式(ASIF),涵盖了从十六进制转储中逆向工程该文件格式的过程。
暂无内容
查看缓存全文
缓存时间: 2026/06/29 08:02
# 剖析苹果稀疏镜像格式(ASIF)| schamper.dev 来源:https://schamper.dev/dissecting-apples-sparse-image-format-asif/ 2026-06-18·18 分钟阅读· 在 WWDC 2025 上,苹果发布了 macOS 26 Tahoe。macOS Tahoe 的一项新特性是一种新的磁盘镜像格式:ASIF。该格式专为虚拟机设计(其文档位于 Virtualization 框架下),ASIF 从现有的虚拟磁盘格式中汲取了大量灵感。实际上,这意味着它是另一种稀疏虚拟磁盘格式,其功能与稀疏的 VMDK、VHDX 或 QCOW2 文件非常相似(对于不熟悉的人来说,它允许你以更小、更“稀疏”的方式存储大型磁盘或文件)。在 macOS Tahoe 发布前不久(2025 年末),我想尝试为 ASIF 文件编写一个解析器,这会是个有趣的练习。自那以后已经过了一段时间,但我还是想回过头来展示一下我处理这类问题的过程。或许某个对逆向工程文件格式不熟悉的人能从中有所收获。因此,你会在本文中看到偶尔穿插的“研究笔记”,其中包含一些额外的见解。让我们用苹果文档中列出的命令创建一个测试文件,向其中写入一个测试模式,然后开始吧:
研究笔记
> 出于测试目的,我通常喜欢编写一个测试模式,以便验证内容是否与“偏移量”匹配。在这个例子中,基本上就是编号为 1 MiB 的字节块。肯定有更好的测试模式,但在初步窥探文件格式时,用任何内容填满文件也很重要。拥有一个可预测且可验证的模式会使后续步骤更容易。
```
❯ 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.
```
## 用肉眼查看十六进制转储
像往常一样,我们先凭借肉眼查看一些十六进制转储,看能否发现一些细节。
```
❯ xxd file.asif | head -5
```
```
00000000 73686477000000010000020000000000 shdw············
00000010 00000000000002000000000000041400 ················
00000020 8af9ead2cf3849c08eec0095cf5c7899 ·····8I······\x·
00000030 00000000001dcd650000080000000000 ·······e········
00000040 001000000200000000000000ffffffff ················
```
我们立即发现某种文件魔数,后面跟着一些看起来像是大端序的整数。
研究笔记
> 每当你逆向工程一个文件格式并看到一些魔数字节时,最好在网上搜索任何可用的信息。我通常会在各种搜索引擎(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)
```
```
00000000 73686477000000010000020000000000 shdw············
00000010 00000000000002000000000000041400 ················
00000020 8af9ead2cf3849c08eec0095cf5c7899 ·····8I······\x·
00000030 00000000001dcd650000080000000000 ·······e········
00000040 001000000200000000000000ffffffff ················
header
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` 的实例,这是常见的扇区大小(512 字节)。通过尝试乘除不同的值,我们还可以推算出 `field30` 可能是虚拟磁盘的扇区数(`1GiB // 512 = 0x200000`)。如果我们暂时假设 `field8` 是扇区大小,那么 `field10` 和 `field18` 可能是文件中的偏移量。让我们看看更大的十六进制转储:
```
❯ xxd -a file.asif | head -32
```
```
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 <!DOCTYPE
00200230 20706c697374205055424c494320222d plistPUBLIC"-
00200240 2f2f4170706c652f2f44544420504c49 //Apple//DTDPLI
00200250 535420312e302f2f454e222022687474 ST1.0//EN""htt
```
信息量更大了。我们在 `0x00000200` 和 `0x00041400` 处发现了一些数据,但目前还无法完全理解。在 `0x00200000` 处我们还能看到 `meta`,随后在 `0x00200200` 处是一个 XML plist。也许我们对 `field30` 是虚拟磁盘大小的判断错了?变量有点多,又没有足够清晰的结构让继续仅凭肉眼查看十六进制转储变得可行,所以我们来寻找二进制文件进行逆向。
## 寻找我们的二进制文件
有几种方法可以查找我们的二进制文件,但既然我不赶时间,就采用了一种相当懒散的方式:
```
❯ 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
[...]
```
研究笔记
> 我经常使用 YARA 来代替 grep 进行此类操作,通常速度更快,但眼下这样做也足够了。我有时采用的另一种方法是,在原始磁盘(通常是虚拟机的虚拟磁盘)上执行针尖搜索,然后反向查找,看看哪些文件对应于我在磁盘上匹配到的偏移量。虽然这种方法对于当前任务来说有些过度,但在其他情况下很实用。
在匹配结果中,`diskimagescontroller` 最为突出,在其上运行 `strings` 命令发现 *大量* 与 ASIF 相关的内容,包括非常有价值的日志消息、修饰后的函数签名和类型名称!
```
❯ strings diskimagescontroller | grep -i "asif"
[...]
N7di_asif7details3dirE
N7di_asif7details8dir_baseE
[...]
Invalid value for asif header field: %s
Size cannot exceed max ASIF size
Unexpected ASIF header length (
[...]
asif_header
Couldn't read asif's header
[...]
```
让我们把这个小家伙丢进 IDA 里!逆向工程的第一步通常是查找对有趣字符串的引用,这次也不例外。对于一种文件格式,首先应该专注于如何解释文件头,然后再进一步。我们已经在 `strings` 命令输出中看到了一些与“asif's header”相关的有趣字符串,所以让我们看看那些引用这些字符串的函数。
研究笔记
> 在我做这项研究的时候,我曾在网上搜索遇到的某些符号名称。我发现 GitHub 上有一些项目,人们会对 iOS 固件更新进行差异分析,而且同一二进制文件的 iOS 版本 *可能* 包含更多的符号/字符串。我最终对这款二进制文件的 iOS 版本进行了逆向工程,但也不完全是出于这个原因。如果同一个二进制文件既有 x86 版本也有 ARM 版本,我喜欢同时打开它们。其中一个版本的反编译器输出有时会比另一个版本清晰得多。我没有什么实际数据来支持这一点,但我的直觉是,这是由于两个架构的编译器以及反编译器优化水平不同所致。不管怎样,我记得当时使用的 IDA 版本在 x86 版本的二进制文件上会随机崩溃,所以我大部分时间都在注释 iOS 版本,尽管我不认为两者之间实际上有什么有意义的差异。好了,我跑题了,让我们继续吧!
图 1:这看起来像是解析了一个 ASIF 文件头
图 1:这看起来像是解析了一个 ASIF 文件头
我们很快就找到了一个看起来是从原始 ASIF 头(`a2`)中解析出值并存储到内存结构(`a1`)中的函数,见图 1。在这个函数稍靠下的位置(未在图中显示),有一些针对这些字段的简单检查,并附有有用的错误/日志消息(例如“ASIF max_write size in header exceed the limit”、“Sector count is too large”等)。我们立即学到了很多:
- 头中每个字段的偏移量和字节长度;
- 其中一些字段的名称或用途。
如果我们更新之前的头定义,再添加一点点“画猫点睛”的功夫,最终会得到类似这样的结果:
```c
struct asif_header {
uint32 header_signature;
uint32 header_version;
uint32 header_size;
uint32 header_flags;
uint64 directory_offsets[2];
char guid[16];
uint64 sector_count;
uint64 max_sector_count;
uint32 chunk_size;
uint16 block_size;
uint16 total_segments;
uint64 metadata_chunk;
char unk_50[16];
uint32 read_only_flags;
uint32 metadata_flags;
uint32 metadata_read_only_flags;
};
```
```
00000000 73686477000000010000020000000000 shdw············
00000010 00000000000002000000000000041400 ················
00000020 8af9ead2cf3849c08eec0095cf5c7899 ·····8I······\x·
00000030 00000000001dcd650000080000000000 ·······e········
00000040 001000000200000000000000ffffffff ················
00000050 00000000000000000000000000000000 ················
00000060 00000000000000000000000000000000 ················
asif_header
header_signature 0x0000 4 bytes 0x73686477
header_version 0x0004 4 bytes 0x1
header_size 0x0008 4 bytes 0x200
header_flags 0x000c 4 bytes 0x0
directory_offsets[2] 0x0010 16 bytes [512, 267264]
guid[16] 0x0020 16 bytes b'\x8a\xf9\xea\xd2\xcf8I\xc0\x8e\xec\x00\x95\xcf\\x\x99'
sector_count 0x0030 8 bytes 0x1dcd65
max_sector_count 0x0038 8 bytes 0x80000000000
chunk_size 0x0040 4 bytes 0x100000
block_size 0x0044 2 bytes 0x200
total_segments 0x0046 2 bytes 0x0
metadata_chunk 0x0048 8 bytes 0xffffffff
unk_50[16] 0x0050 16 bytes b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
read_only_flags 0x0060 4 bytes 0x0
metadata_flags 0x0064 4 bytes 0x0
metadata_read_only_flags 0x0068 4 bytes 0x0
```
看来我们之前猜测的几个字段是正确的!
## 让它变得合理
如果你在想 *“这个‘画猫点睛’的功夫也太大方了吧”*,那你的想法没错。我略过了一些细节,但我是怎么得出这些结论的其实并不那么有趣。在这个阶段,对 `diskimagescontroller` 中各种函数进行逆向工程的过程本身就有很高的“点睛”成分。这是一个相对容易逆向的二进制文件,因此逆向 ASIF 也相对简单。唯一“烦人”的部分是大量的动态分发,所以需要处理一些继承和虚函数表,但大部分情况下它们并不复杂。这些内容不太适合作为博客素材(而且我写这篇文章的时候距离最初逆向工程已经过了 *很久*,所以大部分细节我都记不清了,哎呀)。
好——吧,以下是一些重要的点:
- 头中有一个字段 `directory_offsets`,它是一个字节偏移量,指向一种分配目录;
- 每个目录以一个 `uint64` 版本号开头,版本最高的目录是当前活动目录。这允许原子更新。
- 每个目录有一个表列表,每个表有一个 `uint64`“数据条目”列表。
- 数据条目按一定比率分组,每组后面跟着一个“映射条目”,该条目是前面各组的位图。
- 每个数据条目指向 ASIF 文件中的一个数据块。
- 块大小在头中定义,通常为 1 MiB。
- 数据条目有 55 位用于块编号,9 位保留给标志位。
- 使用默认块大小(我认为目前尚不可配置)时,最大虚拟磁盘大小似乎略低于 4 PiB,末尾有一小部分保留给元数据。
- 虚拟磁盘的实际大小在头中定义,同时还有磁盘可以增长到的最大大小。
- 头中包含一个元数据块的偏移量,该偏移量通常是 `(4 PiB - 1 个块)`,意味着它位于保留区域内。
- 元数据块包含一个小型头部和一个 plist,该 plist 应包含一个 `internal metadata` 和 `user metadata` 字典。
数据条目的位格式如下:
```
0b00000000 01111111 [...] 11111111 11111111 (块编号)
0b00111111 10000000 [...] 00000000 00000000 (保留)
0b11000000 00000000 [...] 00000000 00000000 (标志)
```
当前已知的标志位(根据二进制文件中的字符串推导得出):
```
0b00 (未初始化)
0b01 (完全初始化)
0b10 (未映射)
0b11 (包含位图)
```
如果你曾经研究过其他稀疏磁盘格式(VMDK、VHDX、QCOW2)的工作原理,你会发现 ASIF 是一种相当优雅且简单的格式。诚然,它没有所有相同的功能,但至少基本的东西很容易理解。
将所有信息添加到我们的实现中是一件相当平淡无奇的事情。从头中读取一些字段,验证它们,跳转到某些偏移量,一切就绪了。不过,我们忽略了一个重要细节。我们还没有涵盖如何从虚拟磁盘内的任意偏移量映射到 ASIF 文件中的块。
研究笔记
> 快速回顾一下虚拟磁盘格式,如 ASIF 以及我目前提到的所有其他四个字母的缩写。它们都基于一个简单的原理:
相似文章
我在 Apple 的 fsck_hfs 中发现了一个 Bug
macOS Sequoia 上 Apple 的 fsck_hfs 工具存在一个 Bug,在配备 8GB RAM 的机器上,对大型 HFS+ 卷(24TB 以上)进行检测时会误报损坏错误,而文件系统本身并无问题。
ARM64 指令的真正编码方式
一篇科普文章,解释 ARM64 (AArch64) 指令如何以 32 位固定长度字编码,破除常见误解,并通过 Apple Silicon 上的 ADD immediate 指令提供动手解码示例。
F* 文件系统——直接绕过操作系统内核读取SSD的文件搜索
一款名为 ffs 的 CLI 工具,通过直接读取磁盘来搜索文件,绕过操作系统内核的 VFS 层,在处理大型、未缓存目录时相比 ripgrep 等工具具有潜在的速度优势。支持 ext4、btrfs 和 APFS 文件系统。
@timsneath:我怀疑 WWDC 上宣布的我个人最喜欢的功能之一将是一匹黑马:容器机器,让你的 Mac 运行一个轻量级、持久的 Linux 环境,并自动挂载你的主目录和仓库:https://t.co/dOBdfOOVxC
Apple 已开源 'container' 工具,用于在 Apple 芯片的 Mac 上以轻量级虚拟机方式运行 Linux 容器,支持 OCI 镜像,并为 macOS 26 进行了优化。
WWDC 2026 附加直播博客:与 Craig Federighi 的技术对话
The Verge 正在直播与苹果高级副总裁 Craig Federighi 的技术深度探讨,内容聚焦于 WWDC 2026 上支持 Apple Intelligence 能力的新架构。