Quarantine 与 Gatekeeper

macOS 上有一样 Windows 没有的元数据:扩展属性(extended attributes)。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:Quarantine、com.apple.quarantine、Gatekeeper、xattr、securityd、DS_Store、代码签名 难度:进阶 前置知识:macOS 目录结构(见01)、扩展属性概念

一、概述

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