技术分析文章,剖析 CVE-2024-20154——MediaTek MT6769 NB-IoT 基带固件中的栈缓冲区溢出漏洞,涵盖逆向工程与利用链。
分类: CWE-121 — 基于栈的缓冲区溢出
严重性: 严重(MediaTek 公告)· 8.8 高危,攻击向量:相邻(CISA-ADP)
类型: 远程代码执行 — 无需用户交互,无需事先关联
披露: MediaTek 安全公告,2025 年 1 月 6 日 https://corp.mediatek.com/product-security-bulletin/January-2025
分析目标: Samsung Galaxy A14 SM-A145R — MT6769 系列(Helio G80),属于 MediaTek 受影响芯片组列表 - 固件在安全条件下进行了仿真。
状态: 已修补。
这是我首次发表的基带研究。我的背景与电信基础设施、合法拦截中介层、stingray 和 IMSI-catcher 分析以及嵌入式设备安全相去甚远——我之前从未对蜂窝调制解调器进行过深入的固件逆向工程。我想向自己证明,结构化的分析方法论能够适应不同的目标,而对特定平台的熟悉程度可以被严谨的链路追踪所替代。NB-IoT 之所以引人注目,是因为它处于一个真正危险的交叉点:该协议专为资源受限的 IoT 设备设计,攻击面是预关联的,而且无论手机用户在做什么,调制解调器协议栈都会处理它。
当对已修补的固件进行分析并确认漏洞模式不存在时,用于在针对特定函数之前对固件进行大规模分析的 AI 系统
独立地将重建的漏洞类别、条件和受影响的固件系列与 CVE-2024-20154 的描述相匹配。
技术结论是分析师本人的。
你口袋里的手机至少包含两台独立的计算机。你与之交互的那台运行 Android。另一台——基带——完全独立运行,处理所有无线电通信,并且对上层操作系统几乎完全不可见。Android 可能已完全修补。浏览器可能已被沙箱化。用户可能从未点击过恶意链接。如果漏洞代码位于调制解调器固件中,在应用处理器介入之前就处理无线电信号,那么这一切都无关紧要。
CVE-2024-20154 正是这类漏洞。
一个畸形的 NB-IoT 系统信息广播导致 MediaTek 调制解调器固件接受攻击者控制的调度计数,将该计数通过 RRC 到 L1 的配置路径传递而从不进行钳制,最终将其用作 NB-IoT 广播信道处理程序内部栈写入循环的循环边界。当计数超过目标数组的容量时,循环会越过它们写入,触及栈上保存的寄存器,并覆盖保存的返回地址。然后该函数将损坏的值恢复到返回地址寄存器中并跳转到它。
使其严重性如此之高的原因:
该漏洞在 MediaTek 2025 年 1 月 6 日的安全公告中发布,严重性评级为严重,影响包括 LR12A 调制解调器系列在内的多个系列。Samsung 将该修复纳入其 2025 年 2 月安全维护版本。
本文不发布武器化漏洞利用,且无法根据此处发布的内容复现。 目标是展示链路在哪里断裂、为什么每一层都未能阻止它,以及当你无法将调试器附加到实时调制解调器时,负责任地验证基带漏洞需要什么。
主要目标:Samsung Galaxy A14(SM-A145R)。无线电子系统由 MT6769 芯片组系列(Helio G80)中的 MediaTek 基带处理器驱动。MT6769 系列明确列在 MediaTek 针对 CVE-2024-20154 的受影响芯片组列表中。``` AP/CP firmware: A145RXXU1AWD1 Modem software: MOLY LR12A.R3.TC10.6M.A14.PR.SP.V1.P5 Build date: 2023-04-18
基带固件不是 Android 代码。它是 SoC 无线电子系统上的一个独立嵌入式系统,拥有自己的 CPU、自己的 RTOS 和自己的内存空间,位于 Android 进程沙箱之外。
### 2.2 调制解调器架构
对提取出的二进制文件的分析表明,调制解调器处理器运行 MIPS32,采用小端模式下的 MIPS16e2 压缩指令。MIPS16e2 是一种用于嵌入式代码体积缩减的 16 位编码扩展——这与联发科针对 Helio 代基带的方法一致,并得到了针对该 SoC 系列的独立公开基带研究的证实。
操作系统是 Nucleus RTOS,提供任务调度、IPC 消息队列和基于池的内存分配器。没有内核/用户特权分离,任务之间没有内存保护单元强制隔离,也没有硬件栈保护机制。
本文中的所有地址均为虚拟地址,在 Ghidra 中以基址 `0x90000000` 加载。
### 2.3 缓解措施(在分析的构建版本中观察到)
| 缓解措施 | 状态 | 效果 |
|---|---|---|
| ASLR | 缺失 | 固件地址是静态的,可从镜像中预测 |
| 栈金丝雀 | 缺失 | `SAVE`/`RESTORE` 存储被调用者保存寄存器时没有保护值 |
| NX / W^X | 缺失 | 栈内存可执行 |
| CFI | 缺失 | 返回地址未根据任何策略进行验证 |
### 2.4 分析方法
三条并行路径:
**静态分析。** 三星固件包 → CP 分区提取 → `md1img.img` → Ghidra(MIPS LE 32 位,基址 `0x90000000`),并使用 NCC Group 的 `mtk_bp` 工具集从固件调试段中恢复联发科工程符号。
**动态验证。** 使用 Unicorn Engine(MIPS32 仿真)分两个阶段隔离执行特定的固件例程。阶段 1 试图通过原生指令对证明 `si_count` 未经限制地复制到通道上下文中。阶段 2 在真实固件字节上执行了存在漏洞的循环,并确认固件自身的指令破坏了保存的返回地址。在阶段 1 无法完全原生运行的情况下——因为 CPHY 分发路径所需的 RTOS 服务对象环境未被重建——副作用被直接建模,并在所有输出中如此标注。
**无线电侧验证。** 使用带 ZMQ 回环的 srsRAN 4G——纯软件,无射频发射——确认测试载荷能够在 NB-IoT PHY 编码和传输块传递后存活。
---
## 3. 攻击面:NB-IoT 与 SIB1-NB
### 3.1 关联前攻击面
NB-IoT(窄带物联网)是 3GPP Release 13,旨在利用现有授权 LTE 频谱连接资源受限的 IoT 设备。它被实现在包括消费级智能手机在内的多种现代蜂窝 SoC 中。
在 RRC_IDLE 状态下,在任何 RRC 连接建立之前,搜索服务的设备将:
1. 与小区的时间信号同步(NPSS/NSSS)
2. 通过 NPBCH 解码主信息块(640 ms 传输窗口)
3. 从 NPDSCH 解码 SIB1-NB(2560 ms 调度)
4. 使用 SIB1-NB 中的调度信息定位其他系统信息块
在第 3 步,调制解调器在任何连接或用户交互之前,处理来自一个它尚未认证的实体的消息。满足正常小区选择条件的恶意发射机将被处理。```
+------------------+ +---------------------+
| Rogue Base Stn | | Target UE (Modem) |
+--------+---------+ +----------+----------+
| |
| NPSS/NSSS sync |
|--------------------------------------->|
| MIB-NB (640 ms cycle) |
|--------------------------------------->|
| SIB1-NB (malformed, si_count > 8) |
|--------------------------------------->| ← vulnerability triggered
| [no RRC connection established] |
SIB1-NB 定义于 3GPP TS 36.331。其 schedulingInfoList 字段携带小区广播的系统信息消息数量,规范将其限制为最多 8 个条目(1..maxSI-Message-NB-r13 = 8)。这是协议层约束。内存安全约束——即列表长度不得超过目标数组的容量——必须由固件单独强制执行。
但事实并非如此。
固件从三星 CP 包中获取,并使用 NCC Group 的 mtk_bp 工具集进行提取:```
md1img.img → md1_extract.py → 000_md1rom (17.8 MB code image)
→ 017_md1_dbginfo (XZ-compressed CATI debug symbols)
CATI 调试段通过 `mtk_dbg_extract.py symbols` 解压并解析,随后通过 `ImportSymbolsScript.py` 导入 Ghidra。结果是在整个调制解调器栈中获得了完整的内部函数名——ERRC 层、L1 信道管理、IPC 子系统以及 NB-IoT BCCH 处理链——从而支持语义引导的链重建。
本文中所有函数名均来自从固件镜像中提取的 MediaTek 自身嵌入的调试符号。
---
## 5. 漏洞
### 5.1 存在漏洞的循环
`el1_ch_nbcch_resume_req`(`0x90213940`)处理 NB-IoT 广播信道恢复事件。其 MIPS16e2 函数序言:```asm
90213940: save 0xE8, ra, s0-s1
SAVE 指令将 sp 递减 0xE8,并向下存储被调用者保存的寄存器:```
old_sp (= new_sp + 0xE8)
new_sp + 0xE4 saved ra ← overflow target
new_sp + 0xE0 saved s1
new_sp + 0xDC saved s0
new_sp + 0x98 si_sched_arr [34 halfwords = 68 bytes]
new_sp + 0x78 si_type_arr [32 bytes]
new_sp + 0x00 ← stack pointer after SAVE
从实际固件二进制文件的 Ghidra 反编译结果来看:```c
for (uVar6 = 0; uVar6 < (byte)param_2[0x40a]; uVar6 = uVar6 + 1) {
si_type_arr[uVar6] = /* SI type byte */; // 1 byte/iter, base new_sp+0x78
si_sched_arr[uVar6] = /* SI schedule halfword */; // 2 bytes/iter, base new_sp+0x98
}
param_2[0x40a] 是 ch_ctx[+0x40A] —— 通道上下文 BSS 结构体中的一个持久字节。
循环边界被直接使用,没有事先与数组容量进行比较。
流 A(半字 sh 写入)从 new_sp+0x98 开始,每次迭代前进 2 字节。
它在第 38 次迭代时到达位于 new_sp+0xE4 的保存 RA:```
new_sp + 0x98 + i×2 = new_sp + 0xE4
i = (0xE4 - 0x98) / 2 = 0x4C / 2 = 38
流 B(字节 `sb` 写入)从 `new_sp+0x78` 开始,需要迭代 108 次才能到达 RA 槽位:```
new_sp + 0x78 + i = new_sp + 0xE4
i = 0xE4 - 0x78 = 108
当 si_count = 40(演示值,选择该值是为了超过 38 的溢出阈值)时,
循环运行 40 次迭代。Stream B 永远不会到达 RA 槽位。RA 损坏完全
来自 Stream A。
| 迭代 | sh 写入到 | 效果 |
|---|---|---|
| 0–33 | si_sched_arr[0..33] | 在边界内 |
| 34–35 | new_sp+0xDC — 保存的 s0 | s0 被损坏 |
| 36–37 | new_sp+0xE0 — 保存的 s1 | s1 被损坏 |
| 38 | new_sp+0xE4 — 保存的 ra [15:0] | RA 低半字 |
| 39 | new_sp+0xE6 — 保存的 ra [31:16] | RA 高半字 |
经过 40 次迭代后,MIPS16e2 RESTORE 指令从栈中重新加载被损坏的值
到 $ra,然后 jrc ra 转移控制权。
ch_ctx[+0x40A] 由 el1_ch_nbcch_start 在 0x90213444 处写入。两条连续的 MIPS
指令,它们之间没有任何内容:```asm
; el1_ch_nbcch_start @ 0x90213444
lbu v0, 0x99(s0) ; read IPC_msg[+0x99] = si_count from CPHY_CFG_REQ
sb v0, 0x40A(s1) ; write to ch_ctx[+0x40A] — no clamp, no mask, no compare
### 5.4 ERRC 构建器 — 层 A
CPHY_CFG_REQ 缓冲区在 ERRC 层中由解码后的 SIB1-NB 构建。函数
`errc_chm_l1_set_bcch_si_reception` 写入 `IPC_msg[+0x99]`:```c
// errc_chm_l1_set_bcch_si_reception — Layer A
// Loop bound: decoded schedulingInfoList entry count from SIB1-NB
while (bVar3 < *(byte *)(param_3 + 0x44) && (param_2 != 0)) {
bVar3++;
if (uVar2 != param_2) {
*param_5 = *param_5 + 1; // CPHY_CFG_REQ[+0x99]++ — no upper bound check
}
}
循环边界是来自 SIB1-NB 的解码条目数。计数器每解码一个条目递增一次,递增次数等于已解码的条目数,没有最大限制保护。
一旦 CPHY_CFG_REQ 缓冲区被填充,ERRC 会将其分发给 L1,并使用 sap_id = 0x501F 作为路由键:```
el1_ch_rcv_ilm (sap 0x501F)
→ el1_chmgm_errc_cfg_req_in_idle ← stores CPHY_CFG_REQ @ L1_ctx[+0x323C]
→ el1_chmgm_nbcch_handler
→ el1_ch_nbcch_main
→ el1_ch_nbcch_cphy_cfg_req_process ← validates cell identity, no si_count check
→ el1_ch_nbcch_start @ 0x90213444 ← Layer B: lbu + sb, no clamp
### 5.6 三层缺失的钳位
| 层级 | 函数 | 地址 | 是否存在钳位? |
|---|---|---|---|
| A — ERRC 构建器 | `errc_chm_l1_set_bcch_si_reception` | ERRC 范围 | **无** |
| B — L1 拷贝 | `el1_ch_nbcch_start` | `0x90213444` | **无** |
| C — L1 循环 | `el1_ch_nbcch_resume_req` | `0x90213940` | **无** |
任何一层中的任何单一检查都会打破这条链。
### 5.7 根本原因
一个被违反的不变量:
> SI 调度条目的数量绝不能超过目标数组的容量。
3GPP 提供了预期的协议边界(8 个条目)。固件需要在每一层强制实施内存安全边界,即计数变为索引或循环限制的地方。在易受攻击的构建中,计数从 SIB1-NB 广播字段出发,经过 ERRC 解码器,进入 CPHY_CFG_REQ 消息,跨越 IPC 边界进入 L1 任务,进入通道上下文 BSS,并进入一个栈写入循环——没有任何一层对其进行钳位。
---
## 6. 调用链——如何被发现
### 6.1 两条路径的混淆
两条结构相似但不同的路径可以将一个 0x760 字节的配置缓冲区传递给 `el1_ch_nbcch_main`: