RTOS漏洞分析:CVE-2026-10641和CVE-2026-9263

RTOS漏洞分析:从两个蓝牙漏洞出发

本文想讲两件事:

  1. 漏洞是怎么找到的
  2. RTOS漏洞怎么复现

本文分析两个我拿到CVE的真实Zephyr漏洞:

  • CVE-2026-10641 / GHSA-wx5j-q6f2-59p3 :Bluetooth HFP HF +CIND 解析越界写
  • CVE-2026-9263 / GHSA-6gvp-pmh8-fjh2:Bluetooth Controller ISOAL framed RX 时间偏移处理越界读

两者虽然一个是 host classic profile parser ,另一个是 controller ISO adaptation layer parser,但根子上都是同一种问题:

trusted code 解析 untrusted input 时,把"语义上必须满足的约束"误当成"格式上大致像样就行"。

而这类错误,在 RTOS 里大量存在,有的能造成危险,因为RTOS中对象紧凑、缓冲区贴得近、错误路径少、debug 配置和 release 配置差异大,一旦进入错误状态,很容易直接落成真实内存破坏或者真实信息泄露。


一、在 RTOS 里是怎么找洞的

我感觉RTOS里我能力范围内的漏洞基本集中在以下几类边界:

  1. userspace -> kernel
  2. 协议栈 / 控制器解析器 -> 内部状态机
  3. loader / image / descriptor / metadata parser
  4. 驱动层对长度、索引、offset、callback 的信任

本文两个漏洞都属于第二类:

  • 上层 profile parser:AT + 列表语法 -> 内部状态数组
  • 底层 controller parser:framed ISO PDU -> SDU 重组状态

我看这类代码时,会优先扫三种模式:

1. "循环递增索引 + 写数组"

典型形态:

c 复制代码
while (has_next()) {
    parse_one(...);
    arr[index] = ...;
    index++;
}

如果循环停止条件来自输入,而不是来自数组大小,这就是第一优先级。

2. "先做一个看起来合理的检查,后面再做语义性减法/偏移"

典型形态:

c 复制代码
if (len >= header + field_len) {
    ...
    payload_len = field_len - mandatory_prefix;
}

如果 field_len 在语义上必须 >= mandatory_prefix,但前面的检查没体现这个条件,后面就非常容易出现整数下溢,然后变成 OOB read / OOB write。

3. "协议语义要求某字段必然存在,但代码只检查了总长度,不检查语义分支"

比如:

  • 某个 flag 为 0 时必须带额外 3 字节
  • 某个 LLID/section type 下必须是某种 layout
  • 某个 AT 子列表个数必须不超过固定上限

这类 bug 的共同点是:

代码做了"语法检查",但没做"语义检查"。

下面进入两个具体案例。


二、CVE-2026-10641:HFP HF +CIND 解析越界写

2.1 漏洞位置

文件:

  • subsys/bluetooth/host/classic/hfp_hf.c
  • subsys/bluetooth/host/classic/hfp_hf_internal.h
  • subsys/bluetooth/host/classic/hfp_internal.h

核心代码如下:

c 复制代码
static void cind_handle_values(struct at_client *hf_at, uint32_t index,
                               char *name, uint32_t min, uint32_t max)
{
    struct bt_hfp_hf *hf = CONTAINER_OF(hf_at, struct bt_hfp_hf, at);
    int i;

    for (i = 0; i < ARRAY_SIZE(ag_ind); i++) {
        if (strcmp(name, ag_ind[i].name) != 0) {
            continue;
        }

        hf->ind_table[index] = i;
        break;
    }
}

int cind_handle(struct at_client *hf_at)
{
    uint32_t index = 0U;

    while (at_has_next_list(hf_at)) {
        ...
        cind_handle_values(hf_at, index, name, min, max);
        index++;
    }

    return 0;
}

对应结构体里:

c 复制代码
int8_t ind_table[HF_MAX_AG_INDICATORS];
uint32_t hf_ind;
uint32_t ag_ind;
uint32_t ind_enable;

其中:

c 复制代码
#define HF_MAX_AG_INDICATORS 20

也就是说,ind_table 只有 20 字节 ,但 index 完全由输入控制的列表个数驱动增长,没有任何上界检查


2.2 这个洞是怎么发现的

这个漏洞其实非常"典型":

  • 解析器代码看起来很干净;
  • name/min/max 都做了正常解析;
  • 但真正危险的变量不是字符串,不是范围,而是 个数 ,列表元素的个数。

我扫这段代码时,关注点不是 strcmp(),而是:

c 复制代码
while (at_has_next_list(hf_at))

这意味着循环次数由远端提供的 +CIND: 列表长度决定。接着又看到:

c 复制代码
hf->ind_table[index] = i;
index++;

数组长度是静态 20,循环次数却来自输入,而且中间没有:

  • if (index >= ARRAY_SIZE(...)) break/return/error
  • 没有 min(list_count, HF_MAX_AG_INDICATORS)
  • 没有"只接受标准指标集合个数"的硬限制

这就已经足够判定它是一个高质量候选漏洞了。

换句话说,这不是"深挖数据流"才出来的洞,而是第一层 parser 审计就应该直接命中的模式


2.3 为什么这是漏洞

关键在结构体布局。

ind_table 后面紧跟着:

  • hf_ind
  • ag_ind
  • ind_enable

因此一旦 index >= 20,写入就会落到数组外的相邻字段上。

我在真实 Zephyr qemu_x86_64/atom 环境里观察到的布局是:

text 复制代码
LAYOUT ind_table=0x145905 hf_ind=0x14591c ag_ind=0x145920 ind_enable=0x145924

这说明当前编译布局下,ind_table[20]hf_ind 之间有 3 字节 padding。这件事非常重要:

  • 它说明漏洞一定存在
  • 但它也说明"具体打到哪个字段的哪个字节",会受编译器对齐影响。

这正是 RTOS 实战里经常被忽略的一点:

漏洞根因稳定存在,但"利用形态"要看目标架构、ABI、编译器对齐和结构体布局。

所以这里不能机械地说"第 21 个元素一定改写 hf_ind 第 0 字节",而应该先测量实际布局,再定制 payload。


2.4 利用链怎么构造

触发面

+CIND: 是 HFP HF 在 SLC 初始化中解析 AG 返回的一条 AT 响应。攻击者控制的对象不是本地 API 参数,而是:

  • 远端 AG 返回的 +CIND: 指标列表

也就是说,本质上是:

trusted Bluetooth host parser 解析 remote peer 提供的 AT 列表。

关键思路

PoC 不需要完整蓝牙链路,只要在 真实 Zephyr 里走到同一条解析路径即可。

因此我的 PoC 做法是:

  1. 构造一个真实的 struct bt_hfp_hf 对象;
  2. 注册 cind_resp 作为响应处理函数;
  3. net_buf 构造恶意 \r\n+CIND: ...\r\n\r\nOK\r\n
  4. 调用 at_parse_input() 进入真实解析逻辑;
  5. 在解析前后观测相邻字段变化。

这里的重点不是"模拟蓝牙",而是:

  • 进入的是Zephyr 原生 parser
  • 改写的是真实 Zephyr 结构体内存
  • 结果跑在 QEMU Zephyr 镜像 上,而不是宿主侧简化模型。

2.5 利用细节:为什么最后只改了 hf_ind 的最低字节

PoC 构造了超过 20 个指标项。前 20 个用于填满 ind_table,后续多出来的项继续越界写。

原始目标是把后面 4 次越界写分别写成已知值,观察是否能逐字节改写邻接字段。

但在 qemu_x86_64/atom 这个具体布局下:

  • index 202122 落在 padding 中;
  • index 23 才首次命中 hf_ind 的最低字节。

真实运行日志:

text 复制代码
STATE before hf_ind=0x11223344 ag_ind=0x55667788 ind_enable=0x99aabbcc payload_len=448
STATE parse_err=0 after hf_ind=0x11223303 ag_ind=0x55667788 ind_enable=0x99aabbcc idx20-23=3,51,34,17
EXP_SUCCESS ghsa_wx5j_q6f2_59p3_hfp_hf_cind_oobwrite

这里最关键的观测值是:

  • hf_ind0x11223344 变成 0x11223303

也就是最低字节从 0x44 被改成了 0x03

这已经足够说明:

  1. 写入确实跨出了 ind_table
  2. 写入不是"无害写 padding"这么简单;
  3. 相邻真实状态字段被成功破坏。

这就是一个已打实的 Zephyr 原生 OOB write


2.6 修复为什么是正确的

修复提交 cf7693a8261ae363c9cf46cfd51005486637173e 增加了:

c 复制代码
if (index >= ARRAY_SIZE(hf->ind_table)) {
    LOG_WRN("Invalid indicator index: %u", index);
    return;
}

这个修复很朴素,但正中根因,因为漏洞本质就是:

  • 输入驱动的列表个数
  • 越过了内部固定长度数组

它不需要改 parser 框架,不需要改状态机,只要把"列表长度必须不超过本地表容量"这个语义约束补回去就够了。


三、CVE-2026-9263:ISOAL framed RX 时间偏移处理越界读

3.1 漏洞位置

文件:

  • subsys/bluetooth/controller/ll_sw/isoal.c

关键代码分成两段看。

第一段:segment header 检查

c 复制代码
static isoal_sdu_status_t isoal_check_seg_header(struct pdu_iso_sdu_sh *seg_hdr,
                                                 uint8_t pdu_size_remaining)
{
    if (!seg_hdr) {
        return ISOAL_SDU_STATUS_ERRORS;
    }

    if (pdu_size_remaining >= PDU_ISO_SEG_HDR_SIZE &&
        pdu_size_remaining >= PDU_ISO_SEG_HDR_SIZE + seg_hdr->len) {
        return ISOAL_SDU_STATUS_VALID;
    }

    return ISOAL_SDU_STATUS_LOST_DATA;
}

第二段:framed consume 时处理 time offset

c 复制代码
uint8_t length = seg_hdr->len;
...
if (!seg_hdr->sc) {
    offset = offset + PDU_ISO_SEG_TIMEOFFSET_SIZE;
    length = length - PDU_ISO_SEG_TIMEOFFSET_SIZE;
}

其中 PDU_ISO_SEG_TIMEOFFSET_SIZE3


3.2 这个漏洞是怎么被发现的

这是非常标准的"检查和消费语义不一致"漏洞。

parser 的语义约束

在 framed ISO PDU 里:

  • 如果 sc = 0
  • 那么 segment header 后面必须 跟着 3 字节 time_offset

也就是说,语义上必须满足:

c 复制代码
seg_hdr->len >= 3

代码实际检查了什么

isoal_check_seg_header() 只检查:

c 复制代码
pdu_size_remaining >= 2 + seg_hdr->len

这最多说明:

  • "PDU 里有这么多被声明出来的 segment 长度"

但并没有说明:

  • "当 sc=0 时,这个长度里至少容得下 mandatory 的 3 字节 time_offset"

于是就出现了典型错位:

  • 前面把 len=0/1/2 判成"合法"
  • 后面又默认它一定 >=3
  • 接着做 length - 3

如果 lengthuint8_t,那么:

  • 0 - 3 = 253
  • 1 - 3 = 254
  • 2 - 3 = 255

这时"剩余负载长度"不再是小负数,而是巨大的无符号值,后面复制逻辑自然就会失控。

我看到这一组代码时,几乎可以直接下结论:

这条路径不是 assert-only 问题,而是高概率能落成真实 OOB read。


3.3 为什么这里会变成真实信息泄露

这一点比第一个漏洞更有意思,因为它不是直接 crash,而是把相邻内存当成 SDU 负载发出去

漏洞链条如下:

  1. 攻击者构造一个 framed RX PDU;
  2. 设置 sc = 0,让代码认为后面应该有 3 字节 time_offset
  3. 但把 seg_hdr->len 伪造成 0
  4. isoal_check_seg_header() 因为只看 2 + len,把它误判为有效;
  5. isoal_rx_framed_consume() 执行:
    • offset += 3
    • length -= 3
  6. length0 下溢成 253
  7. 后续 isoal_rx_append_to_sdu()pdu->payload + 5 开始,复制 253 字节 到输出 SDU;
  8. 这 253 字节里,前面一部分已经不属于声明的 PDU 负载,而是相邻内存内容。

所以这个漏洞最终形成的是:

controller 内部相邻内存 -> ISOAL 重组后的 SDU -> 上送到 host 的可见输出

这已经不是"越界读但没有外泄路径"的温和问题,而是有非常清晰的数据外流通道。


3.4 利用时最关键的一个实验设计:baseline 和 trigger 只差一个字节

这个漏洞的 PoC 设计,我刻意做成了"最小差异对照实验"。

baseline

  • seg_len = 3
  • 这是 sc=0 时的最小合法值
  • 理论上此时只有 time offset,没有实际 SDU 负载
  • 因此结果应该是:不拷贝任何字节

trigger

  • 只把 seg_len3 改成 0
  • 其他字段尽量不变

这样做的价值非常大,因为它把结论压缩成一句话:

漏洞不是复杂状态联动导致的;只要把语义上必须为 3 的字段改成 0,就能从"零拷贝"瞬间变成"253 字节越界读"。

这是一种非常强的漏洞证明方式。


3.5 真实 Zephyr 复现结果说明了什么

PoC 运行在 Zephyr native_sim/native/64,关键日志如下:

text 复制代码
BASELINE_OK no bytes copied when seg_len=3
TRIGGER_STATS write_calls=1 total_written=253 emitted_count=2 frag=253 total=253
LEAK_PREFIX KERN_SECRET_ISOAL_TIMEOFFSET
EXP_SUCCESS leaked_prefix_len=28 first_marker=KERN_SECRET_ISOAL_TIMEOFFSET

这几行日志分别证明了四件事。

第一,正常边界条件是稳定的

seg_len=3 时没有复制任何数据,说明 baseline 没问题。

第二,触发后真的发生了大尺寸异常复制

total_written=253 说明不是"多读 1~2 字节"这种边缘问题,而是下溢之后的整块错误复制

第三,复制出来的数据确实来自相邻内存

PoC 在 PDU 有效区域之后放了一个隐藏标记:

text 复制代码
KERN_SECRET_ISOAL_TIMEOFFSET

结果这个前缀真实出现在输出 SDU 里。

第四,漏洞有明确外泄语义

这一步最重要。很多越界读只能证明"读到了不该读的数据",但不能证明"数据能出去"。

而这里,数据已经进入 Zephyr ISOAL 输出路径,变成了可观察的 SDU 内容,所以它是一个有外泄价值的 OOB read


3.6 这个洞的发现过程,比数组越界更有代表性

第一个漏洞属于经典索引越界;第二个漏洞更能体现 parser 审计的核心技巧:

不要只看"有没有检查",要看"检查的条件"和"后续消费逻辑要求的语义"是不是一回事。

很多代码在代码评审里很容易过关,因为大家会下意识认为:

  • "前面已经调用 check_*() 了"
  • "既然进来了,后面减 3 没问题"

但真正危险的地方恰恰是:

  • check_*() 只检查了"报文总长度够不够"
  • 后面消费逻辑却依赖"某个语义分支下 mandatory 字段一定存在"

这类 bug 在协议解析器、镜像解析器、descriptor parser、TLV parser、relocation parser 里都非常常见。


四、两个漏洞放在一起看,真正该学到什么

这两个漏洞一个在 Bluetooth host classic profile,一个在 Bluetooth controller ISOAL,看起来跨度很大,但漏洞发现逻辑几乎一模一样。

4.1 共性一:都不是"复杂算法错误",而是"局部信任错位"

  • CIND 漏洞信任了"远端列表个数不会超过本地数组容量"
  • ISOAL 漏洞信任了"只要 segment 总长度自洽,就一定满足 time offset 语义"

都属于:

把来自外部输入的约束,错误地当成了内部不变量。

4.2 共性二:都很适合用"结构化最小 PoC"证明

  • CIND:只要构造超过 20 个指标项即可
  • ISOAL:只要把 seg_len 从 3 改成 0 即可

这说明它们不是需要非常苛刻时序和复杂环境的"脆弱漏洞",而是根因非常干净。

4.3 共性三:都必须在真实 Zephyr 环境里验证,不能只做宿主模型

这是我这次复现里最在意的一点。

如果只在宿主上写一个小 C 程序模拟:

  • 当然也能证明"这种代码模式危险";
  • 但不能证明"Zephyr 里真能打到这条路径、真能改到相邻字段、真能把数据带出去"。

所以这次我坚持:

  • CVE-2026-10641QEMU Zephyr 复现真实 OOB write
  • CVE-2026-9263native_sim Zephyr 复现真实 OOB read + leak

这两种结果都明显强于 host-side model。


五、从利用角度看,这两个洞的实际价值

5.1 CVE-2026-10641 的利用价值

它的直接效果是:

  • 远端 AT 响应驱动的内部状态结构体越界写

在我当前复现环境里,已经成功打到 hf_ind 的最低字节。进一步利用价值取决于:

  • 目标架构下的结构体布局;
  • 编译器 padding;
  • HFP HF 状态机后续如何使用这些字段;
  • 是否能结合别的逻辑路径,把状态破坏转化成更强的控制力。

所以这个洞的价值不只是"写坏一个字节",而是:

给了远端对 host profile 内部状态对象的越界写原语。

5.2 CVE-2026-9263 的利用价值

它的价值更直接:

  • 可以把 controller 相邻内存带入输出 SDU

这意味着:

  • 泄露的不只是随机垃圾;
  • 理论上可能是之前的缓冲区内容、状态字段、未初始化数据、别的控制器内部对象片段;
  • 如果 host 侧会继续处理这些 SDU,信息影响面还会扩大。

所以这是一个典型的:

低层控制器 parser 错误 -> 控制器内存外泄 -> 上层可见数据面

这类洞在协议栈里一向是高价值问题。


六、给审计者的几个实战建议

如果你也在看 RTOS / 嵌入式协议栈,我建议把下面这几条当成固定 checklist。

6.1 不要被"有检查函数"迷惑

看到 check_xxx() 不是结束,而是开始。要继续问:

  • 它检查的是语法 还是语义
  • 它检查的条件是否覆盖了后续所有分支依赖?

6.2 看到输入驱动的 index++,默认当成高危点

尤其是这种模式:

c 复制代码
while (parse_next()) {
    table[index] = ...;
    index++;
}

这类代码在驱动、协议、shell、loader、descriptor parser 里都值得优先怀疑。

6.3 对整数下溢要比整数上溢更敏感

很多人会盯着:

  • len + x
  • offset + size

但在 parser 里更容易出事的往往是:

  • len - mandatory_header
  • remaining - prefix

因为这些字段经常是 uint8_tuint16_t,一下溢就会变成大值,后面的 copy 长度直接失控。

6.4 做 PoC 时尽量设计 baseline / trigger 对照

好的 PoC 不只是"能 crash / 能泄露",还应该尽量回答:

  • 正常情况下应该发生什么?
  • 只改一个关键字节后发生了什么?

这样写出来的漏洞证明最有说服力,也最容易说服维护者接受。


七、POC说明

本文不贴完整 PoC 代码,我仔细想了想文末不附完整POC需要的话私聊吧。


八、总结

把这篇文章压缩成一句话,GPT给我的回答是:

RTOS 漏洞挖掘里,最值得花时间的地方,不是"大而全地扫代码",而是抓住那些"trusted code 解析 untrusted data"的边界,然后盯死长度、索引、offset、mandatory field 和状态机语义。

这次两个 Zephyr 蓝牙漏洞,一个落成 真实越界写 ,一个落成 真实越界读并外泄,根因都不复杂,但都非常有代表性。

也正因为根因不复杂,它们才说明一件更重要的事:

真正高质量的 RTOS 漏洞,很多时候不是藏在非常深的技巧里,而是藏在那些"大家都觉得这段 parser 看起来挺正常"的地方。

相关推荐
Dola_Zou2 小时前
以CmASIC重塑AI+边缘设备的安全与商业复利
人工智能·安全·软件工程·软件加密
小小小小钰儿2 小时前
网络安全名词术语!
安全·web安全·计算机·网络安全·黑客·编程
其实防守也摸鱼3 小时前
运维--安全与数据库
网络·数据库·学习·安全·安全威胁分析·数据库架构·软件安全
拥抱太阳06165 小时前
HarmonyOS 应用开发《掌上英语》第14篇:可空类型与安全调用:ArkTS 的非空保障策略
安全
Sirius Wu5 小时前
OpenClaw Observability 并发安全(Per-Request Hook)完整详解
服务器·网络·人工智能·安全·ai·架构·aigc
xywww1686 小时前
AWS 账号安全加固:Root 与 IAM 用户的 MFA 设置实操清单
安全·云计算·aws
头茬韭菜6 小时前
4.3 BashTool 防御链 — 七层安全防御的工程实现
安全·ai
数据知道7 小时前
BGP 劫持是怎么回事?运营商级路由安全科普
网络·安全·网络安全·智能路由器·bgp劫持
深信达沙箱7 小时前
SDC沙箱安全存储|内鬼外贼拿不走
安全·源代码防泄密·数据防泄漏·源代码加密·源代码防泄密‘’