BlackHatUSA2026:GCD与XPC竞态检测"

admin 2026-10-16 09:33:16 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

Black Hat USA 2026:GCD与XPC竞态检测

原创

Max Luo
Max Luo

白帽子罗棋琛

2026年9月24日 08:18
中国香港

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

当队列成为漏洞:逆向 GCD、XPC 竞态与 macOS 检测工程

Black Hat USA 2026 议题笔记:When Queues Become Vulnerabilities: Reverse Engineering GCD, XPC Races, and macOS Detection Engineering

macOS 安全审计常把注意力放在签名、Entitlement、Sandbox 和 XPC 客户端身份校验上,却容易忽略另一个决定性条件:同一服务的两个消息处理器,究竟能不能同时运行。

Grand Central Dispatch(GCD)把任务调度抽象成队列,让开发者不必直接管理每一条线程。抽象降低了开发成本,也会隐藏执行顺序。一段代码在单线程测试中始终正确,不代表它在并发队列、QoS 变化、线程池饱和或同步回调下仍然正确。对特权 XPC 服务而言,一次错误的队列配置可能把普通数据竞争放大成权限提升。

图 1:课件从 macOS 竞态条件检测切入 GCD、XPC 和行为检测

这类问题不能只靠搜索危险 API,也不能只靠崩溃告警。有效的工程方法需要同时覆盖静态审查、运行时遥测、并发压力测试和架构约束。

1、进程不是执行单位,线程与队列才决定顺序

进程提供虚拟地址空间、文件描述符和 Mach Port 等共享资源,真正被调度的是线程。GCD 再在应用与线程之间加入一层队列:代码提交的是 Block/Task,系统决定由哪个工作线程执行。

图 2:Dispatch Queue 保存任务,工作线程从队列中取出并执行

这层抽象容易造成三个误判:

  • 创建了两个队列,不等于系统一定创建两条线程;
  • 任务按顺序提交,不等于它们按顺序完成;
  • Handler 写在同一个函数里,不等于同一时刻只有一个实例在运行。

课件用自动售货机说明并发:两个队列可以交错使用同一台机器;并行则是两台机器同时服务。竞态的本质不是“速度快”,而是多个合法操作的交错顺序破坏了状态不变量。

图 3:两个读—改—写序列发生交错,最终结果取决于调度顺序

例如下面的计数器在语法上没有问题,但 value += 1 并非原子操作:

swift

finalclassUnsafeCounter {     private(set)var value =0funcincrement() {         value +=1     } }  let counter =UnsafeCounter() DispatchQueue.concurrentPerform(iterations: 10_000) { _in     counter.increment() } print(counter.value) // 不保证等于 10000

安全审计要寻找的是跨 Handler 共享的状态:全局字典、对象生命周期、临时文件、授权结果缓存、Mach Port、数据库事务和“先检查后使用”的文件路径。只要状态被多个并发入口访问,就必须明确串行化或同步策略。

2、QoS 不只是性能参数,也会改变竞态窗口

GCD 的 Quality of Service(QoS)用于表达任务的重要程度。常见等级包括 userInteractive、userInitiated、utility 和 background,另有 default 与 unspecified。

图 4:不同 QoS 表达 UI、用户触发、长任务和后台任务的调度优先级

很多代码把 QoS 当成“是否省电”的标签。对安全测试而言,它还是操纵交错顺序的变量:

  • 提高某条队列的 QoS,可能让“使用”动作抢在“校验结果写回”之前;
  • 降低持锁线程的 QoS,可能扩大其他线程等待资源的时间;
  • 大量高 QoS 任务会压缩后台清理和状态回收的执行机会;
  • 从调用方继承的 QoS,可能让同一段代码在不同入口表现不同。

创建队列时应显式写出用途和 QoS,不要依赖调用上下文猜测:

swift

let stateQueue =DispatchQueue(     label: \"com.example.agent.state\",     qos: .utility,     attributes: [],     autoreleaseFrequency: .workItem,     target: nil )  let ioQueue =DispatchQueue(     label: \"com.example.agent.io\",     qos: .utility,     attributes: .concurrent )

串行 stateQueue 负责维护状态,ioQueue 负责可并行的无状态 I/O。不要为了“提高吞吐”把所有队列改成 .concurrent;如果共享状态仍需要锁,得到的往往只是更复杂的等待图。

3、优先级反转会把短临界区变成长时间阻塞

优先级反转的典型场景包含三条线程:低优先级线程 L 持有锁,高优先级线程 H 等待同一把锁,中优先级线程 M 又持续抢占 L。结果是最高优先级的 H 被优先级更低的 M 间接延迟。

图 5:L 持锁、H 阻塞、M 抢占 L,导致 H 无法按预期获得资源

pthread_mutex 和 os_unfair_lock 能够跟踪锁所有者,内核在部分情况下可以尝试提升所有者优先级。Semaphore、读写锁或自制同步原语未必具备同样的所有者语义,等待方无法把优先级传递给真正需要推进的线程。

图 6:能够识别所有者的锁可能缓解反转,无所有者语义的原语无法保证这一点

课件对“QoS 继承”的措辞是审慎的:它是可能发生的调度补偿,不应被当成正确性的保证。工程上仍需缩短临界区,禁止在锁内做网络、磁盘、XPC 同步调用和用户回调。

下面这种写法尤其危险:

swift

stateLock.lock() defer { stateLock.unlock() }  let data =tryData(contentsOf: remoteURL) // 锁内阻塞 I/O state.cache[key] = data notifyClient(data)                         // 锁内进入外部代码

更稳妥的结构是先在锁外完成慢操作,再用短临界区提交结果,最后在锁外通知:

swift

let data =tryData(contentsOf: remoteURL)  stateLock.lock() state.cache[key] = data stateLock.unlock()  notifyClient(data)

如果写入前必须确认状态未变化,应引入版本号或不可变快照,而不是把整个 I/O 包进锁里。

4、dispatch_sync 的风险来自等待图,不是 API 本身

同步派发的语义是:调用队列等待目标队列执行完 Block,再继续向下运行。目标队列不同且无环时,这是一种明确的同步点;目标就是当前串行队列时,Block 永远无法开始,形成自等待死锁。

图 7:当前串行队列同步派发给自身时,队列被调用阻塞,目标任务无法启动

最明显的错误是从主线程同步提交到主队列:

swift

funcrefreshUI() {     DispatchQueue.main.sync {         renderStatus()     } }

但真实项目更常见的是间接环:

text

主队列   └─ sync 等待 workerQueue        └─ 调用系统或业务 API             └─ 回调 sync 等待主队列

代码评审只看单个 sync 调用,很难看出这个环。应为每条串行队列建立稳定 Label,并把同步边记录成图。只要新增边会从目标队列重新走回调用队列,就存在死锁条件。

一个简单的封装可以在 Debug/测试构建中阻止队列自同步:

swift

finalclassCheckedSerialQueue {     privatelet queue: DispatchQueueprivatelet key =DispatchSpecificKey<UUID>()     privatelet identity =UUID()      init(label: String, qos: DispatchQoS= .default) {         queue =DispatchQueue(label: label, qos: qos)         queue.setSpecific(key: key, value: identity)     }      funcsync<T>(_work: () throws -> T) rethrows -> T {         precondition(             DispatchQueue.getSpecific(key: key) != identity,             \"attempted synchronous dispatch onto the current serial queue\"         )         returntry queue.sync(execute: work)     } }

这个封装只能识别直接自等待,无法发现跨三条队列的环。真正的修复仍应减少同步跨队列调用,并把回调设计为异步结果或结构化并发。

5、线程池饱和会制造稳定的拒绝服务条件

并发队列不等于无限线程。GCD 使用受系统管理的工作线程池。如果向并发队列提交大量会阻塞的任务,工作线程可能全部卡在信号量、同步 I/O 或另一个队列上,真正负责释放资源的任务反而得不到线程。

图 8:大量阻塞型异步任务占满工作线程,其他组件出现资源饥饿

这种故障与普通高 CPU 不同:CPU 可能并不繁忙,线程数和等待数却快速上升;队列延迟持续增长,服务没有立即崩溃,只表现为 XPC 超时、UI 卡住或守护进程无响应。

不要用“提交更多异步任务”解决背压。入口需要显式限流:

swift

actorRequestGate {     privatelet limit: Intprivatevar active =0init(limit: Int) {         precondition(limit >0)         self.limit = limit     }      functryAcquire() -> Bool {         guard active < limit else { returnfalse }         active +=1returntrue     }      funcrelease() {         precondition(active >0)         active -=1     } }

在 XPC 服务中,超限时应返回明确的忙状态或让客户端退避,而不是无限缓存消息。限额需要按请求成本区分:一个需要解析大型对象、访问 Keychain 或等待网络的请求,不能与一次只读状态查询使用相同并发预算。

6、XPC 目标队列遗漏会让串行假设瞬间失效

历史上的 com.apple.GSSCred 案例说明了队列配置为何是安全边界。CVE-2018-4331 是该特权 XPC 服务中的竞态条件,可导致攻击者在以 root 运行的 GSSCred 进程内执行代码。课件用它说明:预期中的串行消息处理,因为连接没有设置目标队列而落到默认并发队列,多个 Handler 得以同时操作共享状态。

图 9:预期是串行处理,遗漏 Target Queue 后消息被并发执行

这不意味着每个未调用 xpc_connection_set_target_queue() 的服务都可利用。漏洞还取决于 Handler 是否共享可变状态、对象生命周期是否安全、消息能否被低权限客户端并发触发,以及竞态结果是否形成可控的内存破坏或授权绕过。

但对特权服务来说,显式设置队列是必要的可审计约束:

c

staticdispatch_queue_t g_service_queue;  staticvoidconfigure_peer(xpc_connection_t peer) {     xpc_connection_set_target_queue(peer, g_service_queue);     xpc_connection_set_event_handler(peer, ^(xpc_object_t event) {         handle_message(peer, event);     });     xpc_connection_resume(peer); }  intmain(void) {     g_service_queue = dispatch_queue_create(         \"com.example.privileged-xpc.state\",         DISPATCH_QUEUE_SERIAL     );      xpc_main(^(xpc_connection_t peer) {         configure_peer(peer);     }); }

代码还应在接受连接时校验调用方的 Audit Token、签名要求与 Entitlement。串行队列解决的是并发状态问题,不是身份认证;身份校验也不能修复竞态,两者缺一不可。

7、静态审查要寻找“错误假设”,不只是危险函数

课件归纳了四类静态信号:接受 XPC 连接后没有设置目标队列;在并发环境中假设串行执行;未经说明使用 dispatch_sync;全局可变状态缺少锁或串行队列收口。

图 10:目标队列、串并行语义、同步调用和共享状态是重点审查项

可以把规则分成“必须修复”和“必须解释”两类:

yaml

must_fix:-accepted_xpc_connection_without_target_queue-mutable_global_written_from_multiple_handlers-dispatch_sync_to_statically_identical_serial_queue-client_authorization_split_from_privileged_actionmust_justify:-any_dispatch_sync_in_privileged_service-semaphore_wait_on_global_concurrent_queue-blocking_io_inside_lock-concurrent_handler_accessing_reference_counted_shared_object

正则搜索只能用于初筛。例如:

bash

rg -n \\   'xpc_connection_set_event_handler|xpc_connection_set_target_queue|dispatch_sync|\\.sync\\s*\\{|dispatch_semaphore_wait' \\   --glob '*.{c,h,m,mm,swift}' .

审计人员应以“消息入口”为单位向下追踪:连接在哪创建、Target Queue 是什么、Handler 访问了哪些状态、状态保护原语是什么、是否跨队列同步调用、错误路径是否也释放锁或引用。仅统计 dispatch_sync 数量没有安全意义。

8、运行时检测应聚合崩溃、线程抖动和队列积压

竞态未必每次都形成可利用结果。更常见的前兆是偶发崩溃、守护进程重启、线程数尖峰、消息突发和队列排空时间增长。课件建议把这些信号放在一起,而不是只对单次 Crash 告警。

在开发阶段,Xcode 的 Thread Performance Checker 可发现高 QoS 线程等待较低 QoS 线程的情况;Thread Sanitizer 适合发现部分数据竞争,但两者都不是生产检测方案。生产侧应为关键队列埋点:

swift

import OSLog  privatelet log =Logger(subsystem: \"com.example.agent\", category: \"queue\")  funcsubmitTracked(     toqueue: DispatchQueue,     queueName: String,     operation: @escaping@Sendable () -> Void ) {     let submitted =ContinuousClock.now     queue.async {         let delay = submitted.duration(to: .now)         log.info(\"queue_start name=\\(queueName, privacy: .public) delay=\\(String(describing: delay), privacy: .public)\")         operation()     } }

建议采集以下指标:

  • queue_submit_total:按队列和消息类型统计提交量;
  • queue_start_delay_ms:从提交到开始执行的延迟;
  • handler_duration_ms:Handler 执行时间;
  • inflight_handlers:并发中的 Handler 数;
  • thread_count 与 blocked_thread_count;
  • 特权 XPC 服务的 Crash、Jetsam/Watchdog 和重启次数;
  • 客户端、消息类型与拒绝/限流结果。

macOS Unified Log 的现场查询可从进程和时间窗口开始:

bash

log show --last 30m \\   --style json \\   --predicate '(process == \"GSSCred\") OR (eventMessage CONTAINS[c] \"queue_start\")'

不同 macOS 版本的日志可见性与字段会变化,企业采集前要确认隐私设置、日志保留和 Endpoint Security 产品能力。不要在日志中写入 XPC 消息正文、票据或用户秘密。

9、行为模型比单一阈值更能识别并发异常

课件最后把行为信号归纳为:同步程度异常升高、并行度偏离预期、限流行为变化、排空时间变长,以及相对基线的偏移。

图 11:同步、并行度、限流、Drain Time 和基线共同描述队列健康度

单一阈值很容易误报。发布新版本、导入大量证书或设备刚启动时,消息量和线程数都可能合法升高。更实用的是窗口关联:

sql

SELECT     host_id,     process_name,     window_start,     max(thread_count) AS peak_threads,     percentile_cont(0.99) WITHINGROUP (         ORDERBY queue_start_delay_ms     ) AS p99_queue_delay,     sum(process_restart_count) AS restarts,     sum(xpc_request_count) AS requests FROM macos_service_window WHERE window_start >=CURRENT_TIMESTAMP-INTERVAL'1'HOURGROUPBY host_id, process_name, window_start HAVINGmax(thread_count) >2*avg(baseline_thread_count)    ANDpercentile_cont(0.99) WITHINGROUP (        ORDERBY queue_start_delay_ms    ) >5*avg(baseline_queue_delay_ms);

生产实现需要把当前窗口与同版本、同硬件架构、同工作负载的历史基线连接起来,示例中的 avg(baseline_*) 只是表达检测意图。

对特权 XPC 服务,可以提高以下组合的优先级:

text

同一低权限客户端短时间突发 XPC 消息   + 特权服务并发 Handler 数异常   + 线程数或队列延迟升高   + 服务崩溃/重启   + 重启后同一客户端继续发送

这组信号不能证明竞态利用,但足以触发隔离、保全 Crash Report、采集进程树与客户端签名信息。竞态事件最容易在自动重启后丢失上下文,检测系统要把崩溃前后的窗口拼接起来。

10、把并发安全纳入特权服务的交付门禁

课件最终给出的路线很清楚:静态审查、遥测和行为检测需要同时存在。

图 12:GCD 机制、优先级反转、同步死锁、GSSCred 案例和检测策略构成完整链条

对于 macOS 安全代理、系统扩展配套服务、更新器和其他特权 XPC Daemon,可以设置一组可验收的发布门禁:

  1. 每个 XPC Listener 和 Peer Connection 都能追溯到显式 Target Queue;
  2. 每个共享可变状态只有一种同步所有权,不混用多套锁和队列;
  3. 所有 dispatch_sync 都有等待图说明和测试用例;
  4. 锁内不存在网络 I/O、磁盘慢操作、XPC 调用或外部回调;
  5. 并发入口具备容量上限、超时和客户端退避协议;
  6. 压力测试覆盖 QoS 变化、随机延迟、重复消息和并发断连;
  7. Crash、重启、队列延迟、Handler 并行度和限流率可以关联到版本;
  8. 客户端身份校验与状态变更位于同一不可分割的授权边界内。

并发测试不能只重复正常请求。应主动扩大竞态窗口:在校验与使用之间注入随机延迟,改变不同队列 QoS,限制可用 CPU 核数,制造 I/O 抖动,同时以多个连接重复提交相互冲突的消息。测试目标不是证明某一次运行正确,而是证明关键不变量不依赖调度运气。

原始资料与延伸阅读:

  • Black Hat 官方 Session 页面
  • Brandon Azad:CVE-2018-4331 GSSCred 竞态分析
  • Apple Developer:xpc\_connection\_set\_target\_queue

当队列被当作性能工具时,安全问题常被归结为“偶发卡顿”;当它被当作执行顺序和状态所有权的边界时,代码评审才会问对问题:谁能同时进入、谁拥有状态、谁在等待谁,以及攻击者能否稳定操纵这个顺序。

原始会议材料(仓库内)

  • 演讲课件 PDF

开源资料与原始议题 PDF

本文对应的 Markdown 原稿、Black Hat 原始议题 PDF 与配图已整理到 GitHub,可按文章编号查找和下载。

https://github.com/cybermaxluo/black-hat-usa-2026-talks

也可以点击文末“阅读原文”进入仓库。欢迎 Star、提交 Issue 或参与勘误。


BlackHatUSA2026:GCD与XPC竞态检测" 网络安全文章

BlackHatUSA2026:GCD与XPC竞态检测"

本文为BlackHatUSA2026议题笔记,聚焦macOS中GCD与XPC竞态条件检测。核心观点是队列配置错误可放大为权限提升漏洞,而非仅依赖危险API搜索。文章系统分析了GCD抽象、QoS影响、优先级反转、dispatch\\_sync死锁、线程池饱和及XPC目标队列遗漏等风险,并给出静态审查规则与工程缓解建议,强调显式设置串行队列、避免锁内阻塞I/O及建立队列标签审计。
重磅!开源版Jev本地部署教程来了!" 网络安全文章

重磅!开源版Jev本地部署教程来了!"

本文介绍开源模型Laya的本地部署教程。Laya是Jev的开源复刻,采用判别式路线,基于ModernBERT双向编码器,专用于意图识别、路由分流等确定性判定任务,单次前向仅需数毫秒,显存占用4-6GB。文章提供保姆级四步部署流程,涵盖环境配置、权重下载、脚本编写与结果解析,并给出指定模型分支与显存优化方案,实测数据完整,可操作性强。
当\"运维故障\"成为攻击伪装:波兰热电厂事件深度解读" 网络安全文章

当\"运维故障\"成为攻击伪装:波兰热电厂事件深度解读"

波兰热电厂事件揭示关键基础设施风险:攻击者利用私有蜂窝网络作为跳板,从风电场横向移动至热电厂,通过默认凭据控制工业控制器并破坏网络配置以延长恢复时间。攻击潜伏11天,伪装成运维故障,暴露了专网内部缺乏隔离与信任假设的致命缺陷。建议企业重新评估专网架构,实施零信任模型,强化设备身份认证与横向访问控制,并建立异常故障的安全归因机制。
评论:0   参与:  0