2026-10-03 Apple T2 安全研究:FRPFILE MAC T2 V2.0 的 Ramdisk 架构、代码签名与系统观察
- 数据时间:2026-10-01 至 2026-10-02 样本分析、通信记录与设备日志;2026-10-03 补充系统安装、升级及 Touch ID 观察
- 研究对象:FRPFILE T2 V2.0 的 Windows、macOS 版本及 LOLTool Ramdisk V43 Windows;重点观察 macOS 版本与 Apple T2 设备的交互
- 报告类型:嵌入式系统安全研究
- 资料来源:研究样本静态分析、镜像格式离线校验、SSH 前台运行、
dmesg与tcpdump对照、三组通信记录及实机复查;另参考 Apple 的公开安全文档
在一台 MacBook Pro 16-inch 2019(A2141)上,我们观察到 FRPFILE MAC T2 V2.0 相关处理后的激活状态经历冷启动、安装 macOS 12 以及系统内升级至 macOS 15.8.1 后仍保持有效。设备操作前显示 BridgeOS 10.5,安装 macOS 12 期间升至 11,升级至 macOS 15.8.1 后复查仍为 11。功能并未完全恢复:Touch ID 可以录入指纹,却不能用于解锁,重启后已录入的指纹还会消失。
这项研究起初有三个候选,随后逐步收敛到 macOS 样本。真正改变排查方向的是一次设备端异常:修改后的程序能回答设备信息查询,却在处理请求时断开连接。把原版程序、修改版程序、设备日志与代码签名结构放在一起比较后,我们才将故障定位到可执行页完整性。后续的成功响应和冷启动观察又回答了不同层面的问题。下文按这条判断路径展开。
三个候选样本与研究重点
我们取得 FRPFILE T2 V2.0 的 Windows 包、同系列 macOS 包,以及 Windows 版 LOLTool Ramdisk V43。V2.0 和 V43 是资料包或产品标识;Windows FRPFILE 主程序的 PE 版本资源为 1.0.0.0,LOLTool 主程序的版本资源为 1.8.8.8,都不能直接当作保护器版本。
| 候选 | 样本中可确认的结构与保护特征 | 本轮研究取舍 |
|---|---|---|
| FRPFILE T2 V2.0 Windows | 主程序基于 .NET Framework 4.8;VMProtect 归属具有较高可信度,名称混淆与虚拟化分发已定位。修改文件后观察到启动期完整性阻断,调试时还遇到反调试与系统代理检测。 | 主程序的虚拟化路径尚未充分还原,先分析可独立校验的镜像资源。 |
| LOLTool Ramdisk V43 Windows | 独立原生 PE;.vm_sec、.winlice、.boot 等节呈现商业保护壳特征。Oreans/WinLicense 家族是候选判断,具体产品及版本仍待确认。 | 暂时降低优先级,避免把壳特征直接当作设备端行为。 |
| FRPFILE T2 V2.0 macOS | Swift 宿主 GUI、双架构 Go 辅助程序 FRPFILE 和内嵌 ARM64 设备程序 FRPFILET2 构成较完整的调用链;已有通信记录、设备日志及后续系统观察。 | 作为主线,分析 Ramdisk 通信、代码签名故障和设备结果。 |
候选筛选并不要求先还原每个主程序。Windows FRPFILE 的 .NET 主程序有名称混淆与虚拟化分发特征,修改文件后出现启动期完整性阻断;反调试和系统代理检测也增加了动态对照成本。LOLTool 的保护壳归属仍待确定,设备端资产还没有形成可独立审计的执行链。macOS FRPFILE 则能够同时比较安装包资源、宿主辅助程序、设备端二进制和实机通信,因此成为本轮主线。我们仍保留 Windows 资源层已经完成的分析,没有把“暂缓主程序逆向”写成“没有研究结果”。
Windows 镜像为什么值得单独分析
Windows 主程序资源中有一个未加密的 ZIP 容器,但容器内的 61 个 IMG4 条目各自带有 RNCryptor 密文外层。这个分层很关键:ZIP 可打开,只说明资源目录可见,并不意味着启动镜像已经是明文。离线检查密文头与完整性标签后,我们确认其中 60/61 使用数据格式 v2,1/61 使用 v3;macOS 包的 62/62 均使用 v3。这里的版本指密文数据格式,不是 RNCryptor 库的发行版本。
两端的镜像封装参数并不相同。使用对应参数时,Windows 的 61 个镜像都通过 HMAC-SHA256 校验,完成 AES-256-CBC 解密与 PKCS#7 去填充,所得数据带有 IMG4 头;macOS 的 62 个镜像也通过各自的完整性校验。我们因此能确认格式差异是真实样本特征,而不是把校验失败的密文误判为另一种格式。镜像封装与设备许可证的 RSA 验签是两层机制,不能从前者推断后者的私钥或授权规则。
看到 Windows 包几乎全部采用 v2 时,一个自然的解释是其打包链对 v3 曾有兼容问题。然而那份有效的 v3 镜像推翻了“Windows 完全不能处理 v3”的简单说法。RNCryptor v2 规格记录过 Objective-C 实现对多字节口令的历史问题;本次 Windows 样本所用口令仅含 ASCII 字符,这条已知问题无法直接解释版本分布。不同批次封装、打包工具选择或 Windows 实现缺陷都仍是待检验假设。主程序运行时的具体构造路径尚未还原,现有离线结果不足以判定哪一个原因成立。
这些数字只对应本次取得的样本。Windows 主程序的反调试与代理检测来自运行观察,具体判定点尚未从受保护代码中独立确认;LOLTool 的 WinLicense 归属也保持为候选判断。
Ramdisk 与宿主、设备两端的关系
macOS 安装包包含面向 12 种机型模组的启动资产和约 128 MiB 的通用 Ramdisk 镜像,并附带 gaster、irecovery、iproxy 等工具。镜像外层采用 RNCryptor v3 封装;静态调用关系显示,宿主程序在恢复协议的各阶段将引导组件送入设备。样本中的顺序为 iBSS → iBEC → DeviceTree → Universal Ramdisk → TrustCache → KernelCache。这是程序与资源呈现的引导序列,静态提取本身不证明每一项在设备上都成功加载。
离线挂载通用 Ramdisk 后,HFSX 文件系统中的版本文件显示 BridgeOS 6.6(Restore)。目标设备在实验开始前显示 BridgeOS 10.5,安装 macOS 12 的过程中升至 11,后来升级 macOS 15.8.1 时复查仍为 11。6.6、10.5 和 11 分别来自临时镜像、操作前设备和后续设备复查,属于不同对象和时间点。这个区分决定了我们如何解释后面的“升级后仍有效”:临时环境运行过,并不代表设备固件回退到 6.6。Apple 的安全启动文档提供了 T2 与 Intel Mac 信任链的通用背景;上述三个版本是本次样本和设备的观察。
从宿主 GUI 追到设备端服务
宿主 GUI BypassT2 由 Swift 编写,配套的 Go 辅助程序包含 x86_64 与 arm64 两个架构切片。辅助程序通过 embed.FS 携带约 8.55 MB 的 ARM64 设备程序 FRPFILET2。这解释了为什么仅检查宿主 GUI 或仅检查独立镜像都无法看到完整处理链:GUI 决定何时进入下一阶段,辅助程序执行设备传输,真正解析许可证和处理后续数据的是 T2 临时环境中的程序。
宿主侧静态控制流显示,启动时先检查远端版本响应;进入处理流程前还会读取系统代理配置。注册检查存在一个值得单独记录的分支特征:已还原的客户端代码只在收到明确的否决文本时阻断,其他响应类型流向后续辅助程序调用。这个判断来自静态分支,不能据此断言各种网络故障在实机上都能完成整个流程。随后,辅助程序依次建立设备通信、读取设备参数、交付许可证数据、触发设备处理并请求重启。我们用通信记录对照了这些阶段,但不把 GUI 的成功提示等同于设备状态。
设备端 FRPFILET2 使用 GCDWebServer 提供本地服务。程序受 Hikari 混淆处理,样本中有 2,500 余个包装函数;相较之下,静态链接的 OpenSSL 符号与调用关系仍可辨认。许可证处理路径先解析外层 JSON 中经 Base64 编码的载荷与签名,再以内置 RSA-2048 公钥进行 SHA-256 签名校验;通过后,内层 JSON 的 ecid 还要与设备派生值一致。样本中一份 PEM 公钥以明文出现,活动验签路径的另一份 450 字节 PEM 缓冲区经过运行时解混淆;我们对其无分支字节变换做静态求值,确认活动缓冲区的 450/450 字节与明文公钥相同。样本内没有对应私钥。
另一个静态路径涉及内嵌的 payload.tar.gz:程序尝试将其释放、解压,并部署启动策略配置、launchd 配置及一个标识为 com.apple.mobileactivationd 的 ARM64 二进制。归档中的 clean 配置含 force-boot-policy-action=1 与 load-sep-apps-early=true,另有 led 和 old 两个文件名。我们能确认这些对象存在及相关调用,却没有对每个释放文件取得运行后的状态回读,因此不能仅凭静态路径归因最终激活状态由哪个组件维持。
许可证从远端响应到设备消费
我们最初需要厘清两个名字相似的 payload:远端许可证 JSON 的 payload 字段是被签名的内层数据;设备程序内嵌的压缩归档是另一份本地资源。把两者混在一起,会误读宿主为何要转交许可证,也会误读设备后续的文件操作。
宿主辅助程序先从设备信息中取得序列号、UDID、系统版本与机型等字段,再从 UDID 派生提交给远端的 ECID。远端响应的已定位外层字段是 payload、signature,以及一个可能出现的 error。宿主只检查前两个字段非空并完成设备侧文件检查;静态路径中没有看到宿主执行密码学验签。它随后经 SSH 把响应 JSON 的原始文本写入设备的 /mnt2/mobile/Media/Downloads/license.dat,保留外层字段的原貌,而不是在宿主重新签名或重组内层数据。正常服务端可能还有哪些附加字段,我们没有取得真实签发响应来确认。
设备端的消费顺序则清楚得多:先读取外层 JSON,再分别 Base64 解码 payload 与 signature;EVP_DigestVerifyUpdate 消费解码后 payload 的精确原始字节,EVP_DigestVerifyFinal 消费签名字节。内建公钥为 RSA-2048,摘要为 SHA-256;代码未见显式 padding 改写,按 OpenSSL 默认路径推断为 PKCS#1 v1.5。返回值为成功后,程序才把 payload 解析为内层 JSON,读取其中的 ecid,与设备由 UDID 派生的值比较。即使 JSON 含相同的字段,改动内层原始字节的空白或键顺序也会改变签名输入。
text
P = UTF-8 JSON 原始字节;本次测试内层只含 ecid
S = 对 P 的签名字节
外层 JSON = {"payload": Base64(P), "signature": Base64(S)}
设备端:解码 -> 验证 P 与 S -> 解析 P -> 比对 ecid宿主后续发送的设备处理请求不携带这两个字段;它触发设备服务读取此前交付的许可证文件。测试响应中的签名只有 7 字节,远短于 RSA-2048 签名应有的 256 字节,因此原版程序拒绝这份测试许可证并不意外;但错误响应没有指出唯一失败子步骤。静态代码还显示设备程序在后续路径尝试把内嵌的 payload.tar.gz 释放到缓存目录,再解压到设备根卷并处理启动策略与守护进程文件。该归档与上面的 P 没有内容上的等同关系,文件被释放后的实际采用情况也没有逐项回读。
text
+--------------------------------------- Host Mac ---------------------------------------+
| |
| [BypassT2 GUI] |
| |-- (1) version / registration <----------> [local HTTPS simulator] |
| | |-- version / registration replies |
| | |-- license / result replies |
| +-- (2) dispatch [FRPFILE Go helper] |
| |-- setup: USB forwarding + device program deployment |
| | SSH host:4444 <-> T2:9999 |
| | HTTP host:9571 <-> T2:9571 |
| |-- show: device info -> remote license -> SSH write license.dat |
| |-- process: send request, read device status |
| +-- restart: send restart request |
+-------------------------------------------+--------------------------------------------+
| USB / iproxy
|
+-------------------------------------------v--------------------------------------------+
| |
| Apple T2 temporary BridgeOS Ramdisk |
| [Dropbear SSH] <- device program and license.dat |
| [FRPFILET2 / GCDWebServer] |
| |-- read identity; derive ECID from UDID |
| |-- read license: Base64 payload and signature |
| |-- RSA/SHA-256 verification -> ECID comparison |
| |-- archive, boot policy and daemon operations (static path) |
| +-- return status; executable page checked against CodeDirectory |
+----------------------------------------------------------------------------------------+
| cold boot
v
Custom RecoveryOS (Monterey) -> macOS 12 / BridgeOS 11
-> macOS 15.8.1 / BridgeOS 11; Touch ID remains abnormal这幅图概括的是组件和观察层次。临时 Ramdisk、冷启动进入的客制 RecoveryOS(Monterey)与最终安装的 macOS 是三个阶段。图中的许可证、归档和策略节点来自静态分析;最终能否安装、升级和维持激活,则由后面的实机结果判断。
宿主侧模拟了哪些远端响应
为了让同一批样本能反复进入可观察的分支,我们在宿主侧用 mitmproxy / mitmweb 提供本地模拟响应。静态分析先确定了 GUI 与辅助程序分别期望哪些数据,再用抓包检查处理是否继续。这里的模拟对象是宿主看到的远端响应,设备端许可证验签仍是一条独立路径。
| 客户端阶段 | 本轮模拟内容 | 从客户端能观察到的作用 |
|---|---|---|
| 启动版本检查 | 返回纯文本 2.0 | 满足 GUI 对版本文字的精确比较。 |
| 设备注册检查 | 返回肯定答复 | 避开已定位的明确否决分支;异常网络条件的完整行为未由此实验覆盖。 |
| 许可证获取 | 返回带 payload、signature 字段的外层 JSON;解码后的内层 JSON 只含 ecid 字段,签名为测试数据 | 宿主能完成字段检查及设备交付,原版设备程序仍会拒绝无效签名。 |
| 结果上报 | 返回 HTTP 成功确认 | 让宿主完成本轮上报交互;该确认不是设备持久状态的回读。 |
这些响应刻意只满足已还原的客户端消费格式。我们没有取得真实服务端的签发策略,不能用测试 JSON 推断真实许可证是否还含订单、期限等字段。尤其是宿主报出的 verify failed 可能来自文件检查,而设备端的 RSA 验签发生在后续处理阶段;名称相似,证据含义不同。
从验签实验到代码签名故障
为看清授权流程中的各个分支,我们在宿主侧用 mitmproxy / mitmweb 模拟远端响应,并把宿主通信记录与设备日志对应起来。模拟响应能控制客户端收到的版本、注册及测试许可证数据,却不能产生原服务端的有效 RSA 签名。设备端程序原本会校验签名,再检查许可证内层的 ecid。这个分层使我们能够分别观察网络交互、设备程序执行和后续系统状态。
ARM64 修改影响了哪一层
设备端样本虽然有 Hikari 混淆,静态链接的 OpenSSL 验签函数仍保留了可辨认的符号。实验性修改针对 _EVP_DigestVerifyFinal,其 ARM64 汇编逻辑为:
asm
mov w0, #1 ; 将返回值置为成功
ret ; 返回调用方在 ARM64 调用约定中,w0 承载整型返回值;这里的 1 使调用方收到验签成功。它没有生成有效的 RSA 签名,也没有消除上层对载荷内容解析和 ecid 比对的检查。因而后续若返回成功,仍须分析请求是否到达设备、进程是否存活,以及上层分支实际走到了哪里。
怎样取得设备端的一手证据
只看 GUI 很难判断失败发生在宿主辅助程序、USB 传输还是设备进程。我们先从 Go 辅助程序的静态路径确认,它通过 iproxy 建立宿主 4444 到设备 9999 的 SSH 通道,以及 9571 到设备 9571 的 HTTP 通道;又从辅助程序的登录逻辑找到了临时环境中 Dropbear SSH 使用的凭据。凭据只在本地受限保存,不写入本文。借助现有转发独立登录后,我们能在 GUI 之外查看进程、许可证文件和设备日志。一次早期检查中,HTTP 通道连接失败而 SSH 仍可登录,这使“设备已完全断开”不再是恰当判断,也提示我们要分别检查转发与 WebServer 是否就绪。
SSH 只读检查发现,测试许可证文件已存在,长度为 78 字节;其内层 payload 解码后为 27 字节 JSON,测试签名解码后仅 7 字节。这个文件从点击前的不存在变为点击后的存在,支持宿主交付步骤已经发生;它本身不能说明设备验签通过。设备本地时钟当时显示 1970 年,因此我们使用宿主记录的时间与事件顺序关联各次实验,没有把设备时间戳直接当作真实发生时间。
为了排除“GUI 实际运行的不是我们检查的二进制”这一疑点,我们从当时的宿主辅助程序提取其内嵌 FRPFILET2,通过 SSH 放到设备上的独立位置,不覆盖 GUI 使用的程序或许可证。设备端文件被完整读回并与主机源文件比较长度和 SHA-256,然后在 SSH 前台运行,持续保存标准输出、错误输出与退出状态。日志显示 WebServer 启动;普通 curl 请求先得到 HTTP 403,使用与客户端一致的请求特征后,设备信息查询得到 HTTP 200。随后只发送一次处理请求,连接在约 9 ms 后以 Empty reply from server 结束,前台进程返回 137,退出前没有处理接口的业务日志。事后 SSH 仍可用,许可证文件字节未变。这轮实验确认了初版修改的设备程序确实被执行并异常退出,没有把旧 GUI 弹窗与本次进程混为同一轮证据。
常规日志目录在这次临时环境里没有提供对应的业务记录,关键内核线索来自 dmesg。手动前台运行的进程退出附近出现:
text
Text page corruption detected in dying process 282
FRPFILET2.manual[282] Corpse allowed 1 of 5我们还在宿主机对 USB 转发后的本地 TCP 通道使用 tcpdump 抓包,把请求、FIN 关闭、HTTP 响应有无与设备日志按宿主时间对应。抓包说明线上发生了什么,SSH 前台输出说明哪个程序退出,dmesg 则提供代码页异常;三者共同收窄故障位置。后续 GUI 联调的另一设备进程也出现同类内核记录,但这两次不是同一个 PID,不能把它们混写成一次操作。
三次通信记录如何改变判断
第一次设备联调并没有得到预期的处理结果。修改版程序可以回答设备信息查询,说明基本传输与服务启动已经发生;但进入处理请求后,客户端只看到 Empty reply from server。同一轮宿主记录显示连接在请求后约 6.5 ms 由 FIN 关闭,没有形成 HTTP 响应。手动测试还记录到设备程序退出码 137(SIGKILL)。当时需要分辨的是:这是模拟响应、设备传输,还是修改版程序执行本身的问题。
我们随后使用原版程序与同类测试许可证做对照。原版返回 HTTP 200 和 LICENSE INVALID!,进程继续运行。这个结果说明设备程序能接到请求并给出业务错误;测试签名未被接受。原版与修改版的请求形态相同而进程结局不同,排查重点由请求格式转向修改版程序的执行状态。原版错误响应并未细分究竟在哪一个许可证子步骤被否决。
| 2026-10-02 记录 | 程序与条件 | 宿主及设备侧观察 |
|---|---|---|
| 18:22 | 初版修改程序,代码页签名记录未同步 | 设备信息查询可用;处理请求无 HTTP 响应,连接迅速关闭;设备日志出现 Text page corruption。 |
| 18:24 | 原版程序与测试许可证 | HTTP 200,返回 LICENSE INVALID!;进程保持运行。 |
| 18:44 | 调整后的最终联合测试轮次 | HTTP 200,先返回 Activated,随后返回 Reboot。 |
第三轮的两个成功响应使我们能够确认设备接口完成了这一轮处理与重启交互。它们发生在冷启动和系统安装观察之前;本文没有用单次 HTTP 状态代替后续设备验证。
为什么出现 Text page corruption
设备日志中的 Text page corruption 为排查提供了转折点。文件比较显示,初版修改改变了 Mach-O 的可执行页,却没有同步更新 LC_CODE_SIGNATURE 中 CodeDirectory 对应的代码页哈希。该样本的签名按 4 KiB 页面记录 SHA-1 与 SHA-256 哈希;原版的签名记录与内容一致,初版修改后的相关页面则不一致。原版返回业务错误且继续运行,初版修改版没有 HTTP 响应并被终止,这组对照支持代码页完整性校验失败是异常终止的高度可信原因。
Apple 的代码签名技术说明解释了 CodeDirectory 的逐页哈希和可执行页载入时的校验机制。它帮助解释本次日志与文件差异之间的关系;具体换页时刻、物理页状态和内核内部指令顺序没有独立轨迹记录。
签名修正还必须回到完整宿主文件
问题不止存在于单独提取的 ARM64 程序。原版 Go 辅助程序是约 45.88 MB 的 Universal Mach-O,x86_64 与 arm64 两个切片都内嵌一份设备程序,并保存 embed.FS 资源长度。只修正一份提取文件,无法保证正式流程读取到相同字节;只更改指令而沿用旧签名,也已在设备上触发代码页异常。我们因而把设备程序、两份宿主资源及外层签名作为一个整体检查。
text
原版 Universal Go 辅助程序
├── x86_64 切片:内嵌 FRPFILET2 + Go 资源长度
└── arm64 切片:内嵌 FRPFILET2 + Go 资源长度
↓ 提取并修改设备程序
保留原有权限声明;为设备程序重建代码页签名
↓ 尺寸从 8,554,720 变为 8,531,168 字节
回填两个架构切片;同步两处 Go 资源长度
↓
重签名完整宿主文件并检查两个架构设备程序重签名时保留了原版 Mach-O 中的 XML Entitlements(权限声明),按 4 KiB 页重新生成 CodeDirectory 哈希与 DER 格式权限声明。这里使用的是本地 ad-hoc 签名,原发布者的签名没有被保留。尺寸变化后,我们在宿主两个切片中回填相同的设备程序,并把各自 embed.FS 的长度从 8,554,720 更新为 8,531,168 字节;检查资源指针、长度与内嵌字节的一致性。原长度若继续保留,可能导致 Go 在提取资源时读取不完整或越过有效内容;这属于结构分析,没有单独做故障复现实验。
最后使用 codesign 对完整宿主文件作全架构严格校验,本地结构与签名检查通过。这个检查说明产物在主机侧自洽;第三轮通信记录说明设备服务完成了相应接口交互,之后的冷启动和系统升级观察则检验了更长期的设备表现。各层证据的含义不同,合在一起才构成本文的结果。
冷启动、系统安装与升级后的表现
| 阶段 | 设备环境 | 观察结果 |
|---|---|---|
| 操作前 | BridgeOS 10.5 | 记录为该设备的初始版本。 |
| 处理后冷启动 | 客制 RecoveryOS(Monterey) | 能进入恢复环境,并从中安装 macOS 12。 |
| 安装 macOS 12 | BridgeOS 由 10.5 升至 11 | 系统安装完成;BridgeOS 的变化发生在安装过程中。 |
| 系统内升级至 macOS 15.8.1 | BridgeOS 复查仍为 11 | 升级完成,所观察的激活状态继续有效;BridgeOS 11 是这台设备本次显示的当前最新版。 |
Touch ID 是这条路径上的明确功能缺口:设置界面允许录入指纹,但录入后不能用指纹解锁这台 Mac;重启后,先前录入的指纹从设置中消失。现有记录没有足够的 Secure Enclave 或相关状态日志,无法确定失败发生在录入持久化、解锁校验还是其他环节。
本次结果支持上述单台设备在所记录的冷启动、安装和系统内升级路径中保持激活有效。重复冷启动次数、其他升级路径、抹盘恢复后的表现,以及 Touch ID 异常的根因,均未由这组记录覆盖。
结论与证据来源
FRPFILE MAC T2 V2.0 把宿主程序、临时 Ramdisk 和设备端服务串成一条处理链。研究中最关键的故障不是请求未到达设备,而是初版修改程序的可执行页与签名哈希失配;三轮通信对照及设备日志使这一定位具备较强证据。最终接口成功响应之后,冷启动、macOS 12 安装、BridgeOS 升级与 macOS 15.8.1 系统内升级提供了进一步的实机观察。Touch ID 未恢复正常,因此“激活有效”只描述已观察到的激活与安装状态,不涵盖全部设备功能。
本文的一手依据是本次样本的静态分析、镜像格式离线校验、SSH 手动运行与文件读回、设备 dmesg、宿主 tcpdump 的三组通信记录,以及 2026-10-03 补充的实机复查。通用背景参考 Apple 的安全启动说明与代码签名哈希说明。这两份公开文档用于解释平台机制,不替代本次样本与设备的原始观察。