消灭 iOS 原生录音提示音:剖析 AirBeep 的 LocalDevVPN 回环总线、AirTraffic 沙盒逃逸与安全恢复机制

消灭 iOS 原生录音提示音:剖析 AirBeep 的 LocalDevVPN 回环总线、AirTraffic 沙盒逃逸与安全恢复机制

苹果在 iOS 18 中正式引入了期盼已久的通话录音功能,但为了兼顾部分地区的隐私法规,系统在启动和结束录音时,会强制向通话双方播放一段极其突兀的提示音(“此通话正在被录音” / Audible Beep)。在日常商务沟通、采访记录或需要安静留痕的场景下,这声无法关闭的官方播报往往会给通话双方带来不必要的心理负担与尴尬。

针对这一核心痛点,开源社区开发者 LinkPass888 近日推出了专注于无越狱消除提示音的开源工具 AirBeep。它无需用户对 iOS 进行内核越狱或破坏系统签名完整性,仅通过纯端侧侧载的普通 IPA,巧妙利用 LocalDevVPN 建立本地网络回环,结合 Airlift 底层的 AirTraffic 沙盒逃逸漏洞链,成功实现了对 iOS 系统内部 /var/mobile/Library/CallServices/Greetings/default/ 核心音频资源的原生替换。我第一时间对其 Swift 前端与 Rust 底层 FFI 补丁进行了全流程逆向与源码审计,梳理出了这套集网络隧道穿透、心跳保活、容错读写与系统自愈于一体的精妙工程实现。


一、 核心解剖:iOS CallServices 录音提示音的物理存放与播放机制

要消灭通话录音提示音,首先必须弄清苹果官方到底是在哪一层、以何种格式触发这段音频的。

在 iOS 电话子系统(Telephony & CallServices)中,负责处理通话中各种服务播报(如来电转接提示、语音信箱问候、通话录音声明等)的守护进程是 callservicesd。当用户在通话界面点击录音按钮时,callservicesd 会通过内部状态机协同 CoreAudio 引擎,从用户的移动资产配置目录中读取特定的语音素材,并强行混入上行与下行音频总线(Audio Bus):

/var/mobile/Library/CallServices/Greetings/default/
├── StartDisclosureWithTone.m4a   # 开启录音时播放的复合提示音(包含短促 Beep 与语音声明)
├── StartDisclosure.caf           # 部分系统版本专用的备用开启短音(Core Audio Format)
└── StopDisclosure.caf            # 结束或挂断录音时播放的停止提示音
flowchart TD
    subgraph iOS_CallServices_Architecture ["iOS 通话录音音频管线(Native Audio Pipeline)"]
        UserAction["用户在拨号界面点击录音"] --> CallServicesD["callservicesd 守护进程"]
        CallServicesD --> StateMachine["录音前置状态机协商"]
        StateMachine --> AudioMixer["CoreAudio 上下行音频混音总线"]
        
        GreetingsDir["系统资源目录<br/>/var/mobile/Library/CallServices/Greetings/default/"]
        GreetingsDir --> Tone1["StartDisclosureWithTone.m4a (开启音频)"]
        GreetingsDir --> Tone2["StartDisclosure.caf (开启短音)"]
        GreetingsDir --> Tone3["StopDisclosure.caf (结束音频)"]
        
        Tone1 & Tone2 & Tone3 --> AudioMixer
        AudioMixer --> BothParties["强制混入麦克风与扬声器 (双方均可清晰听见)"]
    end

    style iOS_CallServices_Architecture fill:#1e1e2e,stroke:#89b4fa,stroke-width:1.5px
    style GreetingsDir fill:#313244,stroke:#f9e2af,stroke-width:1px
    style BothParties fill:#45475a,stroke:#f38ba8,stroke-width:1.5px

很多初学者容易产生一个误区:既然要静音,直接把这几个文件删掉或者写入 0 字节的空文件不行吗?

答案是绝对不行。callservicesd 在拉起音频流前,会对目标音频文件的格式头、采样率(通常为 44.1kHz 或 48kHz 单声道/双声道 AAC 或 LPCM)以及文件有效时长进行完整性校验。如果文件不存在或文件损坏,状态机可能会抛出解码异常导致通话录音直接失败,甚至引发 callservicesd 守护进程瞬时崩溃。

AirBeep 采纳了此前 AirliftSilence 项目的研究成果:采用严格等长、格式完全对齐的“绝对静音(Zero-amplitude)音频文件”进行原位无损替换。

  • AirBeepStartSilence.m4a:严格按照苹果原版 StartDisclosureWithTone.m4a 的声道参数压制的静音文件;
  • AirBeepStartSilence.caf 与 AirBeepStopSilence.caf:完全保持 CAF 容器格式的静音流。

通过这种“李代桃僵”的方式,系统音频引擎照常加载、解码并混音,但在声学层输出的振幅全部为 0,从而神不知鬼不觉地实现了彻底静音。


二、 破防沙盒:LocalDevVPN 回环总线与 Lockdown 心跳协议

在未越狱的环境下,普通第三方 App 运行在高度受限的沙盒(App Sandbox)中,其读写权限被严格隔离在自身的容器目录(Data/Application/<UUID>/)内,根本不可能通过 FileManager 触碰到 /var/mobile/Library/ 等系统级路径。

AirBeep 是如何打破这道天堑的?它的底层架构建立在由安全研究员 0xjohnny 开源的 Airlift 漏洞利用基座之上。

flowchart LR
    subgraph AirBeep_Frontend ["AirBeep 纯端侧客户端 (SwiftUI)"]
        UI["用户操作界面"] --> Service["CallRecordingToneService"]
        Service --> Transport["AirliftToneTransport (Actor FFI)"]
    end

    subgraph Network_Loopback ["本地网络回环 (Loopback Subnet)"]
        VPN["LocalDevVPN 虚拟网卡 (utun)<br/>10.7.0.1"]
        LockdownPort["lockdownd 监听端口<br/>10.7.0.1:62078"]
        HeartbeatClient["Heartbeat 保活长连接<br/>com.apple.mobile.heartbeat"]
    end

    subgraph Exploitation_Core ["Airlift Rust Core (沙盒逃逸总线)"]
        RustLib["AirliftFFI 原生静态库"]
        ATC["atc (AirTraffic 守护进程)"]
        ZipStream["com.apple.streaming_zip_conduit"]
        TargetFS["/var/mobile/Library/CallServices/Greetings/default/"]
    end

    UI --> Service
    Service --> Transport
    Transport --> RustLib
    RustLib --> VPN
    VPN --> LockdownPort
    LockdownPort --> HeartbeatClient
    RustLib --> ATC & ZipStream
    ATC & ZipStream --> TargetFS

    style AirBeep_Frontend fill:#181825,stroke:#89b4fa,stroke-width:1.5px
    style Network_Loopback fill:#1e1e2e,stroke:#a6e3a1,stroke-width:1.5px
    style Exploitation_Core fill:#313244,stroke:#fab387,stroke-width:1.5px

1. 为什么需要 LocalDevVPN?

在 iOS 系统架构中,苹果官方的调试守护进程 lockdownd(运行在 62078 端口)拥有管理设备文件传输、安装应用和调用开发者服务的极高权限。在通常情况下,lockdownd 只接受通过 USB 数据线连接的 Mac/PC 发起的连接。

iOS 沙盒机制严密限制了 App 之间对 127.0.0.1 环回接口的随意探测。为了让 AirBeep 能够与本地的 lockdownd 建立会话,用户需要在设备上安装并启动 LocalDevVPN。该应用通过 iOS 的 Network Extension 框架在设备内部创建了一个虚拟 VPN 接口(如 utun4,通常分配设备 IP 10.7.0.1)。

这样一来,AirBeep 发出的网络数据包通过虚拟 VPN 路由,成功将流量“回环”注入到宿主机的 lockdownd 端口,打通了沙盒内外的通信桥梁。

2. 跨版本配对凭证认证:从无线 RPPairing 到离线证书导入

仅仅连通端口还不够,lockdownd 要求所有会话必须通过非对称密钥对(Pairing Record)的双向签名认证:

  • iOS 27 及更高版本:苹果在系统内部深度整合了无线配对服务(Remote Pairing / RPPairing)。AirBeep 内置的 PairingController.swift 会通过 Bonjour(mDNS)向局域网广播自己为一个名为 AirBeep、型号伪装成 Mac17,7 的受信任主机,并自动生成 6 位配对 PIN 码。用户只需进入系统的“设置 › 隐私与安全性 › 开发者模式 › 与 AirBeep 配对”,输入 PIN 即可在手机本地生成合法的 aircard_pairing.plist;
  • iOS 18.0 至 iOS 26.x:由于苹果尚未在旧版本中向端侧开放这一配对 UI,AirBeep 在 v2.5.1 中重构了适配逻辑:引导用户在电脑端使用开源的 iloader 或 idevice_pair 生成配对文件,并通过 iOS 系统的 UIDocumentPickerViewController 一键导入至应用沙盒,一举将支持门槛从原本的 iOS 27 垂直下探至 iOS 18.0。

3. 攻克偶发断连:Rust 底层 Lockdown Heartbeat 心跳保活

在早期的测试版本中,很多用户反映静音替换频繁报错:“连接意外中断”或“服务已关闭”。

LinkPass888 与贡献者经过深层排查发现:苹果从 iOS 26 起对 lockdownd 会话施加了更加严苛的活跃度超时限制。如果一个网络会话在建立后没有启动苹果的心跳服务,系统会在 10~15 秒内强行重置 TCP 连接。

为此,AirBeep 针对 rust-core 依赖打上了专门的心跳补丁(scripts/airlift-read-file.patch),在建立隧道的同时并行拉起 com.apple.mobile.heartbeat 服务客户端,开启持续的心跳应答循环:

// scripts/airlift-read-file.patch
impl AppDeviceTunnel {
    async fn new_lockdown(
        provider: TcpProvider,
        pairing_file: PairingFile,
        legacy: bool,
        logger: &Logger,
    ) -> Result<Self, String> {
        let stream = connect_service_lockdown(
            &provider, &pairing_file, legacy, "com.apple.mobile.heartbeat", logger,
        ).await?;
        let mut heartbeat = HeartbeatClient::new(Idevice::new(stream, "Airlift"));
        let mut interval = heartbeat.get_marco(10).await
            .map_err(|e| format!("Heartbeat initial Marco failed: {e:?}"))?;
        heartbeat.send_polo().await
            .map_err(|e| format!("Heartbeat initial Polo failed: {e:?}"))?;
        logger.log("airlift: Lockdown heartbeat established");
        let task = tokio::spawn(async move {
            loop {
                // 周期性接收 Marco 挑战并立即回送 Polo 响应
                match heartbeat.get_marco(interval.saturating_add(5)).await {
                    Ok(next_interval) => interval = next_interval,
                    Err(error) => {
                        tracing::warn!("Lockdown heartbeat ended: {error:?}");
                        break;
                    }
                }
                if let Err(error) = heartbeat.send_polo().await {
                    tracing::warn!("Lockdown heartbeat reply failed: {error:?}");
                    break;
                }
            }
        });
        Ok(Self::Lockdown {
            provider, pairing_file, legacy, _heartbeat: LockdownHeartbeat(task),
        })
    }
}

通过这套基于 Tokio 异步任务的 Marco / Polo 心跳维持机制,只要 AirBeep 的操作尚未完成,底层隧道就能保持持续存活,彻底终结了文件传输过程中的闪退与断流。


三、 穿透文件系统:AirTraffic 逃逸与 AFC 容错提取全流程

拥有了 lockdownd 授权,下一步就是把静音文件写入受保护的系统路径。AirBeep 依赖的核心利用点是 iOS 遗留的媒体同步组件 AirTraffic(atc) 与流式解压服务 streaming_zip_conduit。

1. 越狱级沙盒逃逸的写入管线

整个跨沙盒目录写入由 Rust 端的 al_exploit_write_dir 驱动:

  1. 符号链接(Symlink)构造:利用 streaming_zip_conduit 能够解压归档并写入 Media 目录的特性,在临时目录构造一组指向目标系统目录 /var/mobile/Library/CallServices/Greetings/default 的深层相对符号链接;
  2. 元数据重定向:篡改 Books 同步元数据 Books/Sync/Books.plist,在其中注入伪造的书籍下载资产路径;
  3. AirTraffic 搬运触发:唤醒系统媒体守护进程 atc 执行图书同步。atc 以为自己在同步受信任的媒体数据,顺着符号链接解引用,以系统服务级别(mobile 用户身份)直接将沙盒外暂存的静音音频批量写入目标目录;
  4. 痕迹清除:写入完成后,自动恢复原始的 Books.plist,清理所有临时软链接与缓存。

2. 首创端侧回读:AirliftToneTransport 的容错读取机制

在以往的同类工具中,沙盒逃逸几乎都是“单向写入”,一旦写入失败或者文件损坏,App 根本无法得知真实状态。

在 AirBeep 2.5.x 中,开发者在 rust-core 中突破性地实现了 al_exploit_read_file,构建了一条高技巧性的逆向读取通道:

  1. 临时搬运:利用 atc 的同步管线将目标系统文件(如 StartDisclosureWithTone.m4a)逆向软链接并搬运至未受保护的 Media 共享目录;
  2. AFC 复制:通过经过配对鉴权的 Apple File Conduit(com.apple.afc)标准协议,直接读取已搬运文件的原始二进制流;
  3. 安全回位:读取完毕后立即将原文件写回原路径,并擦除 Media 暂存区。
// airbeep/Airlift/AirliftToneTransport.swift
private static func readSync(path: String, pairingPath: String) throws -> Data {
    guard let symbol = dlsym(rtlDefaultHandle(), "al_exploit_read_file") else {
        throw CallRecordingToneError.airliftUnavailable
    }
    let readFile = unsafeBitCast(symbol, to: ExploitReadFileFn.self)

    var output: UnsafeMutablePointer<UInt8>?
    var outputLength = 0
    var outputError: UnsafeMutablePointer<CChar>?
    let result = pairingPath.withCString { pairingCString in
        path.withCString { pathCString in
            readFile(
                pairingCString,
                pathCString,
                AirBeepLog.ffiCallback,
                nil,
                &output,
                &outputLength,
                &outputError
            )
        }
    }
    // ... 解析并返回 Data
}

[!NOTE] 为什么读取通道在部分 iOS 版本上会被设计为“容错降级”?
在部分 iOS 27.x 系统版本中,苹果对 atc 的并发回调进行了保护,导致读取探针的搬运可能偶发挂起。AirBeep 极富远见地将读取策略定义为“宽容降级(Tolerant Read)”:每个音频文件独立读取,若读取超时绝不中断主写入流程,而是以写入通道的返回值作为最高真值判定,避免了全量操作因校验超时而惨遭拦截。


四、 稳定性与人机交互设计:摒弃歧义开关与“系统语言恢复”不变式

早期的许多系统微调类工具非常喜欢在首页放一个类似 iOS 原生的 UISwitch 开关(开表示静音,关表示恢复)。但 AirBeep 在 v2.5.1 中断然将这个开关全面废除,这一架构变更背后蕴含着极高水准的防御性编程思考。

1. 为什么说在不稳定 I/O 下用开关是糟糕的设计?

在一个依赖非公开系统行为的底层工具中,要准确显示一个 UISwitch 的开关状态,必须在 App 每次冷启动时先执行一次全量系统文件回读。

一旦底层 airlift 读取在特定设备或系统上卡住,整个界面的开关就会永远呈现灰色禁用态,导致用户既不能静音、也不能恢复。

因此,AirBeep 重新设计了确定性独立三键操作范式:

  1. “备份原始提示音”:在首次操作前,引导用户将系统出厂的原始文件备份至 App Documents 的 AirBeepBackups 目录。备份具有只读性,后续绝不被二次覆盖;
  2. “静音录音提示音”:一键写入静音文件。若已存在备份则执行静音;若尚未备份,优先尝试自动备份;
  3. “恢复原始提示音”:备份存在后,原备份按钮动态切换为“恢复”,随时将出厂音频原路写回;
  4. “校验提示音状态”:独立提供按需检测按钮,明确给出三种客观结果:已静音、原始提示音仍在 或 本机无法校验,绝不以虚假状态欺骗用户。
stateDiagram-v2
    [*] --> Idle: 打开 AirBeep (不执行昂贵的底层读)
    Idle --> BackingUp: 点击【备份原始提示音】
    BackingUp --> BackupComplete: 回读并写入 Documents/AirBeepBackups
    BackupComplete --> Silencing: 点击【静音录音提示音】
    Silencing --> SilencedVerified: 写入静音 + 回读校验一致
    Silencing --> SilencedUnverified: 写入成功但回读超时 (提示按需校验)
    
    SilencedVerified --> Restoring: 点击【恢复原始提示音】
    SilencedUnverified --> Restoring: 点击【恢复原始提示音】
    Restoring --> BackupComplete: 出厂文件写回成功
    
    state "极速兜底:无备份恢复" as LangFallback {
        [*] --> LanguageChange: 用户前往系统设置切换语言
        LanguageChange --> NativeRegen: iOS callservicesd 自动重新生成官方提示音
    }

2. 苹果系统自愈机制:更改系统语言的终极兜底

如果用户在没有备份的情况下直接执行了静音,或者误删了 App 导致本地备份丢失,手机是否就永远失去原声了?

答案是不会。AirBeep 的开发者敏锐地利用了 iOS 系统的一项底层内建自愈机制:每当用户在系统“设置 › 通用 › 语言与地区”中更改系统主语言(例如将简体中文临时切换为繁体中文或英文,然后再切回)时,iOS 会自动触发全量系统多语言音频资产的重生成(Asset Invalidation & Regeneration)。

此时,系统内部的守护进程会以最高权限重新从只读系统分区的只读镜像中,将标准出厂提示音重新释放到 /var/mobile/Library/CallServices/Greetings/default/。这一天然的系统“重置开关”,构成了 AirBeep 最坚不可摧的安全护城河。


五、 实机部署与网络排查全流程指南

在实际编译与部署 AirBeep 时,涉及签名、网络路由与网段对齐,我梳理了以下实操要点:

1. 编译与签名构建

项目支持本地 Xcode 编译,或利用仓库内置的 GitHub Actions 工作流云端构建 unsigned IPA:

# 本地终端构建 Release 目标
xcodebuild -project airbeep.xcodeproj -scheme airbeep \
  -configuration Release -destination 'generic/platform=iOS' \
  DEVELOPMENT_TEAM=<你的开发团队ID> AIRLIFT_TARGET_HOST=10.7.0.1 build

如果你使用 AltStore、Sideloadly 或企业证书侧载工具,可直接下载 GitHub Actions 产出的 unsigned IPA 文件进行自签名分发。

2. 应对局域网与网段冲突(高级设置)

从 iOS 26.4 起,系统的回环连接策略会受到物理 Wi-Fi 网段分配的严格制约。如果你的 LocalDevVPN 分配的网段与当前局域网发生重叠,或者你修改了 LocalDevVPN 的内网 IP,默认的 10.7.0.1 可能会遭遇 Connection Refused 报错。

AirBeep 在“关于 › 高级设置”中提供了两项硬核诊断功能:

  1. 实时网络接口审查(Network Interfaces):通过 C 语言底层的 getifaddrs 直接枚举当前所有活跃网卡(如物理 Wi-Fi en0、移动网络 pdp_ip0 以及虚拟隧道 utun)。这避开了高层 NWPathMonitor 在 VPN 接管默认网关后误报“Wi-Fi 断开”的深坑;
  2. 自定义设备 IP(Custom Device IP):允许用户在 UI 层将拨号目标直接覆写为 LocalDevVPN 的实际分配地址,无需重新编译应用即可动态适配复杂网络环境。
+---------------------------------------------+
|                高级网络设置                 |
+---------------------------------------------+
| 自定义设备 IP: [ 10.7.0.1                 ] |
|                                             |
| 当前连接状态:                               |
|   目标地址: 10.7.0.1:62078                  |
|   Wi-Fi 状态: 已连接 (en0: 192.168.1.105)   |
|   隧道状态: 已连接 (utun4: 10.7.0.1)        |
|                                             |
| 活跃网络接口列表:                           |
|   en0     192.168.1.105   [UP, RUNNING]     |
|   utun4   10.7.0.1        [UP, RUNNING]     |
+---------------------------------------------+

六、 总结与安全反思

AirBeep 是一个将 iOS 逆向工程、沙盒逃逸漏洞利用与严密工程防御完美融合的典范项目:

  1. 架构极简而专注:它没有把精力耗费在复杂的越狱环境搭建或高危的内存 Hook 上,而是精准锚定系统的物理音频资源,通过对齐格式的静音文件实现“声学级”消除;
  2. 工业级的稳定性保障:引入 Rust 底层的 Lockdown Heartbeat 心跳维持机制,解决了会话意外断连;设计“宽容降级”读取通道,防止了单点 I/O 阻塞整体业务;
  3. 闭环的安全自愈链条:通过强制备份、校验提醒以及借力 iOS 原生“语言重置机制”,彻底消除了让用户设备陷入不可逆损坏的风险。

[!WARNING] 法律与伦理提示
苹果在通话录音中加入提示音,核心目的在于遵循各国和地区关于“双方知情同意(Two-Party Consent)”的法律法规。在使用此类工具消除提示音时,必须严格遵守所在地区的法律规定,在确保合法合规并征得通话对方同意的前提下使用。


原文链接与参考资料