关键词:iTunes 备份、Manifest.db、PBKDF2、AES-256-CBC、iOS 沙盒、AFC
难度:进阶
前置知识:SQLite 结构、PLIST 格式、AES 与密钥派生概念、libimobiledevice 工具链
相关文章:iOS 系统痕迹与 DFU 恢复、SQLite 基础与结构
一、概述
iOS 取证与 Android 最大的不同在于:苹果把"可获取的数据"按层级严格区分,且每一层都需要不同的凭证。
| 层级 |
手段 |
需要什么 |
覆盖率 |
| 备份层 |
iTunes / Finder 备份 |
设备解锁 + 信任 |
约 70%(短信、通讯录、日历、Safari、部分 App 数据) |
| 逻辑层(AFC) |
媒体与部分文档 |
设备解锁 + 信任 |
仅媒体类 |
| 物理层 |
完整文件系统 |
越狱或开发者镜像 |
近 100% |
实战中绝大多数 iOS 案件停留在备份层。不需要越狱,不需要锁屏密码之外的额外条件,不会对设备造成任何改动。
这不是能力不足,是性价比最高的起点。
一次备份就能拿到短信、通讯录、日历、Safari 历史,还有相当一部分应用数据。更重要的是检材状态不变——需要向委托方交代"我们做了什么"的时候,这一点值不少。
没有这一层会发生什么:直接切物理层,要么越狱(改了设备状态,还得额外授权),要么走 DFU 恢复(见 iOS 系统痕迹与 DFU 恢复),风险更高。
而在备份层就能回答的问题上硬走物理层,付出的是检材完整性风险,收益几乎为零。
在取证流程中的位置:采集环节的主路径,也是 iOS 侧唯一不需要额外条件就能拿到大量数据的入口。
上游是设备状态固定——ideviceinfo 的输出本身要哈希固定,DeviceName 属于个人信息,需脱敏。
下游是文件定位与解包:Manifest.db 索引 → 按哈希取文件 → 还原目录树,再往下是数据库与沙盒内容分析。
它同时是 Keychain 与 Safari 痕迹 的数据来源之一。备份里含 Safari 历史数据库与部分 Keychain 条目。
结构说明:iOS 的 APFS 容器、卷、快照结构与桌面 macOS 高度一致,这部分见 APFS 容器与快照,不重复。
本篇只讲备份层与沙盒层。
本篇要讲清的三件事:备份存了什么、加密备份的密钥怎么来、沙盒怎么提取。
涉及的证据形态(保留真实产品的强制文件名):
| 形态 |
具体位置 |
结构特征 |
| 备份根目录 |
~/Library/Application Support/MobileSync/Backup/<设备 UDID>/ |
目录名即 UDID |
| 核心索引 |
Manifest.db |
SQLite,Files 表存 fileID / domain / relativePath / flags / file BLOB |
| 归档字段 |
Manifest.db 的 file 列 |
NSKeyedArchiver 归档(头 1b 0b 00 00),不是普通 plist,含 Size、时间、Hash、保护类与 per-file key |
| 备份配置 |
Manifest.plist |
二进制 plist;加密备份时含 ManifestKey,另有 ITER 迭代次数 |
| 完整性判据 |
Status.plist 的 IsFullBackup |
为 1 才是全量备份,差量备份的数据不完整 |
| 设备信息 |
Info.plist |
文本 plist,含设备名、型号、版本、已安装应用列表 |
| 实际数据 |
<2hex>/<38hex> 与同名 .plist |
内容本体 + 该文件自己的 Info.plist(未加密备份为明文 XML) |
| 沙盒路径 |
AppDomain-<BundleID>-... |
每个应用独立域,路径含随机化前缀 |
Manifest.db 为什么是整个备份里最关键的一个文件:备份中所有文件都被重命名为 SHA-1 哈希,原文件名一个都不留。Manifest.db 存的就是"哈希 ↔ 原始路径"这张映射表。
没有它,备份目录只是一堆无法解释的随机文件。后面所有操作都建立在它上面。
能回答什么:设备上装过哪些应用、短信与通讯录内容、备份发生在什么时间点、某个应用的数据存在哪些路径。某次数据变动是否发生在备份之后,对比哈希就能判定。
不能回答什么:备份时点之后的事——备份是时间切片,不是持续记录。
未进入备份域的应用数据,各 App 覆盖率不同,得逐个查 domain。已删文件的残留,备份层没有 freelist 概念。Apple 账户云端数据走 iCloud,那是另一套法律程序。
本文不做的事:不越狱,不提供 DFU 绕过或激活锁绕过手段,不编写针对特定 iOS 版本的解密工具。
加密备份口令走委托方提供路径:委托方知道口令就用委托方给的,不知道就明确告知"需要委托方在设备上手动解锁后重新备份"。
这个动作必须由有权解锁的人完成,不是分析方能代替的技术步骤。
读者前提:SQLite 读取能力(本篇的核心索引就是 SQLite,见 SQLite 基础与结构)、Apple plist 与 NSKeyedArchiver 的概念、基本的目录与哈希操作。
建议先把备份当作一棵哈希命名的树来看。
理解"索引 + 内容"这两层之后,后面的定位与解包就都顺了。
二、核心原理
2.1 备份目录结构
~/Library/Application Support/MobileSync/Backup/ (macOS)
└── 00008020-001A2B3C4D5E6F70 ← 目录名 = 设备 UDID
├── Info.plist ← 设备名/型号/版本/上次备份时间/应用列表
├── Status.plist ← IsFullBackup、备份状态
├── Manifest.db ← ★ 核心索引(SQLite)
├── Manifest.plist ← 备份配置;加密备份时含 ManifestKey
├── 3d/ 3d0d7e5fb2ce288813306e4d4636395e047a3d28
└── ... ← 数万至数十万个哈希命名文件
| 文件 |
格式 |
取证价值 |
Info.plist |
文本 plist |
★★★★ 设备识别 + 已安装应用列表 |
Status.plist |
二进制 plist |
★★★ IsFullBackup 判断完整性 |
Manifest.db |
SQLite |
★★★★★ 备份里最关键的一个文件 |
Manifest.plist |
二进制 plist |
★★★★★ 加密备份的 ManifestKey |
<2hex>/<38hex> |
原始或密文 |
★★★★★ 实际数据 |
Manifest.db 为什么是核心:备份里所有文件都被重命名为 SHA-1 哈希,文件名完全不保留原名。它保存"哈希 ↔ 原始路径"的映射。
没有它,备份里就是一堆无法解释的随机文件。
2.2 Manifest.db 的表结构
CREATE TABLE Files (fileID TEXT PRIMARY KEY, domain TEXT,
relativePath TEXT, flags INTEGER, file BLOB);
| 字段 |
含义 |
fileID |
形如 HomeDomain-Library/SMS/sms.db,其 SHA-1 就是磁盘文件名 |
relativePath |
域内相对路径,这是最有用的字段 |
flags |
1=文件,2=目录,4=符号链接 |
file |
NSKeyedArchiver 归档,含 Size、时间、Hash、保护类与包裹的 per-file key |
| domain |
内容 |
HomeDomain |
短信、通讯录、日历、Safari、系统偏好 |
CameraRollDomain |
相机胶卷照片与视频 |
AppDomain-<BundleID> |
单个 App 的沙盒数据 |
MediaDomain / RootDomain |
音乐播客 / 系统根目录配置 |
file 归档内含每个文件自己的元数据,其中 Hash(内容 SHA-1)、ProtectionClass、EncryptionKey 是加密备份解密链的必需材料。
sqlite3 Manifest.db "SELECT file FROM Files WHERE fileID='HomeDomain-Library/SMS/sms.db';" > file_meta.raw
xxd -l 16 file_meta.raw # 1b 0b 00 00 04 0b 08 01 ... ← 归档头,非 XML plist
# 键值在 $objects 数组中,需用归档解析器读取,不是普通 plist
2.3 未加密备份的 per-file Info.plist
未加密备份的每个文件在磁盘上都有两个伴生文件:<2hex>/<38hex> 是内容本体,<2hex>/<38hex>.plist 是该文件自己的 Info.plist,明文 XML。
这是最省事、也最适合做完整性验证的路径,优先走它。
HASH=3d0d7e5fb2ce288813306e4d4636395e047a3d28
plutil -p $BK/${HASH:0:2}/$HASH.plist
# { "Birth" => 2023-04-11T02:19:33Z
# "Hash" => "3d0d7e5fb2ce288813306e4d4636395e047a3d28"
# "LastModified" => 2024-08-31T15:14:02Z
# "RelativePath" => "Library/SMS/sms.db"
# "Size" => 2457600 }
| 价值 |
说明 |
| 完整性验证 |
Hash 是内容 SHA-1,可与实际文件重算比对 |
| 时间线锚点 |
Birth 与 LastModified 是设备侧时间(UTC) |
| 路径还原 |
RelativePath 是设备上的原始路径 |
Last Backup Date 与 LastModified 不是一回事:前者是备份动作的时间,后者是该文件最后被修改的时间,两者常相差数月,不可混用。
2.4 加密备份的密钥结构
备份口令(用户设置,与设备锁屏密码无关)
│ PBKDF2-SHA256( salt, iterations )
▼
Backup Key(32 字节)
│ 解密 Manifest.plist 的 BackupKeyBag
▼
Class Key(按数据保护类:ThisDeviceOnly / AfterFirstUnlock / …)
│ 解密 Manifest.plist 的 ManifestKey
▼
ManifestKey(32 字节,保护 Manifest.db 与元数据)
│ 解密每个文件记录中的 per-file key
▼
Per-file Key(每文件一个 32 字节密钥)
│ AES-256-CBC(每 4096 字节滚动 IV,每段 16 字节 HMAC-SHA1 tag)
▼
明文文件
| 环节 |
参数 |
取证意义 |
| 派生 |
PBKDF2-SHA256 |
迭代次数在 Manifest.plist 的 BackupKeyBag 中读取 |
| 迭代次数 |
早期约 10,000;iOS 10.2 之后约 10,000,000 |
决定口令恢复的可行性与耗时 |
| 内容加密 |
AES-256-CBC |
密钥 32 字节,IV 长度见 IVLength(常见 0x10) |
备份口令与设备锁屏密码完全无关。 即使通过司法程序解锁了设备,备份仍可能是加密的。报告中必须把"设备已解锁"与"备份已解密"作为两件独立的事分别说明。
迭代次数的影响是决定性的:10,000,000 次 PBKDF2-SHA256 使现代硬件上每个候选口令的验证也需要可观计算量。短口令(6 位数字、常见弱口令)仍有机会,长随机口令在合理时间内不可行。 遇到高迭代 + 强口令的备份,应当如实说明不可行并转向其他证据来源,而不是给出含糊结论。
2.5 iOS 沙盒结构
/private/var/mobile/Containers/Data/Application/<UUID>/
├── Documents/ ← 用户可见文档
├── Library/
│ ├── Preferences/<BundleID>.plist ← 应用配置
│ ├── Caches/
│ └── Application Support/ ← 数据库常在这里
└── tmp/
| 事实 |
取证意义 |
| 每个 App 独立沙盒 |
只能逐个分析 |
| 沙盒 UUID 每次重装会变 |
不能当稳定的 App 标识,应通过 BundleID 关联 |
系统数据库在 /private/var/mobile/Library/ |
短信、通话、通讯录、日历等 |
iOS 侧没有 Android 那套 userdata 分区与多用户机制,两条移动取证路径的门槛差异见第 01 篇,便于在预算与时间受限时先做取舍。
三、操作步骤
第 1–2 步:环境准备与设备识别
sudo apt install libimobiledevice-utils usbmuxd ifuse libplist-utils
sudo usbmuxd & # ★ 不跑会报 no device found
idevice_id -l
ideviceinfo > device-info.txt # ★ 提取前先固定设备信息
grep -E 'UniqueDeviceID|ProductType|ProductVersion|DeviceName' device-info.txt
# UniqueDeviceID: 00008020-001A2B3C4D5E6F70
# ProductType: iPhone14,2
# ProductVersion: 16.6.1
# DeviceName: 某某的 iPhone ← 用户自命名,可能是姓名线索
sha256sum device-info.txt
第 3 步:执行备份
mkdir -p /work/ios-backup
# 交互式:会提示备份口令,务必在终端内输入,不写在命令行
idevicebackup2 -i backup /work/ios-backup/ 2>&1 | tee backup.log
| 实务建议 |
理由 |
| 交互式输入口令 |
命令行会进 shell 历史,也会出现在 ps 输出里 |
| 若为空密码,明确记录"未设置" |
空密码加密备份没有 KDF 开销,且是设备使用习惯证据 |
⚠️ 备份不会修改设备数据,这是它相对物理提取的最大优势。但部分 App 可能因"备份触发"更新状态(如云同步标记),应在报告中作为已知限制说明。
第 4–5 步:分析备份结构并查询 Manifest.db
BK=/work/ios-backup/00008020-001A2B3C4D5E6F70
plutil -p $BK/Status.plist | grep IsFullBackup # "IsFullBackup" => 1 ← 全量
if plutil -p $BK/Manifest.plist | grep -q ManifestKey; then
echo "★ 加密备份"
plutil -p $BK/Manifest.plist | grep -E 'ITER|WasPasscodeSet'
# "ITER" => 10000000 ← 10,000,000 次 PBKDF2
fi
sqlite3 $BK/Manifest.db "PRAGMA integrity_check;"
sqlite3 $BK/Manifest.db "
SELECT domain, COUNT(*) n FROM Files WHERE flags=1 GROUP BY domain ORDER BY n DESC LIMIT 5;"
sqlite3 -header $BK/Manifest.db "
SELECT fileID, relativePath FROM Files WHERE relativePath LIKE '%sms.db%';"
关键判断:迭代次数 10,000,000 说明是 iOS 10.2 之后创建的备份。委托方已提供口令,本步无恢复问题——但该字段仍应记录在报告中,它决定了"若口令错误是否还有恢复路径"。
第 6 步:按哈希定位并提取文件
# fileID 的 SHA-1 就是磁盘路径:前 2 字符为目录名
echo -n "HomeDomain-Library/SMS/sms.db" | sha1sum
# 3d0d7e5fb2ce288813306e4d4636395e047a3d28
cp $BK/3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28 ./sms.db
HASH=3d0d7e5fb2ce288813306e4d4636395e047a3d28
plutil -extract Hash raw $BK/${HASH:0:2}/$HASH.plist
sha1sum sms.db # 两者必须一致 ✓
注意增量备份:最新一次 Manifest.db 引用了全部文件,但未被修改的旧文件可能物理上仍存放在历史备份目录中。不能只在最新备份目录里找文件。
第 7 步:解包为可浏览目录树
cat > restore_backup.py <<'PYEOF'
#!/usr/bin/env python3
# 把 iTunes/Finder 备份按 Manifest.db 还原为目录树(仅未加密备份)
# 用法: python3 restore_backup.py <备份目录> <输出目录> Python 3.8+,仅标准库
import hashlib, os, sqlite3, sys
src, dst = sys.argv[1:3]
conn = sqlite3.connect(f"file:{os.path.join(src, 'Manifest.db')}?mode=ro", uri=True)
n = ok = miss = 0
for domain, relpath in conn.execute(
"SELECT domain, relativePath FROM Files WHERE flags=1"):
n += 1
# 磁盘文件名 = fileID 的 SHA-1,前 2 字符为子目录
h = hashlib.sha1(f"{domain}-{relpath}".encode()).hexdigest()
f = os.path.join(src, h[:2], h)
if not os.path.exists(f):
miss += 1 # 增量备份:文件可能在历史备份目录中
continue
out = os.path.join(dst, domain, relpath)
os.makedirs(os.path.dirname(out), exist_ok=True)
with open(f, "rb") as fi, open(out, "wb") as fo:
fo.write(fi.read())
ok += 1
print(f"共 {n} 个文件,还原 {ok} 个,缺失 {miss} 个(增量备份时属正常)")
PYEOF
python3 restore_backup.py $BK /work/restored/
find /work/restored -type f | wc -l
⚠️ 仅适用于未加密备份。 加密备份的 Manifest.db 本身也是密文,顺序是"先解密 Manifest.db,再用它定位其他文件"。
第 8 步:沙盒与 AFC 提取
# AFC(App File Container):通过 usbmuxd 访问的媒体/文档通道
ifuse --list-documents # 列出 AFC 可见的文档目录
afcclient ls /DCIM/100APPLE | head
mkdir -p /mnt/ios-afc && ifuse /mnt/ios-afc && ls /mnt/ios-afc
fusermount -u /mnt/ios-afc # ★ 用完务必显式卸载
| AFC 能取 |
AFC 不能取 |
DCIM/、PhotoData/ |
/private/var/mobile/Library/ 下的系统数据库 |
Download/、部分 Documents/ |
App 的 SQLite(除非应用把库放在 Documents) |
| 部分应用的媒体目录 |
Keychain、沙盒内的配置与缓存 |
AFC 通道 ≠ App 沙盒提取。 AFC 在 iOS 侧是按目录白名单开放的,它不提供遍历 /private/var/mobile/Containers/Data/Application/ 的能力。凡声称"用 AFC 就能拿到任意 App 沙盒"的结论都是错误的。
要取沙盒内的 SQLite 库,实际可行的路径有四条:
| 路径 |
前提 |
证据强度 |
| ① 加密备份 |
该 App 参与备份、且有备份口令 |
最强(可逐文件校验) |
| ② 越狱设备 + 沙盒工具 |
设备已越狱,须委托方授权 |
强 |
| ③ 开发者镜像 |
设备支持、应用为本方开发 |
中 |
| ④ 物理/内存镜像 |
有文件系统级检材 |
视解密情况 |
# 越狱设备上的沙盒提取(示例,能力与支持范围以实际环境为准)
frida-ios-dump -H <host> -U -F # dump 指定 bundle id 的沙盒
frida-ios-dump 的两条硬前提:① 越狱设备,或已为该 App 挂载开发者磁盘镜像(仅对可配证书的自家 App 可行);② 必须逐个指定 bundle id,无法"一把梭"导出所有 App。
它的实现原理是在 App 进程内枚举文件并复制,因此对正在运行且被系统保护的 App 可能失败——失败就如实记为"未获取",不可推断为"无数据"。
四、常见陷阱
4.1 备份凭证与整体判断
陷阱 1:把备份口令当成设备锁屏密码
现象:委托方提供了锁屏密码,分析师直接用它作为备份口令尝试解密;失败后报告写"口令错误,备份无法解密"。
为什么会误判:备份口令与设备锁屏密码在 iOS 中是完全无关的两套凭证。
前者是用户在设置 iTunes/Finder 备份时自己设定的字符串,可以与锁屏密码毫无关系;后者由用户单独设置,与设备加密密钥体系也是两条链路。
误判的来源是"移动取证 = 拿到设备密码 = 拿到一切"这个直觉。这个直觉在 iOS 上是错的——即使通过司法程序解锁了设备,备份仍可能是加密的。
误判代价:把一个可解的检材直接判定为不可解,并终止了后续取证工作。
更实际的后果是,这个"口令错误"的结论会写进报告。委托方据此以为还能提供别的密码,而实际问题在于,他们提供的从来就不是需要的东西。
正确做法:"设备已解锁"与"备份已解密"必须作为两件独立的事分别记录,并确认口令来源的合法性。
# 先判断备份是否加密、是否需要口令
BK=/work/ios-backup/00008020-001A2B3C4D5E6F70
if plutil -p $BK/Manifest.plist | grep -q ManifestKey; then
echo "★ 加密备份,需备份口令"
plutil -p $BK/Manifest.plist | grep -E 'ITER|WasPasscodeSet'
fi
报告中的状态表述应该分三句写,不要合并:
"设备已于 <时间> 由委托方解锁并信任分析机;备份口令由委托方于 <时间> 提供,来源与合法性已确认;备份加密状态:<加密/未加密>。"
合并成一句"设备已解锁,数据可提取",后两个信息就都丢了。
陷阱 2:★ 在命令行传入备份口令
现象:为了脚本化,把口令写进命令行执行备份,事后在 shell 历史或进程列表中发现了它的痕迹。
为什么会误判:命令行参数在三个地方同时暴露——shell 历史文件、同一用户下其他进程的 ps 输出、以及部分审计日志。
无人值守、便于放进流水线,这样