anat0m1a/liblzraven

GitHub: anat0m1a/liblzraven

Apple LZRAVEN 压缩格式的跨平台 C11 开源实现,解决了非 Apple 平台无法解码 OS 27 OTA payload 的问题。

Stars: 0 | Forks: 0

# liblzraven *Apple 的 LZRAVEN codec(`COMPRESSION_LZRAVEN`,算法枚举 `0xD05`), 使用 C11 重新实现 —— 可在任何平台上解码 OS 27 OTA payload。* ## 前言(关于 AI 的使用) 来自本仓库人类所有者的一段简短前言: (是的,这是我在说话,而不是 claude) 这里的每一行代码都(很明显)是由 Claude 编写的。坦白说,我需要让它为我自己的个人工具服务,而这是实现该目标的最快途径。我不打算以任何形式维护这个项目,但我觉得它值得分享给其他可能需要它的人。 根据许可证的规定,你可以随意使用这些代码 :) 我希望人们会发现它在处理 Apple OTA 发布(或者他们选择使用这种新格式压缩的任何内容)时很有用,特别是当他们的所选平台不是 *OS 时。 正是出于这个原因,我想让测试成为这篇 readme 的核心部分,以表明虽然这 100% 是由 AI 撰写的,但对于生成的代码还是进行了一些“测试”工作。 Claude 确实也编写了这里的测试,但这些测试是以 Apple 的 libcompression 本身为基准进行评分的,并在模拟环境下运行。每当 claude 在这里提到某个结果是字节级完全一致的,它指的是我们实现的输出与 libcompression 的对比。 无论如何,回到 claude :) ## 是什么 **LZRAVEN** 的纯净室 C 实现,这是 Apple 在 OS 27 周期(`COMPRESSION_LZRAVEN`)中引入的压缩 codec。 Apple 没有发布任何源代码和格式规范——只有一段文字描述。据我所知,目前没有其他开源实现:目前仅存的“lzraven 支持”是通过 cgo 调用 Apple 闭源的 `libcompression`,因此仅限 macOS 使用。 这个库可以在任何地方解码 LZRAVEN 流。C11 编写,无依赖,无动态分配。格式记录在 [`SPEC.md`](SPEC.md) 中。 ## 为什么 OS 27 OTA payload 从 `pbzx` 容器(xz)迁移到了**使用 LZRAVEN 压缩的 `pbzm`**。这种切换在 OS 27 边界处非常清晰——在所调查的 OTA 中,OS 27 上的 10 个全部使用 `pbzm`,而 OS 26 上的 7 个全部使用 `pbzx`。 因此,任何在非 Apple 平台上解包 Apple OTA payload 的工具都会在 OS 27 处停止工作,因为那里不存在 `libcompression`。watchOS 是最极端的情况:它仅通过 OTA 发布,因此没有 IPSW 可以作为备选。 ## 测试方法 LZRAVEN 唯一的参考实现仅在 Apple 平台上运行,而这是在 Linux 上开发的。解决这个问题是首要任务,其他一切都以此为基础。 **oracle。** `tools/oracle.py` 将 Apple 的 `libcompression.dylib` 加载到 Unicorn ARM64 模拟器中,并直接调用其真实的编码器和解码器——在 Linux 上获得真值,无需 Mac。因此,假设是通过使用 Apple 自己的代码生成数据来证实的,而不是通过对反汇编进行推理。 在 OS 27 周期的 Apple 平台上,`tools/native_oracle.py` 提供了相同的接口,但由 `libcompression` 本身支持,速度快了大约一千倍。 两者兼有使得模拟器可以在不同的 OS 构建上,与它所模拟的对象进行核对:**当时运行时带有明文的每一个提交向量——共 30 个——在本机编码器下重新编码后字节完全一致**,两种 scratch 大小完全一致,并且所有 14 个派生常量表在 macOS 镜像中逐字节出现。该格式是通过模拟器逆向得出的,而模拟器是正确的。 **差分测试**(`tools/difftest.py`)在相同的输入上运行两个解码器,并逐字节进行比较。结果: | 集合 | 输入 | 明文比较量 | 差异 | | --- | --- | --- | --- | | 提交的向量 | 33 | 1,432,758 B | 0 | | \+ 模糊测试语料库,+ 20 个真实的 OS 27 OTA payload | 183 | 102,668,856 B | 0 | | \+ 原始 AFL 队列 | 1574 | 27,790,368 B | 0 | | 对比 Apple 的**原生**解码器(macOS 27,M4 Pro) | 182 | 41,774,082 B | 0 | 所有 20 个切取的真实 OTA payload 解码后与 Apple 的输出在字节上完全一致。 这种比较是*分类*的,而不是断言相等,因为这两个解码器故意设计得不完全等价——我们比 Apple 验证得更多。“我们在 Apple 接受的地方拒绝”被单独计算,而另外两种情况属于真正的 bug(“我们在 Apple 拒绝的地方接受”和“两者都接受,但字节不同”)。 针对原生解码器进行测试发现了第一类情况实际上来自哪里。Apple 的解码器**不**检查八个 rANS 状态是否在一个块结束时被耗尽;它将它们的低半部分作为未编码的尾部写出,并且无论如何都返回成功。我们要求这一点,因此对于一个熵编码器没有一致结束的数据块,Apple 会进行解码,而我们则会拒绝。两个展示此情况的流被保留在 `tests/vectors/apple-accepts/` 中。这种不变性在 Apple 自己的编码器为我们生成的每一个块上都成立——这是他们编码器的一个属性,而不是他们的解码器强制执行的规则。 **模糊测试。** AFL++:解码器上进行了 3.77 亿次执行,BCJ filter 上进行了 5000 万次执行——零崩溃,零挂起。libFuzzer:在三个测试工具(harness)上又进行了约 3500 万次执行。整个过程中 ASan + UBSan 均无异常。覆盖率:rANS 编码器、模型和 filter 的 100%;符号语法的 97%。 编码器有其自己的对抗性测试层:一个往返测试工具(在模糊测试选择的级别和窗口下,将我们编码器的输出通过我们的解码器,并进行字节比较——这种对于无校验和的格式无法承受的失败,正是它会中止的失败)以及一个直接匹配查找器测试工具,用于在每个位置断言查找器的契约。运行至覆盖率达到平台期,而不是运行至特定时间:653,226 次往返执行和 210 万次匹配查找器执行,零发现,往返语料库在新特性枯竭前增长了八倍。它仍然是较新的代码,远不及解码器的累积里程——`fuzz/README.md` 如实地记录了该统计。 **突变测试**——因为一个从未捕获到任何东西的测试套件什么也证明不了。故意向解码器的副本中注入了五个 bug: | 注入的 bug | 捕获者 | | --- | --- | | 移除匹配边界检查 | 模糊测试 + UBSan,第 34 次执行(<1 秒) | | BCJ 偏移变为块相对 | 差异测试 —— 静默分歧 | | 移除保留位检查 | 差异测试 —— 接受 Apple 拒绝的内容 | | CDF 自适应率为 6 而不是 7 | 差异测试 —— 29 个严格度差异 | | 匹配文字 nibble 速率错误 | 差异测试 —— 28 个严格度差异 | 五个全部被捕获。这就是为什么“3.77 亿次执行,零崩溃”这句话有意义的所在。 **它发现了什么。** 差分层捕获了一个结构性模糊测试无法捕获的真实 bug:此解码器接受的两个流被 Apple 拒绝了。Apple 在每次匹配复制前检查 `pos + length <= coded_end`,并直接拒绝;而这段代码之前是进行截断(clamped),基于一个写在注释中且从未测试过的假设。已修复,并将两个重现用例保留为拒绝 fixture。模糊测试在 3.77 亿次执行中都显示通过,因为它没有产生内存错误——只是行为错误。 **默认门控。** `make test` 是自包含的,它强制执行上述声明而不是信任它们:所有提交的向量通过 C 库时均字节级精确一致(全部 33 个,包括多块流,在最低数量底线之后,这样缺失的语料库会导致失败,而不是空洞地通过);所有四个必须拒绝的 fixture 都被 C 解码器以确切预期的状态拒绝,并且同样被独立的 Python 参考解码器拒绝;确定性测试锁定了 rANS 刷新边界和超长距离拒绝——核心安全检查——具有突变验证的敏感性;以及容器和帧解析工具套件。C 测试在完全等于目标容量的输出缓冲区上进行分配,因此在 `make asan` 下,一字节的越界就是一个报告,而不是通过。`make diff` 默认在 `--max-stricter 0` 处设下关卡,如果 oracle 无法产生判定则直接严重失败,而不是报告空洞的一致。 **固有的警告。** 所有这些都是针对确切的某一个 Apple 构建进行验证的:仓库记录了其 sha256 的 iOS 27.0 beta 4 dylib,并针对 macOS 27.0 镜像进行了一次交叉检查(`SPEC.md` §14)。一个修订版可能会更改格式,并且在 payload 失败之前这里无从知晓——这是没有规范的重实现所固有的。来源纪律约束了它:每一项声明都与特定的工件绑定,因此未来的分歧将是可归因的,而不是神秘的。 ### 重现 ``` make test-paths # every SIMD path, 33 vectors python3 tests/validate_symbols.py # independent Python decoder python3 tools/difftest.py --full --fresh --max-stricter 0 # against Apple's decoder python3 tools/enc_roundtrip.py --big # ours encodes, Apple decodes python3 tools/enc_roundtrip.py --far --far6 # distance classes 5 and 6 python3 tools/enc_real.py # the same on real OTA plaintext make asan-paths # sanitizers, every path make -C fuzz run-decode # libFuzzer, decoder make -C fuzz run-roundtrip # encoder round-trip fuzzer ``` `--far6` 构建一个 271 MB 的明文,并需要大约 3 GB 的 RAM;这里不会提交任何大文件,测试工具会动态生成它。 向量测试是自包含的。差分测试工具需要 Apple 的 codec:在 Linux 上,这意味着在 `work/` 目录中放一份 `libcompression.dylib` 的副本,你必须自己提供——这是 Apple 的专有代码,不在此处分发。在 macOS 27 或更高版本上什么都不需要,因为操作系统自带;`difftest.py` 会自动选择该后端。 基准测试需要一个语料库。`python3 tools/make_corpus.py` 使用 Apple 的编码器从该仓库自身的内容构建一个语料库,并且两个基准测试工具都可以通过 `--streams` 接受它。 ## 使用方法 ``` make # build/liblzraven.a make shared # build/liblzraven.so ``` ``` #include size_t n = lzraven_decode_buffer(dst, dst_capacity, src, src_size); if (n == 0) { /* rejected — lzraven_decode_buffer_ex() gives the reason */ } ``` 编码使用调用者提供的 scratch,默认值即 `lzraven_encode_buffer` 所使用的: ``` void *scratch = malloc(lzraven_encode_scratch_size()); size_t n = lzraven_encode_buffer(dst, lzraven_encode_bound(len), src, len, scratch); lzraven_encode_params p; /* or tune it */ lzraven_encode_params_default(&p); p.level = 8; /* 0..9, 6 = default */ p.window = 32u << 20; /* max match distance */ scratch = malloc(lzraven_encode_scratch_size_params(&p)); n = lzraven_encode_buffer_params(dst, cap, src, len, scratch, &p); ``` 由于 scratch 由调用者分配,其大小可以在编码前从参数中推导出来;它主要由匹配查找器决定,因此由窗口决定。 Python 中的用法相同:`bindings/python/` 是一个仅依赖标准库的 `ctypes` 绑定——一个文件,无需构建步骤,加载上面提到的共享对象(`pip install bindings/python`,或复制 `lzraven.py`)。`decode` 需要解压后的大小,因为 raven 流不携带该信息;`pbzm` 容器携带(`SPEC.md` §10)。 ``` import lzraven plain = lzraven.decode(blob, unc_size) coded = lzraven.encode(plain, level=8) ``` 仅限缓冲区,与 Apple 的 API 对应:LZRAVEN 没有流式接口。解码器不分配任何内容——固定的 35,336 字节工作集,完全自动的存储,且共享对象根本不导入任何分配器。 在 89.7 MiB 的真实 OS 27 payload 上与 liblzma 的对比测量(`tools/benchmark.py`,完整数据见 `SPEC.md` §12): | | 相对 `xz -6` 的压缩率 | 相对 `xz -6` 的吞吐量 | 解码器内存 | | --- | --- | --- | --- | | liblzraven | 不相上下(10 胜,9 负,1 平) | **1.96×** (AVX2) | 固定 35,336 B | | liblzma `-6` | — | 1.00× | 8.06 MiB | CDF 搜索和更新是手动向量化(SSE2 / AVX2 / NEON)的,带有运行时分发;可移植的 C 路径是参考,**要求所有其他路径与其在字节上完全一致,并以此方式进行测试。** 向量化将解码器的速度从 liblzma 的 0.67× 提高到了 1.96×。在密度更高的子集上——这是最真实的数据,因为整个语料库被压缩了 29.9 倍——它的速度为 1071 MiB/s,而 xz 为 652 MiB/s。 在 Apple 芯片(M4 Pro,macOS 27)上,NEON 路径的速度是可移植参考的 **2.37×**,并且 Apple 自己的解码器终于可以在相同的流上与我们的解码器一起进行计时了。在真正经过编码的高熵内容上: | 解码器 | MiB/s | 对比 xz -6 | | --- | ---: | ---: | | Apple 原生 | 1333 | **4.07×** | | 我们,`neon` | 954 | 2.91× | | 我们,scalar` | 502 | 1.53× | | xz -6 | 328 | 1.00× | 从中可以得出两点结论。Apple `[apple]` 提出的“在 Apple 芯片上解码速度比 LZMA 快约 3 倍”的声明——`SPEC.md` 曾将其标记为无法验证——**成功复现,且留有余地。** 并且**他们的解码器确实比我们的快**——在此内容上,我们的吞吐量约为他们的 72%;我们仅在稀疏 payload 上获胜,在那种情况下,我们的向量化重叠匹配复制击败了他们。在此库进行后续更改后重新测量:我们解码器的吞吐量在 ±1% 的范围内保持不变。 完整的方法、语料库和各个 payload 的数据见 `SPEC.md` §12.7。该语料库与上述 x86 的语料库不同(Apple 的 OTA payload 不可重新分发,因此 `tools/make_corpus.py` 使用我们拥有的数据构建了一个替代品),因此在两者之间进行比较时,请看比率,而不是绝对数字。 内存是结构性优势:LZRAVEN 根本没有解码器侧的窗口,因此无需保留任何内容。该解码器也从不调用分配器,因此解码不会因为内存不足而失败。 ## 状态 **解码器已完成**——帧解析、熵编码器、符号语法、以及两个 BCJ 可执行文件 filter。 **编码器:可用,且已接近完美。** 存储块加上整个符号语法——字面量、匹配的字面量、全新距离的匹配,以及四个重复距离中任意一个的匹配——基于哈希链匹配查找器,使用贪婪解析,带有一个针对实时自适应模型(而不是静态表)进行定价的单位置惰性前瞻。所有情况在通过 *Apple 自己的解码器* 时均实现字节级精确往返,包括重新编码并回馈的 94 MB 真实 OS 27 OTA 明文。它也有自己的模糊测试层——即上述的往返和匹配查找器测试工具——尽管其对抗性里程仍然只是解码器的一小部分;`fuzz/README.md` 如实说明了它被推进到了何种程度。 在该 OTA 明文上的压缩率为 **0.0347,而 Apple 为 0.0334**(默认级别下)——整体大 4%,但在 20 个块中有 5 个比 Apple 的更小。编码在该数据集上的运行速度约为 70 MB/s,在冗余度较低的内容(Mach-O、英文文本)上为 4–20 MB/s,这与 xz 在类似字典大小下的速度相当。 剩下的差距在于解析:在类文本数据上,Apple 的解析优于贪婪加惰性解析,而最优(动态规划)解析是下一步的工作。 压缩级别和匹配查找器窗口已公开(`lzraven_encode_params`);在同一语料库上,级别范围从 110 MB/s 下的 0.0389 到 13 MB/s 下的 0.0340,默认值为 6,因为它是拐点。 所以:请大胆使用解码器;编码器是符合标准且具有竞争力的,但最后几个百分点的压缩率仍有提升空间。 **格式中最后一个未经验证的部分现已得到测试。** 距离类 5 和 6 在整个项目中都是通过推断得出的,因为 Apple 的*编码器*具有硬编码的 8 MiB 匹配查找器窗口,物理上无法发出它们——这使得模糊测试和差分测试都无法触及它们。Apple 的*解码器*无论如何都会实现所有七个类,因此一旦我们的编码器可以被要求提供更大的窗口,流就可以从我们这一端生成:一个 271 MB 的明文,带有十六个类 5 距离(`d1` 字段的每一个值)和两个类 6 距离,**通过 Apple 的解码器、独立的 Python 参考解码器以及我们的解码器**均实现了字节级精确往返。 `tools/enc_roundtrip.py --far --far6`;类 6 中目前仍无法触及的部分,以及为什么这是算术问题而不是方法论问题,在 `SPEC.md` §5.4 中有说明。 ## 布局 ``` SPEC.md the format specification include/ src/ the library tests/ vectors, C tests, an independent Python reference decoder tools/ oracle, differential tester, benchmarks, pbzm container parser fuzz/ libFuzzer + AFL++ harnesses and corpora docs/notes/ per-area reverse-engineering notes ``` ## 法律声明 这属于互操作性逆向工程,与 LZFSE 重实现和 `libzbitmap`(Apple 的 LZBITMAP,通过黑盒测试逆向)属于同一类别。此处不重新分发任何 Apple 代码或二进制文件——`libcompression.dylib` 仅在本地进行分析,并未提交。 ## 许可证 **0BSD**——见 [`LICENSE`](LICENSE)。每个源文件都带有 `SPDX-License-Identifier`。它对你没有任何要求:无需署名,无需保留声明,没有任何条件。 这涵盖了这里的原创工作:库、测试工具、工具和文档。它不能授予 LZRAVEN *格式*(属于 Apple)中的权利,它也不是专利许可证——它仅授予版权许可,并且不对任何其他人持有的专利作出任何陈述。
标签:Apple, C11, LZRAVEN, 云资产清单, 客户端加密, 编解码器, 解压缩, 逆向工具, 逆向工程