一、概述
macOS 上有一样 Windows 没有的元数据:扩展属性(extended attributes)。
它不占数据块,ls -l 里的文件大小也不包含它,却能回答"这个文件是怎么来的"。
其中最要紧的一条是 com.apple.quarantine。浏览器下载文件时会自动打上这个属性,格式固定:
0083;5f56283b;Safari;0123ABCD-0123-ABCD-0123-0123ABCD4567
四段分别是:
| 段 |
内容 |
说明 |
| 1 |
0083 |
旗标值(十六进制),控制 Gatekeeper 的检查行为 |
| 2 |
5f56283b |
下载时间的 Unix 秒,以十六进制编码 |
| 3 |
Safari |
下载该文件的应用名 |
| 4 |
0123ABCD-… |
本次下载的唯一标识 UUID |
第 2 段是最容易写错的一处。
它不是十进制字符串,而是用十六进制编码的 Unix 秒。实测验证:
printf '%d\
' 0x5f56283b
# → 1599551515 → UTC 2020-09-07 12:31:55
把它当十进制读会怎样?多数情况下根本走不到「算错」这一步——5f56283b 里带 f,int() 直接抛 ValueError。更麻烦的是那些恰好只由数字组成的十六进制值:sqlite3 的 CAST('66d94e21' AS INTEGER) 静默返回 66,awk 的 +0 返回 660,都不报错,直接给出 1970 年附近的时间。这才是取证里真正会漏过去的那种错。
报告里写出的下载时间必须来自这一步十六进制解码。
还有一条线索是 Gatekeeper 的绕过痕迹:Quarantine 属性被删掉,往往意味着有人执行过 xattr -d com.apple.quarantine,或者在提示框里选了"打开"绕过检查。
这种"缺失"本身就是痕迹。
二、核心原理
2.1 扩展属性与 xattr 命令
扩展属性(xattr)是 APFS/HFS+ 支持的元数据扩展机制。取证相关的主要有:
| 属性名 |
含义 |
com.apple.quarantine |
★ 下载来源与时间(Gatekeeper 依据) |
com.apple.metadata:* |
Finder 标签、颜色、排序等 |
com.apple.FinderInfo |
Finder 图标位置与文件类型 |
com.apple.provenance |
文件来源链路(较新系统引入) |
com.apple.ResourceFork |
资源分支数据 |
xattr -l <file> # 列出全部扩展属性名与长度
xattr -p com.apple.quarantine <file> # 打印某个属性的值
xattr -w com.apple.quarantine "…" <file> # 写入属性(⚠ 改证据)
xattr -d com.apple.quarantine <file> # 删除单个属性(⚠ 改证据)
xattr -c <file> # 清除全部扩展属性(⚠ 改证据)
xattr -r <file> # 递归(对目录有效)
-w / -d / -c 都是改写操作,作用在原始检材上等同于篡改证据。分析阶段只读;要"试写"就在工作副本上做,并记录副本哈希。
批量导出全机 Quarantine 是 macOS 取证里性价比最高的一步:
# 只读方式:逐文件读取,不改动任何属性
find /mnt/mac-data/Users -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do
v=$(xattr -p com.apple.quarantine "$f" 2>/dev/null) || continue
[ -n "$v" ] && printf '%s\ %s\
' "$f" "$v"
done > /evidence/quarantine-all.tsv
xattr 打印值时保留原样(十六进制字符串)。
顺带说一句,xattr -r 不是"递归读取属性"——它是"递归作用于文件",主要对目录有用。要遍历全机取属性,得靠 find 逐文件调 xattr -p。
2.2 Quarantine 旗标值的含义
第一段 0083 是十六进制,即 0x83。默认值随版本变化(常见的有 0083、0085、0177),各个二进制位控制"是否来自网络""是否已由用户确认"之类的行为。
两条判读规则:(1) 有 Quarantine 属性 = 该文件曾经经过网络下载路径,是"来源为网络"的直接证据;(2) 没有该属性 ≠ 文件不是下载来的(原因见 2.4)。
不要因为旗标值不同就判定"被篡改"——它随系统版本与用户操作变化,差异本身不构成结论。
2.3 Gatekeeper 的三道检查
macOS 10.15 之后 Gatekeeper 是三道叠加:
| 检查 |
作用 |
绕过方式(★ 只讲现象) |
| 代码签名 |
验证开发者身份与完整性 |
伪造/移除签名 |
| 公证(notarization) |
Apple 记录该软件已扫描无恶意 |
使用未公证的开发者工具 |
| Quarantine 触发 |
只有带 Quarantine 的文件才受前两道约束 |
删除 Quarantine 属性 |
codesign -dv --verbose=4 /Applications/Some.app 2>&1 | grep -E 'Identifier|TeamIdentifier|Authority|Timestamp'
codesign --verify --deep --strict --verbose=2 /Applications/Some.app 2>&1 | head -5
spctl --assess --type execute --verbose /Applications/Some.app
# → /Applications/Some.app: accepted
# source=Notarized Developer ID
# → /Applications/Some.app: rejected
# source=no usable signature
实测行为:spctl --assess --type install --verbose /Applications/Safari.app 返回 rejected;spctl --status 输出 assessments enabled。
spctl --assess 对普通文件(如 .txt)返回 rejected / no usable signature 是正常结果,不是异常。真正决定 Gatekeeper 行为的是对应用包(.app)的评估。
2.4 为什么 Quarantine 会"消失"
这一点要格外小心:Quarantine 的缺失,不能直接等同于"文件不是下载的"。
| 原因 |
是否人为 |
说明 |
| 文件由程序生成(非下载) |
否 |
curl/程序写出、cp 本地复制,cp 不复制 xattr |
| 拖入邮件附件后另存 |
否 |
附件被解压/重写,属性不继承 |
| 解压归档 |
否 |
解压工具默认不还原 xattr |
| 用户显式删除 |
★ 是 |
xattr -d com.apple.quarantine |
| 右键"打开"绕过 |
★ 是 |
一次确认后系统清除该文件的 Quarantine |
| 挂载到不保留 xattr 的文件系统 |
否 |
FAT32/exFAT 会丢弃扩展属性 |
复制到 DigiForensics.dd 之外的中转介质 |
否 |
取决于中转格式 |
结论:Quarantine 存在 = 强证据(网络来源 + 下载时间)。Quarantine 缺失 = 弱线索(需要解释,但不能定性)。
2.5 securityd 日志
Gatekeeper 的实际校验动作由 securityd 记录在统一日志中:
/usr/bin/log show --last 30d --info --style json \
--predicate 'subsystem == "com.apple.securityd"' > /evidence/securityd.log
# Gatekeeper 评估与隔离属性处理
/usr/bin/log show --last 7d --info --style compact \
--predicate 'subsystem == "com.apple.security" OR process == "syspolicyd"' | head -60
应重点检索的关键词:
grep -niE 'quarantine|notariz|gatekeeper|assessment|reject' /evidence/securityd.log | head -40
| 日志线索 |
含义 |
Gatekeeper + rejected |
某次 Gatekeeper 校验未通过 |
quarantine attribute |
Quarantine 相关处理记录 |
notarization |
公证状态查询 |
| 同一文件在短时间内多次评估 |
可能存在反复尝试打开 |
采集现实必须交代:统一日志属于运行系统的私有数据存储,只能在现场用 /usr/bin/log show 导出。离线镜像里的 /var/db/diagnostics/ 虽然会随数据卷一起被 dd 进来,但它是二进制索引库,直接解析困难,日志本身也会被时间轮转覆盖。所以 securityd 日志的证据强度取决于"是否现场采集",事后补采往往只能拿到最近几天的内容。详见02。
2.6 .DS_Store 与 AppleDouble:三者常被混淆
这三样都和"文件的外围信息"有关,但作用完全不同:
| 工件 |
实际内容 |
是否含下载来源 |
.DS_Store |
Finder 目录视图状态:文件名、图标位置、显示方式、窗口大小。实测文件头 0000 0001 4275 6431(Bud1 块标识) |
否,不含 URL |
._filename(AppleDouble) |
资源分支与 Finder 信息的伴生文件,用于不支持 xattr 的文件系统 |
否 |
com.apple.quarantine |
下载来源应用 + 下载时间 |
是,本篇重点 |
.DS_Store 的取证价值有限,但不为零:
| 它能证明 |
它不能证明 |
| 某文件曾存在于某目录并被 Finder 浏览 |
文件从哪下载、什么时候下载 |
| 目录的浏览顺序(弱线索) |
文件的创建/修改时间 |
| 某些情况下残留已删除文件的目录项 |
文件当前是否还存在 |
常见错误:把 .DS_Store 当成"下载历史"。它记的是视图状态,与文件来源无关。要证明文件来源只能靠 com.apple.quarantine 与 kMDItemWhereFroms(见04)。
三、操作步骤
3.1 第 1 步:定位带 Quarantine 的文件
EV=/mnt/mac-data/Users/liwei
# 快速抽样:Downloads 是命中率最高的位置
for f in "$EV/Downloads"/*; do
[ -f "$f" ] && xattr -p com.apple.quarantine "$f" 2>/dev/null && echo " ← $f"
done
# 全量导出(只读,不改属性)
find "$EV" -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do
v=$(xattr -p com.apple.quarantine "$f" 2>/dev/null) || continue
[ -n "$v" ] && printf '%s\ %s\
' "$f" "$v"
done > /evidence/quarantine-all.tsv
wc -l /evidence/quarantine-all.tsv
3.2 第 2 步:解码(四段解析)
# 单文件解码
decode_q() {
q=$(xattr -p com.apple.quarantine "$1" 2>/dev/null) || return 1
IFS=';' read -r flag ts app uuid <<< "$q"
printf 'flags=%s time=%s app=%s uuid=%s\
' \
"$flag" "$(date -u -r "$(printf '%d' "0x$ts")" '+%Y-%m-%d %H:%M:%S UTC')" "$app" "$uuid"
}
decode_q ~/Downloads/客户名单.xlsx
# → flags=0083 time=2020-09-07 12:31:55 UTC app=Safari uuid=0123ABCD-0123-ABCD-0123-0123ABCD4567
| 字段 |
提取方式 |
陷阱 |
| 旗标 |
${q%%;*} |
十六进制字符串,不要当十进制解读 |
| 时间 |
${q#*;} 后取到下一个 ;,再 printf '%d' 0x… |
★ 十六进制,不是十进制 |
| 应用名 |
第 3 段 |
可能是 Safari、com.google.Chrome、Safari.Downloads |
| UUID |
第 4 段 |
每次下载唯一,可用于关联同一次下载事件 |
3.3 第 3 步:来源应用分布统计
cut -f2 /evidence/quarantine-all.tsv | cut -d';' -f3 | sort | uniq -c | sort -rn
# 41 Safari
# 12 com.google.Chrome
# 3 Slack
# 1 com.google.Chrome.dn0crl0dhlcpm ← 扩展来源,可见具体扩展 ID
分布统计的取证价值:
| 现象 |
可能含义 |
绝大多数来自 Safari |
常规下载 |
出现 com.google.Chrome.dn0crl0dhlcpm 形态 |
由浏览器扩展发起下载(如云盘、翻译类扩展)——值得深挖 |
| 出现未知应用名 |
可能自研客户端、脚本或恶意程序 |
| 某应用只有 1 条且时间孤立 |
需单独核实 |
3.4 第 4 步:绕过痕迹检测
# 4.1 与 Spotlight 来源交叉(见第 4 篇)
grep -f <(cut -f1 /evidence/quarantine-all.tsv | xargs -d'\
' mdfind -onlyin /Users/liwei 2>/dev/null) \
/evidence/quarantine-all.tsv 2>/dev/null | head
# 4.2 Gatekeeper 全局策略状态
spctl --status # → assessments enabled
spctl --disable-status # → 全局禁用状态(本机实测:当前版本已不支持该开关)
# 4.3 应用评估结果
codesign -dv --verbose=4 /Applications/Target.app 2>&1 | grep -E 'Identifier|TeamIdentifier|Authority'
spctl --assess --type execute --verbose /Applications/Target.app
spctl --add 在现代 macOS 已被移除(实测返回 This operation is no longer supported.)。所以"曾经 spctl --add 过"在现代系统上不会留下可查的痕迹,只能从 spctl --status(assessments enabled 表示未被全局禁用)与 securityd 日志间接判断。不要在报告里写"通过 spctl --add 添加信任"这种在现代系统上无法验证的话。
3.5 第 5 步:验证
| 验证对象 |
方法 |
通过标准 |
| Quarantine 值完整 |
四段都以 ; 分隔 |
段数为 4 |
| 时间解码正确 |
date -u -r 回环验证 |
与文件 mtime 差值可解释 |
| 未改动检材 |
分析前后 shasum -a 256 比对 |
完全一致 |
| 旗标值未被篡改 |
与同批文件对比分布 |
异常值单独说明 |
| 日志与属性印证 |
securityd 日志 + Quarantine |
时间窗口吻合 |
四、常见陷阱
这些陷阱按属性读写 → 字段解码 → 缺失的解释 → 结论表述归组,每条给出:现象、为什么会误判、误判代价、正确做法。
★ 标记表示该操作会导致原始检材永久灭失或不可逆改变,执行前必须停下来确认。
4.1 属性读写
陷阱 1:★ 用 xattr -w / -d / -c 在检材上"试一下"效果
现象:想弄清某个 Quarantine 值的具体含义,在挂载点内对某个文件执行 xattr -w com.apple.quarantine "0083;..." file 或 xattr -d com.apple.quarantine file,或者用 xattr -c 清理"脏"属性。
原因:-w / -d / -c 是改写操作,在原始检材上等同于篡改证据。
动机往往还很合理——"先清掉属性看看文件行为有没有变化",或者"这个属性格式看不懂,写一个标准的进去对比"。
但这会直接销毁证据本身。
影响:不可逆,销毁的正是本主题的核心证据。
Quarantine 属性一旦被 -d 或 -c 清除,该文件的下载来源与下载时间永久消失——这两个值在磁盘上没有任何其他副本。
它们是"文件来自网络、什么时候下的、通过什么应用下的"这三重信息的唯一载体。案件里"竞争对手客户名单的下载时间与来源"整个定案链条都建立在它上面。
核查:分析时只能读。要"试写"就在工作副本上做,并记录副本哈希:
# 只读:逐文件读取,绝不写入
xattr -l <file> # 列出全部属性名与长度
xattr -p com.apple.quarantine <file> # 打印某个属性的值
# 需要试写时:先复制
cp -p <file> /evidence/testfile
xattr -w com.apple.quarantine "0083;..." /evidence/testfile # 只在副本上写
shasum -a 256 <file> /evidence/testfile # 记录两者哈希,证明原文件未变
批量提取用只读循环(2.1 说过,这是 macOS 取证性价比最高的一步):
find /mnt/mac-data/Users -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do
v=$(xattr -p com.apple.quarantine "$f" 2>/dev/null) || continue
[ -n "$v" ] && printf '%s\ %s\
' "$f" "$v"
done > /evidence/quarantine-all.tsv
注意 -r 不是"递归打印所有属性"——它指"递归作用于文件"(对目录用)。想遍历全机取属性,得靠 find 逐文件 xattr -p。
这个误解的典型表现是有人写 xattr -r -p 期望遍历,结果什么都没输出。