RTOS漏洞分析(三):CVE-2026-8718和CVE-2026-9771

RTOS漏洞分析:从两个"内核验证层"漏洞出发(三)

前两篇讲的都是协议解析 :一篇是蓝牙 host/controller 的上行解析(CVE-2026-10641、CVE-2026-9263),一篇是 OTA/充电协议的下行响应解析(CVE-2026-10849、CVE-2026-10848)。四个洞的共性都是"解析器信任了远端输入的格式/长度约束"。

这一篇换个完全不同的边界:讲讲我在userspace -> kernel 拿到的两个CVE。也就是用户线程通过系统调用进入内核时,内核侧那层**验证函数(z_vrfy_* / getsockopt 的 syscall verifier)**到底验证了什么。

  • CVE-2026-9771 / GHSA-68cj-3hg4-5vpmflash_copy() 系统调用缺少设备指针校验,允许用户态权限提升(CVSS 8.8 - High)
  • CVE-2026-8718 / GHSA-p3r6-mx6c-33gqTLS_DTLS_PEER_CID_VALUE 的 getsockopt getter 忽略调用者缓冲区大小,越界写(CVSS 8.4 - High)

这两个洞可以归纳成同一句话:

内核验证层(syscall verifier)是 userspace 和 kernel 之间的"契约":验证函数负责把"用户传进来的参数"翻译成"内核可以信任的东西"。而这里存在的洞要么是验证函数忘了验证某些参数,要么是验证函数验证了、但后端实现根本没遵守。

前两篇的漏洞是"解析器信任",这一篇的漏洞是"验证层信任 "。展开讲,这两个洞正好是验证层契约的两种不同断裂方式

  1. 验证不全 (flash_copy):z_vrfy_flash_copy() 只验证了输出缓冲,漏掉了对设备指针的校验 ,把未经 k_object_validate 的对象指针直接交给实现层去解引用函数指针;
  2. 验证与实现脱节 (DTLS CID):verifier 严格按 optlen 分配了内核 bounce buffer,后端 getter 却无视 optlen,把完整的 32 字节 CID 直接写进去。

下面分开看。


一、CVE-2026-9771:flash_copy() 缺少设备指针校验

1.1 漏洞位置

drivers/flash/flash_util.c,两个函数:

c 复制代码
/* 内核验证函数:userspace 的信任边界 */
int z_vrfy_flash_copy(const struct device *src_dev, off_t src_offset,
		      const struct device *dst_dev, off_t dst_offset,
		      off_t size, uint8_t *buf, size_t buf_size)
{
	K_OOPS(K_SYSCALL_MEMORY_WRITE(buf, buf_size));
	return z_impl_flash_copy(src_dev, src_offset, dst_dev, dst_offset,
				 size, buf, buf_size);
}
c 复制代码
/* 实现函数:在超级用户模式下真正执行 flash 操作 */
int z_impl_flash_copy(const struct device *src_dev, off_t src_offset,
		      const struct device *dst_dev, off_t dst_offset,
		      off_t size, uint8_t *buf, size_t buf_size)
{
	...
	if (!device_is_ready(src_dev)) {
		return -ENODEV;
	}
	if (!device_is_ready(dst_dev)) {
		return -ENODEV;
	}

	write_size = flash_get_write_block_size(dst_dev);
	...
	for (uint32_t offs = 0, ...; offs < N; ...) {
		if (bytes_left < write_size) {
			const struct flash_driver_api *api =
				(const struct flash_driver_api *)dst_dev->api;
			const struct flash_parameters *params = api->get_parameters(dst_dev);
			...
		}
		...
		ret = flash_read(src_dev, src_offset + offs, buf, bytes_read);
		...
		ret = flash_write(dst_dev, dst_offset + offs, buf, bytes_read);
		...
	}
	return 0;
}

flash_copy() 是 Zephyr 在 4.2 引入的"闪存片间拷贝"系统调用:把 src_dev 上的一段 flash 拷到 dst_dev 上,用一个用户提供的 buf 做中间缓冲。在 CONFIG_USERSPACE 构建里,z_vrfy_flash_copy() 就是它面向用户态的信任边界。


1.2 一个"半成品"的验证函数

z_vrfy_flash_copy() 的完整验证逻辑,非常简洁就一行:

c 复制代码
K_OOPS(K_SYSCALL_MEMORY_WRITE(buf, buf_size));

它只检查了输出缓冲 buf------确认用户传入的缓冲确实是用户内存且可写。而两个最关键的参数,src_devdst_dev一行校验都没有

对比同文件里它的每一个"兄弟"系统调用(flash_read / flash_write / flash_erase / flash_get_parameters......),它们的 verifier 无一例外都先校验设备指针:

c 复制代码
/* drivers/flash/flash_handlers.c */
static inline int z_vrfy_flash_read(const struct device *dev, off_t offset,
				    void *data, size_t len)
{
	K_OOPS(K_SYSCALL_DRIVER_FLASH(dev, read));       /* <-- 有 */
	K_OOPS(K_SYSCALL_MEMORY_WRITE(data, len));
	return z_impl_flash_read((const struct device *)dev, offset,
				 (void *)data, len);
}

static inline int z_vrfy_flash_erase(const struct device *dev, off_t offset,
				     size_t size)
{
	K_OOPS(K_SYSCALL_OBJ(dev, K_OBJ_DRIVER_FLASH));   /* <-- 有 */
	return z_impl_flash_erase((const struct device *)dev, offset, size);
}

K_SYSCALL_DRIVER_FLASH(dev, op) 会展开成 K_SYSCALL_DRIVER_OP + K_SYSCALL_OBJ 的组合,作用有两个:

  1. K_SYSCALL_OBJ(dev, K_OBJ_DRIVER_FLASH):调用 k_object_validate(),确认这个指针是一个合法的 flash 内核对象 ,而且调用线程对该对象有权限
  2. K_SYSCALL_DRIVER_OP:确认 dev->api 指向的 driver API 表里,对应的操作(read/write确实存在

flash_copy 偏偏跳过了这整个校验。

一个验证函数,把"缓冲"和"对象"两类参数分开了:缓冲验证了,对象一个都没验证。这就是"验证不全"。


1.3 为什么伪造 struct device 就能 LPE

要理解这个洞的严重性,先看 flash_read / flash_write 到底怎么调用驱动:

c 复制代码
static inline int z_impl_flash_read(const struct device *dev, off_t offset,
				    void *data, size_t len)
{
	return DEVICE_API_GET(flash, dev)->read(dev, offset, data, len);
}

#define DEVICE_API_GET(_class, _dev) \
	((const struct Z_DEVICE_API_TYPE(_class) *)_dev->api)

也就是说,flash_read(dev, ...) 展开后是:

c 复制代码
((const struct flash_driver_api *)dev->api)->read(dev, offset, data, len);

这是一次通过 dev->api 的函数指针间接调用 。在正常情况下,dev->api 指向内核里静态注册的 flash_driver_api 表,没问题。但在 z_impl_flash_copy 里,src_devdst_dev用户传进来的、未经任何校验的裸指针

struct device 的布局(include/zephyr/device.h):

c 复制代码
struct device {
	const char *name;
	const void *config;
	/** Address of the API structure exposed by the device instance */
	const void *api;      /* <-- 函数指针表 */
	struct device_state *state;
	...
};

CONFIG_USERSPACE 下,用户线程完全可以 在自己的内存里伪造一个 struct device:自己分配一个对象,填上伪造的 api 指针,指向一块同样由用户控制的 flash_driver_api 结构体------里面的 readwriteget_parameters 全部是指向攻击者选定地址的函数指针。

然后把这两个伪造指针传给 flash_copy()。内核在超级用户模式下执行:

c 复制代码
device_is_ready(src_dev);                    /* 读伪造的 state->initialized,用户可控 */
api->get_parameters(dst_dev);                /* 用户可控函数指针 */
flash_read(src_dev, ...);                    /* ((flash_api*)src_dev->api)->read(...) ------ 用户可控 */
flash_write(dst_dev, ...);                   /* 用户可控函数指针 */

这就是标准的 userspace 逃逸原语

非特权线程让内核在超级用户模式下,用基本可控的参数,调用一个攻击者选定的函数指针。

而且参数也不是完全不可控的:read/write 收到的 (dev, offset, data, len) 里,data 指向已校验的用户缓冲(内核 bounce),offset/len 由攻击者的 src_offset/size 控制。攻击者可以选一个"把内存写到任意地址"之类的现有内核函数(gadget),或者直接指向一个能改写权限状态的位置。

这就是 CVE-2026-9771 的标题所在:本地权限提升(LPE) 。CVSS 8.8(AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H,Scope=Changed),因为一个"flash 驱动子系统"里的洞可以越过组件边界,把内核完全拿下来。


1.4 两级利用

这个洞其实有两个利用级别,值得分开说。

第一级:混淆代理 / 权限绕过(有真实复现)

flash_copy() 的目的就是"copy between devices"。既然 verifier 不校验设备指针,那么任何用户线程都可以拿任何 flash 设备对象来做 read/write------即使它从未被授予该设备的访问权限。攻击者不需要伪造任何东西,只要知道(或能读到)一个 flash 设备对象的内核地址,把它传进去就能用。

这是一个标准的 Confused Deputy:受信任的内核,被一个权限不足的调用者指挥着去操作它没权限碰的设备。

第二级:伪造设备指针 -> 内核态任意函数调用(CVE 标题)

更进一步,用户可以不传真实设备,而是传一个伪造的 struct device ,把 api->read / api->write / api->get_parameters 指向攻击者选定的地址。因为第一级已经证明"指针未校验",第二级是它的直接推论------把"合法的指针"换成"伪造的指针",同一个漏洞就升级成任意内核函数指针调用 / LPE

第一级证明"验证确实缺失",第二级把缺失变成代码执行。同一个缺失,两个后果。


1.5 真实 Zephyr 复现

复现跑在 Zephyr qemu_x86_64CONFIG_USERSPACE=ydrivers/flash/flash_util.c 的同路径)。PoC 的流程:

  1. 内核线程(supervisor)初始化 flash 模拟器,在 SRC_OFF 写入已知 pattern,DST_OFF 初始为全 0xff
  2. 创建用户线程,故意不把 flash 设备对象加入它的权限集
  3. 用户线程调用 flash_copy(flash_dev, SRC_OFF, flash_dev, DST_OFF, ...)
  4. 观察返回值和目标区域内容。

真实输出(flash-copy-device-perm-bypass-exp-zephyrv4.4.0-373-g4f2e63556a0f):

text 复制代码
*** Booting Zephyr OS build v4.4.0-373-g4f2e63556a0f ***
PoC: flash_copy() missing flash device permission checks
PREP_ERASE rc=0
PREP_WRITE rc=0
BASELINE_READ rc=0
BEFORE_DST ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
user: start
USER_FLASH_COPY rc=0
POST_READ rc=0
AFTER_DST a0 a1 a2 a3 a4 a5 a6 a7 b0 b1 b2 b3 b4 b5 b6 b7
EXP_SUCCESS user copied flash data without flash device permission

这四行证据链:

  1. BEFORE_DST ff ff ff:目标区域初始是全 0xff(已擦除);
  2. USER_FLASH_COPY rc=0:一个没有 flash 设备权限的用户线程 调用 flash_copy() 返回成功;
  3. AFTER_DST a0 a1 ... b7:目标区域变成了 supervisor 预先写入的 pattern;
  4. EXP_SUCCESS:判断条件 user_rc == 0 && memcmp(check, pattern) == 0 成立。

也就是说,未授权用户线程通过官方 flash_copy() 系统调用,真的把一个 flash 区域的数据拷贝到了另一个区域------权限模型被绕过了。这是第一级(混淆代理)在真实 Zephyr 里的铁证;第二级(伪造设备指针)是同一个缺失的直接升级。


1.6 修复:把漏掉的那两行补回去

修复(advisory 建议的形态)非常朴素------把兄弟 syscall 都在做的那两行校验补进 verifier:

c 复制代码
int z_vrfy_flash_copy(const struct device *src_dev, off_t src_offset,
		      const struct device *dst_dev, off_t dst_offset,
		      off_t size, uint8_t *buf, size_t buf_size)
{
	K_OOPS(K_SYSCALL_DRIVER_FLASH(src_dev, read));
	K_OOPS(K_SYSCALL_DRIVER_FLASH(dst_dev, write));
	K_OOPS(K_SYSCALL_MEMORY_WRITE(buf, buf_size));
	return z_impl_flash_copy(src_dev, src_offset, dst_dev, dst_offset,
				 size, buf, buf_size);
}

K_SYSCALL_DRIVER_FLASH(src_dev, read) 一步完成两件事:k_object_validate 校验对象合法性 + 调用者权限,同时确认 driver API 里 read 存在。这样伪造指针在任何解引用之前就被拦下。

这个洞之所以值得记一笔,是它的"对照"价值:同文件的每一个兄弟 syscall 都做了校验,唯独新增的 flash_copy 漏了。这种"复制粘贴模板时少贴一行校验"的错误,在系统调用密集的边界上特别常见。


二、CVE-2026-8718:TLS_DTLS_PEER_CID_VALUE getter 越界写

2.1 漏洞位置

subsys/net/lib/sockets/sockets_tls.c,getsockopt 的 DTLS 对端 CID 取值函数:

c 复制代码
static int tls_opt_dtls_peer_connection_id_value_get(struct tls_context *context,
						     void *optval,
						     net_socklen_t *optlen)
{
#if defined(CONFIG_MBEDTLS_SSL_DTLS_CONNECTION_ID)
	struct tls_session_context *session_ctx;
	int enabled = false;
	int ret;
	size_t optlen_local;                 /* <-- 未初始化 */

	if (!context->is_initialized) {
		return -ENOTCONN;
	}

	session_ctx = get_latest_session(context);
	if (session_ctx == NULL) {
		return -ENOTCONN;
	}

	ret = mbedtls_ssl_get_peer_cid(&session_ctx->ssl, &enabled,
				       optval, &optlen_local);
	if (enabled) {
		*optlen = optlen_local;
	} else {
		*optlen = 0;
	}
	return ret;
#else
	return -ENOPROTOOPT;
#endif
}

再看它调的 mbedtls(modules/crypto/mbedtls/library/ssl_tls.c):

c 复制代码
int mbedtls_ssl_get_peer_cid(mbedtls_ssl_context *ssl, int *enabled,
                             unsigned char peer_cid[MBEDTLS_SSL_CID_OUT_LEN_MAX],
                             size_t *peer_cid_len)
{
    *enabled = MBEDTLS_SSL_CID_DISABLED;
    ...
    if (ssl->transform_in->in_cid_len == 0 &&
        ssl->transform_in->out_cid_len == 0) {
        return 0;
    }

    if (peer_cid_len != NULL) {
        *peer_cid_len = ssl->transform_in->out_cid_len;   /* 只写、不读入 */
        if (peer_cid != NULL) {
            memcpy(peer_cid, ssl->transform_in->out_cid,
                   ssl->transform_in->out_cid_len);        /* 无条件 memcpy 满 32 字节 */
        }
    }

    *enabled = MBEDTLS_SSL_CID_ENABLED;
    return 0;
}

以及 getsockopt 的 syscall verifier(subsys/net/lib/sockets/sockets.cCONFIG_USERSPACE 时):

c 复制代码
int z_vrfy_zsock_getsockopt(int sock, int level, int optname,
			    void *optval, net_socklen_t *optlen)
{
	net_socklen_t kernel_optlen = *(net_socklen_t *)optlen;
	void *kernel_optval;

	if (K_SYSCALL_MEMORY_WRITE(optval, kernel_optlen)) {
		errno = -EPERM;
		return -1;
	}

	kernel_optval = k_usermode_alloc_from_copy((const void *)optval,
						   kernel_optlen);   /* 分配恰好 kernel_optlen 字节 */
	K_OOPS(!kernel_optval);

	ret = z_impl_zsock_getsockopt(sock, level, optname,
				      kernel_optval, &kernel_optlen);
	...
}

2.2 三段契约的错位

这个洞最好从"三段代码各自以为的契约"来看:

它以为的契约
getsockopt 协议(libc/POSIX) 调用者传 optval(缓冲)+ optlen缓冲大小 ),getter 必须写不超过 optlen 字节,并把实际写入长度写回 *optlen
syscall verifierz_vrfy_zsock_getsockopt *optlen 分配了恰好这么多字节的内核 bounce buffer,信任后端不会越界写
mbedtls get_peer_cid peer_cid_len 参数是 out-only (只写长度、不读入当容量),memcpy 无条件写满 out_cid_len(最多 MBEDTLS_SSL_CID_OUT_LEN_MAX,默认 32 字节)

Zephyr 的 getter 坐在中间,同时违反了三张契约

  1. 它没有把调用者的 *optlen(缓冲容量)作为容量传给 mbedtls------它传的是一个未初始化的局部变量 optlen_local
  2. 更关键的是,即使传了也没用,因为 mbedtls 的 get_peer_cid 根本不读这个参数当容量 ,它无条件 memcpyout_cid_len 字节;
  3. 于是调用者的缓冲(或内核 bounce buffer)被完整写满 32 字节 ,而调用者只声明了 optlen 字节。

一句话:verifier 按 optlen 建立了"缓冲只有这么大"的契约,后端却无视它直接写满。 这就是"验证与实现脱节"。


2.3 两个形态:内核态写越界 vs 用户态内核堆越界写

漏洞落成什么后果,取决于调用者是内核态还是用户态。

内核态调用者

直接 getsockopt(SOL_TLS, TLS_DTLS_PEER_CID_VALUE, optval, &optlen)optval 是内核调用者的缓冲,实际只有 optlen 字节,但 mbedtls 写满 out_cid_len(peer 协商的 CID,最大 32 字节)→ 越过调用者缓冲写 31 字节。OOB write 进相邻内存。

用户态调用者(CONFIG_USERSPACE=y

这里更有意思。看 verifier 的三行关键代码:

c 复制代码
kernel_optlen = *(net_socklen_t *)optlen;                       /* 用户传的"缓冲大小" */
kernel_optval = k_usermode_alloc_from_copy(optval, kernel_optlen); /* 内核分配恰好 kernel_optlen 字节 */
ret = z_impl_zsock_getsockopt(..., kernel_optval, &kernel_optlen);  /* 后端写满 32 字节 */

用户传入的 optlen 是几,内核就分配几字节的 bounce buffer。用户传 1 字节,内核就分配 1 字节------然后后端 getter 让 mbedtls 往里写满 32 字节。

非特权用户线程可以制造内核堆越界写 :溢出的是 k_usermode_alloc_from_copy -> z_thread_malloc 分配的内核线程堆(恰好 optlen 字节),溢出的数据是对端协商的 DTLS CID(最多 32 字节,攻击者可控制溢出条件------optlen 传多小------但不能完全控制字节内容)。

这正好解释了 CVSS 为什么给 8.4 且 Scope=ChangedAV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H):一个"TLS 套接字选项"的洞,能越过组件边界去破坏内核堆。虽然溢出的字节内容由对端 CID 决定(不能完全自由选字节),但溢出本身是确定性的、可被任何本地低权限线程触发,落在完整性(I:H)和可用性(A:H)上。


2.4 未初始化 optlen_local 的角色

这里还有个细节值得单独说:getter 里

c 复制代码
size_t optlen_local;                 /* 未初始化 */
...
mbedtls_ssl_get_peer_cid(..., optval, &optlen_local);

optlen_local 没初始化就传给 mbedtls 当 out 参数。对 mbedtls 而言无所谓(它根本不读入),但对审计者而言这是个强烈的信号:

  • 一个"长度"变量从声明到使用,没有任何赋值路径
  • 唯一的赋值发生在 mbedtls 内部(*peer_cid_len = out_cid_len);
  • Zephyr getter 从来没有用 *optlen(调用者给的容量)初始化过它。

正常写法应该是 optlen_local = *optlen; 先把手上的容量给 mbedtls,再用本地固定缓冲承接、最后按实际长度截断拷回。而这里的实现把"容量"这一信息整个丢掉了uninitialized 只是表象,真正的问题是容量信息根本没进入这条调用链


2.5 复现:baseline 和 trigger 只差一个 optlen

这个洞的复现设计得特别干净:同一个 DTLS 会话、同一个 getsockopt,只改调用者的 optlen

复现跑在 Zephyr qemu_x86_64CONFIG_NET_LOOPBACK 上起一对 DTLS client/server,两端都开 TLS_DTLS_CID,完成握手后服务端调用 getsockopt(TLS_DTLS_PEER_CID_VALUE)

baseline:optlen = 32(缓冲刚好够)

text 复制代码
baseline ret=0 errno=0 optlen=32 expected_peer_len=32
baseline buffer:
0000: 2c ee 66 6b 30 a3 60 df 2f 13 99 dd ad f6 d2 1d
0016: f2 db 73 82 3b 8d 0e 22 b9 78 eb 56 82 45 fe 92
0032: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
BASELINE_OK full-size buffer preserved canary

前 32 字节是 peer CID,第 32~47 字节的 canary(0x41)完好

trigger:optlen = 1(缓冲只有 1 字节)

text 复制代码
trigger ret=0 errno=0 optlen=32 expected_peer_len=32
trigger buffer:
0000: d7 b3 61 81 13 d3 69 2b c1 5d 7c b3 d3 ad d4 8f
0016: a6
EXP_SUCCESS trigger canary corrupted past 1-byte user buffer

关键读数:

  1. trigger ret=0:getsockopt 返回成功------即使缓冲只有 1 字节,后端也没报错;
  2. optlen=32:写回的长度是 32------后端确实写了 32 字节;
  3. 缓冲区 dump 显示 17 个字节(cid[1] + canary[16])全部被 CID 数据覆盖,canary 的 0x41 一个都不剩;
  4. EXP_SUCCESS:断言 optlen > sizeof(cid) 且 canary 被破坏,成立。

两个构建只差 optlen 这一个变量:32 时 canary 完好,1 时 canary 被 31 字节 CID 冲掉。这把这个洞压缩成一句话:

getter 完全无视调用者声明的缓冲大小;optlen 只是"写回"的载体,不是"写入"的限制。

在 qemu_x86_64 上,这是用户进程内的 1 字节缓冲被 32 字节 CID 覆盖(内核态形态);在 CONFIG_USERSPACE 构建里,同一个 getsockopt 走 verifier 时,变成内核 bounce buffer(恰好 optlen 字节)被 32 字节 CID 覆盖------内核堆越界写。


2.6 修复:用固定本地缓冲承接,再按容量截断

advisory 给的修复方向很标准:getter 不能把调用者的缓冲直接交给 mbedtls ,必须先写进一个固定大小的本地缓冲,再按 *optlen 截断拷回,同时拒绝过小的 optlen

c 复制代码
unsigned char cid[MBEDTLS_SSL_CID_OUT_LEN_MAX];
size_t cid_len = sizeof(cid);

ret = mbedtls_ssl_get_peer_cid(&session_ctx->ssl, &enabled, cid, &cid_len);
if (ret != 0) {
	return -EINVAL;
}

if (*optlen < cid_len) {
	*optlen = 0;                 /* 或返回 -EINVAL:缓冲不够 */
	return -EINVAL;
}
memcpy(optval, cid, cid_len);      /* 现在拷贝被 *optlen 约束 */
*optlen = cid_len;

关键改动是两点:本地固定缓冲承接 mbedtls 的"写满" ,以及回拷时以 *optlen 为上限 。这样无论 mbedtls 写多少,落到调用者内存里的都不会超过它声明的大小。(修复在 4.4.1 及之后,涉及 main/v4.4/v4.3/v3.7 多个分支的 backport。)

这个洞的教训是:当你的代码把一个带"容量"语义的参数交给一个"只写不读容量"的库函数时,容量信息必须在你自己这一层被强制成边界。 库函数的签名不会替你兜底。


三、两个洞放在一起:验证层契约的两种断裂

3.1 验证不全 vs 验证与实现脱节

CVE-2026-9771(flash_copy) CVE-2026-8718(DTLS CID)
边界 syscall verifier(z_vrfy_* getsockopt verifier(z_vrfy_zsock_getsockopt
断裂方式 验证不全:verifier 漏校验设备指针 验证与实现脱节:verifier 校验/分配了,后端没遵守
用户可控的"量" 伪造 struct device + 函数指针 optlen 传得多小
后果 内核态任意函数调用 -> LPE 内核 bounce buffer 越界写 -> 内存破坏
触发前提 CONFIG_USERSPACE CONFIG_MBEDTLS_SSL_DTLS_CONNECTION_ID(+ 用户态时还要 CONFIG_USERSPACE

两个洞都在回答同一个问题:验证层建立的"参数契约",在哪个环节断了?

  • flash_copy:契约根本没建立 ------src_dev/dst_dev 这两个"必须校验"的参数,verifier 一行都没写;
  • DTLS CID:契约建立了但没兑现 ------verifier 认真按 optlen 分配了 bounce buffer,后端实现直接绕过了它。

前一种错在"少做了",后一种错在"做了但不一致"。两种都是审计时最容易被"看起来有验证"骗过去的形态:flash_copy 看起来验证了缓冲(有 K_SYSCALL_MEMORY_WRITE),DTLS 看起来验证了 optlen(有 bounce buffer)------但真正的边界都在更深一层。

3.2 与前两篇的对照:解析器信任 -> 验证层信任

前两篇的四个洞,攻击面都是"远端输入 "(蓝牙包 / 服务器响应),信任的是"格式/长度约束的充分性"。这一篇的两个洞,攻击面是"本地调用者",信任的是"验证层把用户参数翻译成了可信任的内核对象/大小"。

但底层的思维方式完全一致------找一个"代码以为成立、实际未必成立"的边界假设

前两篇(解析器信任) 这一篇(验证层信任)
信任的对象 远端消息的格式/长度 验证函数的完整性/一致性
典型假设 "strncpy 会补 NUL"、"分配尺寸够用" "设备指针一定校验过"、"后端一定遵守 optlen"
突破方式 构造恶意报文 传伪造指针 / 传小 optlen
触发者 远端攻击者 本地低权限线程

对 RTOS 来说,这篇的两个洞尤其关键:CONFIG_USERSPACE 是 Zephyr 面向安全部署的核心机制,而这两个洞都在 userspace 逃逸的命门上(syscall verifier)。它们的共同教训是:

验证层不是"写了 K_SYSCALL_* 宏就算数",而是"每个进入内核的指针都必须过 k_object_validate,每个带容量语义的参数都必须被后端真正遵守"。

3.3 为什么这两个洞都是 Scope=Changed

flash_copy 是 S:C,DTLS 是 S:C------这两个洞都是"跨组件边界"的:flash 驱动子系统 / TLS 套接字子系统里的洞,能破坏内核整体。

这正是"验证层漏洞"和"解析器漏洞"的差异之一:解析器漏洞往往止步于"这个子系统崩了/漏了",而验证层漏洞天然具有越权语义 ------因为它发生在权限转换的瞬间。审计 RTOS 的 userspace 边界时,z_vrfy_* 里任何一行缺失的校验,都应该按"可能等于 LPE"来对待。


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

4.1 CVE-2026-9771(flash_copy):本地权限提升

  • 第一级(混淆代理) :任何用户线程可对任意 flash 设备做读/写/擦除------绕过设备的权限模型,即使该线程从未被授予设备对象。在真实 qemu_x86_64 上已验证。
  • 第二级(LPE) :伪造 struct device,把 dev->api->read/write/get_parameters 指向攻击者选定地址,让内核在超级用户模式下调用------越出 userspace 沙箱,任意 flash 读写、内核崩溃、潜在代码执行。
  • 价值:对暴露本地用户接口的 Zephyr 设备(受管设备、车载/工业设备),一个低权限线程能直接打穿内核。CVSS 8.8,且 advisory 提示已有利用活动迹象。

4.2 CVE-2026-8718(DTLS CID):内核堆越界写

  • 内核态形态 :调用者缓冲被 32 字节 CID 覆盖(optlen 小于 CID 长度时)。
  • 用户态形态 :verifier 的 bounce buffer 只有 optlen 字节,后端写满 32 字节------内核堆越界写 。溢出字节是 peer 协商的 CID,攻击者可自由控制"溢出多少"(传多小 optlen),但不能完全控制字节内容。
  • 价值 :本地低权限线程可触发确定性的内核堆破坏。字节内容虽不完全可控,但落在堆元数据/相邻分配上,按堆布局可能升级为代码执行。CVSS 8.4,Scope=Changed。触发前提是 CONFIG_MBEDTLS_SSL_DTLS_CONNECTION_ID + 建立过带 CID 的 DTLS 会话------对启用 DTLS CID 的 IoT/车联网设备是可远程/本地达成的。

POC说明

本文不贴完整 PoC 代码,需要的话私聊我吧。复现侧的做法提一句:

  • flash_copyzephyr-vuln-repro/flash-copy-device-perm-bypass-exp-zephyr/(真实 qemu_x86_64,CONFIG_USERSPACE=y;用户线程未授权 flash 设备仍 flash_copy() 成功并改写目标区域)。
  • DTLS CIDzephyr-vuln-repro/dtls-peer-cid-value-smallbuf-oob-exp-zephyr/(真实 qemu_x86_64,loopback 上 DTLS client/server 完成 CID 握手后调 TLS_DTLS_PEER_CID_VALUEbuild_qemu_x86_64_baseline.sh 用 32 字节缓冲、..._trigger.sh 用 1 字节缓冲,canary 证明越界)。
相关推荐
Fluxproxy3 小时前
Python请求玄学根治:彻底解决脚本间歇性超时、断连、假死问题
网络·python·安全
三8443 小时前
宝塔 Nginx 防火墙下文件包含漏洞与文件上传
安全
深念Y3 小时前
AI Agent 时代运维安全:rm 防误删方案对比
linux·运维·人工智能·安全·自动化·agent
Tom·Ge4 小时前
Harness 沙箱与安全专题:让 Agent 能干活但不闯祸
安全
Turboex邮件分享4 小时前
MX记录与邮件路由:一封邮件如何找到正确的服务器
服务器·网络协议·安全
山东科恩光电4 小时前
安全地毯在工业生产中的重要作用与选型指南
安全
李可以量化4 小时前
Redis 从了解到精通(三)上:量化交易场景下的数据备份与安全配置
redis·python·安全·qmt·ptrade
ly76895 小时前
Windows 操作系统详解:从系统架构、文件管理到安全与运维
windows·安全·系统架构
数据知道5 小时前
网络安全实战:漏洞扫描器对比——Nessus、OpenVAS、Nuclei 实战评测
网络·安全·web安全·网络安全