kats1123/maxwell-firmware-research
GitHub: kats1123/maxwell-firmware-research
对Audeze Maxwell耳机固件进行逆向工程,揭示并修复了因NVDM配置键被锁定而导致的左/右声道不平衡问题。
Stars: 3 | Forks: 0
# Audeze Maxwell — 固件逆向工程
一份关于 **Audeze Maxwell**(原始版,非 V2)固件内部的参考文档:
音频流水线的工作原理、为何许多设备存在左/右声道平衡问题,
以及刷写自定义固件的可行方案。
Audeze 已大约两年未为原始版 Maxwell 发布固件更新。当前固件 **v1.0.1.74** 导致许多设备出现可感知的左/右声道响度不平衡。本仓库记录了为理解*原因*及应对措施而进行的逆向工程工作。
## L/R 平衡问题 —— 我们发现的情况
**症状。** 许多原始版 Maxwell 的一个声道比另一个声道响。通常是右侧,严重程度因设备而异,并且 USB-C 和无线适配器之间的表现往往*不同*。Audeze 应用程序中没有平衡控制,因此受影响的用户没有内置方法进行校正。
### 实际发生了什么
L/R 不平衡是 Audeze 在 `v1.0.1.61` 中引入的一个 Bug。v61 之前的固件没有这个问题。以下是根据跨版本二进制分析和经验性硬件测试确定的 v56 → v61 → v74 的时间线:
1. **v1.0.1.56(及更早版本)—— 不存在按音频源的机制。** v56 只有一个平衡 NVDM 密钥(`0xF668`),出厂默认值为 `142/142`(对称)。没有 `0xF665`、`0xF702`、`0xF703`-`0xF707`、状态处理程序、调度表——这些在 v56 固件中根本不存在,即使是字面引用也没有。**L/R 不平衡 Bug 在 v56 中根本不存在。** 这就是为什么回退到 v56 的用户报告不平衡消失的原因:没有东西可以让任何设备“卡住”。
2. **v1.0.1.61(2024 年 4 月)—— Audeze 添加了按音频源的平衡系统。** 新代码引入了:
- 第二个平衡 NVDM 密钥 `0xF665`,出厂默认值为 `141/149`(非对称——针对 USB-C 信号路径的逐声道校正)。
- 原始对称默认值 `0xF668 = 147/147` 保留给无线模式。
- 选择器 NVDM 字节 `0xF702`,用于在启动平衡加载器时选择 `0xF665`(当值为 `0x0A` 时)或 `0xF668`(其他情况)。
- 逐音频源的辅助函数、事件总线密钥(`0xE42A`、`0xEE23`)、一组逐状态配置密钥(`0xF703`-`0xF707`),以及逐音频源 DSP 系数表上的第二列(USB-C 逐声道校正 EQ;无线模式更平坦且对称)。
3. **`0xF702` 最终冻结在其出厂值上。** 耳机仅在启动时读取一次 `0xF702` 以选择活跃平衡。缺少的是*正常操作中任何自动更新 `0xF702` 以反映音频源变化的机制*。该系统具有一个本应在运行时切换配置文件的功能结构(`0xF703`-`0xF707` 这组密钥读起来像是逐状态的清理备忘录),但固件内部没有任何代码路径会写入 `0xF702`。我们反汇编了该密钥的每次读取和写入,追踪了耳机和适配器上 v56/v61/v63/v74 中的所有调用者和调度表。**整个固件中 `0xF702` 的唯一写入者是 RACE `0x0900 sub 0x2F` 主机命令处理程序。** 在正常操作中——播放音频、在 USB-C 和无线之间切换、插拔、恢复出厂设置——固件中没有任何东西会更新 `0xF702`。
4. **不同设备出厂时具有不同的 `0xF702` 值。** 一些产品设备的 `0xF702 = 0x0A`,并在每次启动时加载非对称的 `141/149` 默认值。**那些设备的用户会听到不平衡**,无论他们实际如何连接。其他设备的 `0xF702 = 0x00`,加载 `147/147` 对称值,从未注意到任何问题。这解释了社区报告模式:一些用户抱怨,另一些则不抱怨,恢复出厂设置从未修复任何一方的设备(恢复出厂设置不会影响 `0xF702`)。
5. **v1.0.1.63 / v1.0.1.74 延续了相同的架构。** v74 重写了无线 DSP 系数列中 118 个条目中的 115 个,而 USB-C 列一个未动;v74 的发布说明提到“修复了更新旧版本固件时可能导致 EQ 问题的 Bug”。Audeze 积极维护无线配置文件,从未触碰 USB-C 非对称配置文件。无线配置文件显然是预期的那个。
因此,配套工具提供的修复是基于原理而非启发式的:将 `0xF702` 翻转为 `0`,以便启动加载器在出厂时使用了 USB-C 非对称配置文件的设备上选择无线(已维护且对称的)配置文件。写入是持久的——通过写入探测值(`0x55`)并在完全断电后读回不变来验证。对于想要双重保险的用户,自定义 v61+ 固件还将 `0xF702` 读取函数修补为始终返回 `0`,而自定义 v56 固件则是直接回退到 Bug 前的固件,并熔入了用户的平衡校正。
### 我们仍无法解释的两件事
这是对 Bug *如何显现*以及如何*修复*任何单个设备的完整逆向工程。关于整个系统,有两个问题我们仅凭固件无法回答:
* **是否存在固件内部的 `0xF702` 自动切换器?** Audeze 几乎肯定在发布前内部测试了逐音频源系统。要么他们打算由 Audeze 应用程序(或单独的开发/质控工具)通过 RACE 驱动 `0xF702`,要么他们打算使用一个我们无法找到的固件内部触发器。我们花了大约五十小时寻找这样的触发器——检查每个 NVDM 写入路径、每个 USB/BT 连接事件处理程序、每个调度表、每个事件总线密钥,包括耳机和适配器,跨越所有四个固件版本。我们没有找到。有可能这样的路径以某种我们遗漏的编码方式存在(计算指针间接寻址、运行时安装在 RAM 中的处理程序、我们不认识的适配器→耳机 BT 厂商消息)。我们无法声称不存在;我们只能报告通过多种方法进行的详尽搜索没有找到任何结果。
* **为什么出厂设备具有不同的 `0xF702` 值?** 不同生产线上设置的不同 SKU 默认值、在某些批次上留下了测试模式值的 QC 工作台、在某些设备上写入该字节但在其他设备上未写入的制造步骤——所有这些都与社区模式一致,但没有办法在不访问 Audeze 的初始配置流程或一批全新密封设备来调查的情况下证明。
### 早期的文章:哪里错了
随着调查新证据,本节已被重写多次。为透明起见,更正如下:
* 一份草稿说逐音频源功能**以非活跃状态出厂**——导致 `0xF702` 写入器的调度链在固件中没有任何调用者。那是错的。写入路径是可到达的:RACE 命令 `0x09xx` 在主调度表 `0x0828A8E0` 中注册为处理程序 `0x0817B92C`,而 `0x0817B92C` 通过 `B.W` 无条件分支(尾调用)到达 `0x0817B0E4` 的 SET 调度器(并从那里到达 `0xF702` 写入器),而不是 `BL`——早期的静态引用搜索只查找了 `BL`/`BL.W`,错过了它。经验证明:通过工具写入 `0xF702 = 0x55`,然后给耳机断电再上电,该值读回不变。因此主机命令路径是存活的。但*不存在*的是固件内部自动发出该写入的路径。
* 一份更早的草稿说 Audeze **移除了**主事件路由器(将一个看似孤立的 22 条目调度表 `0x081C3134` 及其周围的 `bx lr` 桩集群视为移除的证据)。跨版本与 v56 的差异证明这是错的:v56 中没有 F702、F665、F703-F707、E42A、EE23、通过选择器的 F668、调度表或桩集群。整个系统在 v61 中首次出现。`0x081C3134` 表的看似孤立状态本身也不再可靠;我们在 `0x0817B0E4` 上遗漏的同一类 `B.W` 尾调用同样可能隐藏其调用者。因此该表可能存活也可能不存活——我们无法通过静态分析判断。
* 还有一份更早的草稿说“他们合并了音频源并放弃了逐音频源系统”(基于推测)。v56 也证伪了这一点——v56 的单一配置文件是 `142/142`(不是 `147/147`),而逐音频源系统是在 v61 中*新增*的,带有新的非对称默认值,并非移除以支持合并。
### 证据
**逐音频源 DSP 滤波器组。** 位于 `0x082938CE` 的一个包含 550 个系数的系数表,对于每个系数,持有*两个*左/右对——一个用于 USB-C,一个用于无线模式。启动时,固件将一列加载到 DSP 中。
* **USB-C 列是逐声道校正的**:550 个系数中有 101 个的左 ≠ 右。这种刻意的非对称性是内置于 USB-C 调音中的 L/R 校正。
* **无线列是对称的**:550 个系数中只有 1 个左右不同,并且约 248 个被完全旁路——平坦且简单。
**逐音频源平衡默认值。** 两个 NVDM 密钥保存 4 字节的 L/R 平衡——`0xF665` 用于 USB-C,`0xF668` 用于无线模式。出厂默认值:`0xF665 = (L 141, R 149)`——**非对称**;`0xF668 = (L 147, R 147)`——**对称**。无意中设置非对称出厂默认值是不可能的;这是一个刻意的校正。
**加载哪个配置文件由 NVDM `0xF702` 选择**(`0x0A` = USB-C,其他值 = 无线模式),在每次启动时由平衡加载器 `0x081de120` 读取一次,该加载器在调度器启动前从主启动初始化 `0x081d9406` 调用一次。
**`0xF702` 在整个固件中恰好有一个写入者**:RACE `0x0900 sub 0x2F` 主机命令处理程序 `0x0817B2B2`(SET 调度器 `0x0817B0E4` 的子 ID 情况 `0x2F`)。通过跨耳机和适配器、v56/v61/v63/v74 的所有 `0xF702` 的 `movw` 编码、每个 NVDM 写入函数(`0x814fed8`、`0x814ff40`、`0x814ff68`)以及每个函数指针表的穷尽性固件扫描验证。没有其他代码路径写入该密钥。
**RACE 写入链是存活的——经验验证。** RACE 命令 `0x09xx` 在主调度表 `0x0828A8E0` 中注册(条目对 `{u32 cmd_id_range, u32 handler}`,8 字节步长;范围 `0x0900..0x09FF` 的条目指向处理程序 `0x0817B92C`)。`0x0817B92C` 按精确命令 ID 分派,并通过尾调用(`B.W`)到 `0x0817B0E4` 处理命令 `0x0900`。尾调用就是为什么早期仅搜索 `BL`/`BL.W` 的静态查找发现零个直接调用者并认为调度器被孤立的原因。经验确认:通过工具写入 `0xF702 = 0x55`,然后给耳机完全断电再上电,再次读取 `0xF702` 返回 `0x55`——不变。主机命令写入路径持久化到 NVDM。
**固件内部没有代码会自行发出该写入。** 主机命令处理程序是 `0xF702` 的唯一写入者,我们没有找到任何固件路径能够合成 RACE 帧或内部调用该调度器。搜索方法包括:BL/BL.W 直接调用者、B/B.W 直接调用者、对齐和未对齐的 32 位指针字面量、MOVW+MOVT 对组装的指向指针、ADR.W / ADD-PC 的 PC 相对计算、函数指针注册位点,以及耳机和适配器的 v56→v61 NVDM 密钥差异。适配器在 v56/v61/v63/v74 中对 RACE 命令 `0x0900` 和 `0x0901` 的每个引用都是逐字节相同的;当 v61 添加逐音频源系统时,适配器并未更改。预计投入:约 50 小时的针对性分析。可能存在某种我们遗漏的编码方式——我们无法证明不存在——但我们未能找到。
**实测数据。** 在测试设备上,未应用任何校正(对称缓冲区),耳机自身输出在 USB-C 上测得 **R 比 L 响 6.8 dB**,在适配器上测得 **R 比 L 响 2.2 dB**——方向相同,但幅度差异很大。单个校正值无法同时修复两个音频源路径;每个路径需要在其自己的 NVDM 密钥中内置自己的校正。
**版本。** 固件 `v1.0.1.61`、`.63` 和 `.74` 在音频源状态和平衡代码上基本相同——该系统在 v61 中引入,此后仅进行了小的调整。相比之下,`v1.0.1.56` **完全早于该系统**:v56 的代码中不存在对 `F665`、`F702`、`F703`-`F707`、`E42A`、`EE23` 的 `movw` 引用。v56 只有一个平衡 NVDM 密钥(`F668`),具有单个对称默认值(`142/142`)。L/R 不平衡 Bug 在 v56 中根本不存在。耳机上 v56→v61 的 NVDM 密钥差异恰好是:`F006`、`F665``F702`、`F703`、`F704`、`F705`、`F706`、`F707`——这些是构成整个逐音频源切换界面的新密钥。
### 仍待解决的问题
* **固件内部是否存在 `0xF702` 的自动切换器。** 我们进行了穷尽性搜索,但没有找到。Audeze 几乎肯定在发布前测试了逐音频源系统;要么他们在 QA 中使用外部 RACE 工具设置 `0xF702`,要么他们打算使用一个我们未能定位的固件内部触发器。看似孤立的 22 条目表 `0x081C3134` 及其周围的 `bx lr` 桩集群 `0x081567CC` 和 `0x0820D264` 起初看起来像是触发器从未被连接的证据,但我们在 `0x0817B0E4` 上遗漏的同一类 `B.W` 尾调用同样可能隐藏它们的调用者,因此我们不能将其静态孤立状态视为证据。诚实的答案是:我们找不到,但也不能排除存在。
* **出厂设备如何最终得到不同的 `0xF702` 值。** 一些设备有 `0x0A`,另一些有 `0x00`。我们无法了解 Audeze 的初始配置流程。可能的候选:不同生产线设置的 SKU 默认值、批次间 QC 工作台的差异(一个测试步骤写入了 `0x0A` 但忘记清除)、在一个批次上写入该字节但在其他批次上未写的制造步骤。如果不访问一个全新的、在主机活动之前就能读取的密封设备,或者不访问 Audeze 的 QC 工具,我们就无法区分。
### 实用的修复方法
配套工具修复了两类用户的问题:
1. **一次性的 RACE 写入**(“设置音频源”按钮)将 `0xF702` 设置为 `0x00`,将耳机切换到无线(Audeze 维护的、对称的)配置文件。经测试在设备上完全断电后持久化——我们写入了探测值 `0x55`,完全关闭耳机电源,重新上电后 `0xF702` 读回为 `0x55`,不变。
2. **自定义 v61+ 固件**还修补了 F702 读取函数(`FUN_0x0817B2F4`,4 字节补丁:`07 B5 FF 23` → `00 20 70 47`),使读取器始终返回 0。刷写后,固件完全忽略 `0xF702`,无论 NVDM 状态如何,无线配置文件被固定。这实际上将 v74 简化为单路径架构——无线配置文件是唯一加载的配置文件,就像 v56 在逐音频源机制添加之前的工作方式一样。
3. **自定义 v56 固件**是回退到 Bug 前的固件——没有 `0xF702`、没有逐音频源切换、根本没有非对称默认值。架构上产生与选项 2 相同的结果(一个配置文件、一个平衡默认值、无音频源选择逻辑),只是通过回退而不是向前修补来实现。
4. **逐设备平衡校正**继续将两个 NVDM 密钥(`0xF665` *和* `0xF668`)写入用户校准的对称值,因此无论哪个配置文件处于活动状态,平衡都是正确的。详见 **[AUDIO.md](AUDIO.md)**。
## 自定义固件是可刷写的
本报告的早期版本说自定义固件被引导 ROM 中的 ECDSA 签名验证阻止。**那是错的**——Maxwell 固件上没有非对称签名。引导加载程序仅执行 **SHA-256 完整性检查**,这是无密钥且可重新计算的。
有效的修改固件必须更新三处:
1. **TLV `0x0014`** —— 每个分区的 SHA-256 哈希(4 × 32 字节)。当分区内容发生变化时,所有四个都必须重新计算。
2. **TLV `0x0011`** —— LZMA 流长度;重新压缩会改变它。
3. **外部 SHA-256**,作用于 `file[0x100:]`。
当这三项都正确时,修改后的固件会通过验证并正常启动。Maxwell 还是**双存储区(dual-bank)** 的 —— 刷写会写入非活动存储区,设备在重启时切换——因此刷写失败应回退到先前固件。完整步骤见 **[FLASHING.md](FLASHING.md)**。
## 文档地图
| 文档 | 内容 |
|------|------|
| **[AUDIO.md](AUDIO.md)** | 音频流水线、逐音频源平衡系统、增益寄存器、EQ —— **完整的 L/R 平衡调查在此** |
| **[FIRMWARE.md](FIRMWARE.md)** | 文件格式(LZMA-Alone)、芯片(AB1568)、内存映射、如何解压和分析 |
| **[PROTOCOL.md](PROTOCOL.md)** | RACE 协议、HID 接口、命令调度表、NVDM 密钥参考 |
| **[VERSIONS.md](VERSIONS.md)** | v1.0.1.63 和 v1.0.1.74 之间的字节级差异 |
| **[FLASHING.md](FLASHING.md)** | FOTA 流程内部 + 可用的自定义固件刷写方案 |
| **[PATCHES.md](PATCHES.md)** | 已知自定义固件补丁手册(L/R 平衡、并发播放) |
| **[BOOTLOADER.md](BOOTLOADER.md)** | 第一阶段引导加载程序(在闪存中,非 ROM)、TLV 解析、完整性检查 |
| **[COMMUNITY.md](COMMUNITY.md)** | 未解决问题、建议的前进方向、使用的工具 |
## 研究状态
| 领域 | 状态 |
|------|------|
| 固件解压缩 | ✅ 已解决(LZMA-Alone,无加密) |
| 内存映射 / 芯片识别 | ✅ 已解决(Airoha AB1568,Cortex-M4F,代码位于 `0x08000000+`) |
| 固件文件 TLV 格式 | ✅ 已映射(基本信息、移动器、SHA、版本、芯片 ID) |
| RACE 协议结构 | ✅ 已映射(调度表、子命令、NVDM 密钥) |
| 引导加载程序(闪存中的第一阶段) | ✅ 已转储(76 KB)+ 部分解码 |
| 音频混合器架构 | ✅ 已映射(增益寄存器、声道选择器、EQ 交换) |
| L/R 平衡 —— 固件机制 | ✅ 已映射(逐音频源配置文件系统、NVDM 密钥、运行时缓冲区、启动加载器、RACE 写入路径经验验证为存活的) |
| L/R 平衡 —— 逐设备根本原因 | ✅ 已映射(`0xF702` 出厂值选择非对称与对称平衡默认值;出厂配置后固件中没有任何东西会更新它) |
| L/R 平衡 —— `0xF702` 的固件内部自动切换器 | ❌ **未解决** —— 已穷尽搜索(约 50 小时),未找到。可能以我们遗漏的编码方式存在。 |
| L/R 平衡 —— 出厂配置逻辑 | ❌ **未解决** —— 什么决定了设备出厂时带有 `0xF702 = 0x00` 还是 `0x0A`。未访问 Audeze 的 QC 流程则未知。 |
| 通过 RACE 的运行时控制 | ✅ 可用(读写 L/R 平衡、EQ、chatmix持久化到 NVDM) |
| 自定义固件刷写 | ✅ **可用**(TLV `0x0014` + TLV `0x0011` + 外部 SHA-256 方案) |
| 并发播放(USB-C + BT) | 🟡 部分 —— 位于 `0x135C66` 的一个 NOP 启用了一些音频源组合;BT↔适配器需要更多工作 |
| 引导 ROM(`0x00000000` 处的真实 ROM) | ❌ 不可读 —— 对 `0x0` 的 RACE 内存读取会导致芯片崩溃;可能是一个很小的 IPL 存根 |
## 硬件
| | |
|--|--|
| **SoC** | Airoha AB1568(= MediaTek MT2822) |
| **核心** | ARM Cortex-M4F,CPUID `0x410FC241`(r0p1 + FPU),经验证存活 |
| **SDK** | Airoha IoT_SDK_for_BT_Audio v3.4.1 |
| **闪存** | 8 MB SPI XIP,位于 `0x08000000`–`0x087FFFFF` |
| **第一阶段引导加载程序** | 位于闪存 `0x08000000`–`0x08013000`(约 76 KB,可转储) |
| **活跃固件存储区** | 映射到闪存 `0x08013000`+ |
| **FOTA 分区** | 闪存 `0x084A1000`–`0x087F5000`(约 3.4 MB 非活跃存储区) |
| **SRAM** | `0x14000000`+(音频缓冲区、混合器状态、`0x142039AC` 平衡缓冲区) |
| **TCM(快速 RAM)** | `0x04000000`–`0x0402C000`(向量表 + 热路径代码) |
| **音频硬件寄存器** | `0x42000000`–`0x4200FFFF`(内存映射 I/O;读取会导致芯片崩溃) |
| **驱动单元** | 90 mm 平面磁性 |
## 许可证
本仓库中的文档采用 **CC BY 4.0**(知识共享署名)许可。您可以自由地为任何目的使用、分享和改编它们,包括商业目的——只需注明本仓库即可。
本仓库与 Audeze LLC 无关联、未获其认可或赞助。“Audeze” 和 “Maxwell” 是其各自所有者的商标。
标签:Audeze Maxwell, L/R不平衡, NVDM键值, RACE协议, USB-C, v56, v61, v74, 二进制分析, 云安全运维, 云资产清单, 固件, 固件版本, 固件逆向, 嵌入式系统, 左右声道平衡, 无线适配器, 硬件测试, 耳机, 逆向工程, 音量校正, 音频处理, 音频流水线, 音频混音器