代码审计视角下的 CVE-2026-40369:12 字节内核写与沙箱逃逸的底层触发点剖析
Ots安全
2026年8月21日 18:18
广东
在小说阅读器读本章
去阅读
威胁简报
恶意软件
漏洞攻击
在之前的文章里,我们聊了 CVE-2026-40369 这个漏洞的宏观影响——一个从浏览器的低权限沙箱一路通往 SYSTEM 权限的“满分跳板”。但对于我们这些技术人来说,光知道“能提权”是远远不够的。这 12 字节的内核写原语在汇编层到底长什么样?为什么 ProbeForWrite 偏偏在这个函数里形同虚设?
今天咱们就卷起袖子,直接上 Hex-Rays 反编译出来的内核源码,从代码审计的视角,把这个漏洞的触发点和调用链彻底盘个底朝天。
一、 调用链跟踪:从 NtQuerySystemInformation 到底层工作函数
当我们在用户态发起系统调用时,整个执行流会从 nt!NtQuerySystemInformation 一路下沉。通过对 Windows 11(build 26100.8246)内核符号的逆向分析,其核心调用路径非常清晰1:
NtQuerySystemInformation(SystemInformationClass, SystemInformation, Length, ReturnLength) -> nt!NtQuerySystemInformation -> nt!ExpQuerySystemInformation (负责参数分发与头部的基本 Probe) -> nt!ExpGetProcessInformation (核心进程遍历工作线程,包含未校验写)
漏洞的核心引爆点,就藏在 nt!ExpGetProcessInformation 这个负责收集进程信息的内部函数中。当传入的信息类(Information Class)为 253 时,函数内部会进入一段特殊的处理逻辑。
二、 核心源码审计:那个让校验失效的“零长度”陷阱
我们直接来看 nt!ExpGetProcessInformation 在处理类 253 时的关键反编译代码片段1:
NTSTATUS __fastcall ExpGetProcessInformation( __int64 a1, // 用户态传入的 SystemInformation 指针(完全由调用者控制) unsignedint a2, // 传入的 Length 缓冲区长度 _DWORD *a3, // 返回长度指针 _DWORD *a4, // 会话过滤参数 int a5) // 信息类 (Class, 此处为 253){ unsignedint *v85, *v95, *v99; ... v95 = (unsignedint *)a1; ... if ( a5 == 253 ) { v77 = 0; v86 = 12; // 固定的目标写入长度:12 字节 v71 = 12; v87 = 0; v99 = v95; // <-- 关键点:v99 直接指向了用户态传入的指针 a1! v85 = NULL; goto LABEL_11; } ...LABEL_11: ... /* 长度校验段:仅仅设置状态,但不进行提前返回! */ v97 = v86; v11 = a2 < v86; if ( a2 < v86 ) { if ( !a3 ) return STATUS_INFO_LENGTH_MISMATCH; v11 = a2 < v86; } v13 = v11 ? STATUS_INFO_LENGTH_MISMATCH : 0; // 状态被置为 MISMATCH,但函数继续硬着头皮往下走!
从这段代码里,我们可以敏锐地抓出两个极其致命的设计缺陷:
- 指针无条件混同:当
a5 == 253时,全局指针变量v99被直接赋值为了v95(即用户态传入的指针a1)。在这之后,没有任何中间层去对v99指向的物理或虚拟地址合法性进行重新核对。 - 校验逻辑“形同虚设”:虽然代码检查了
a2 < v86(即用户传入的缓冲区长度是否小于 12 字节),但它并没有选择立即return错误码,而是把错误状态暂存到v13里,随后继续执行主循环。也就是说,即便你传进来的Length是0,代码也不会在这里被拦住。
三、 触发点引爆:硬编码的 12 字节任意内核写
当代码流进入主循环、遍历完系统的进程链表后,真正触发崩溃和任意写的汇编现场出现了:
if ( a5 == 253 ) { v25 = v99; ++*v99; // 写入 #1: [target + 0] 自增 1 v25[1] += PsGetProcessActiveThreadCount((__int64)NextProcess); // 写入 #2: [target + 4] 累加线程数 v25[2] += ObGetProcessHandleCount((struct _EX_RUNDOWN_REF *)NextProcess, 0LL); // 写入 #3: [target + 8] 累加句柄数 }
这就是整个漏洞的绝对触发点:
- 第一次写入:对目标地址的内容执行
++*v99(4 字节递增)。 - 第二次写入:在目标地址偏移
+4的位置,加上当前遍历进程的活跃线程数(4 字节累加)。 - 第三次写入:在目标地址偏移
+8的位置,加上当前进程的句柄数(4 字节累加)。
加起来刚好是12 字节的内核写入原语。
为什么在顶层的 ExpQuerySystemInformation 里没有拦住它?因为在上层分发时,通用的 ProbeForWrite 检查是用用户传入的 Length(比如设为 0)去跑的。而当 Length == 0 时,ProbeForWrite 内部的页触摸和范围检查会直接变成空操作(No-op):
void __stdcall ProbeForWrite(volatilevoid *Address, SIZE_T Length, ULONG Alignment){ if ( Length ) { // <-- 只要 Length 为 0,大门直接敞开,根本不进循环检查! ... }}
这就形成了一个完美的逻辑闭环:攻击者在沙箱内调用 NtQuerySystemInformation,精心伪造类 253 且 Length=0,顶层检查放行,底层误以为有合法的 12 字节空间,直接往任意可写的内核地址(甚至是通过某种方式构造的虚拟地址)上疯狂叠加计数器。
四、 从 12 字节到 SYSTEM:攻击者的提权闭环
拿到这个 12 字节的写原语之后,利用链并没有直接去搞传统的内核劫持,而是玩了一套非常现代的“组合拳”:
- 破除 KASLR:利用这几次累加写入,去翻转内核中的某些状态标志位(如
Feature_RestrictKernelAddressLeaks),把原本藏得严严实实的内核地址指针给“挤”出来。 - 绕过经典窃取:攻击者没有去修改常规进程的 Token 指针,而是直接呼叫
NtCreateToken系统调用。 - 凭空造令牌:在内存中利用系统原生 API 的合法通道,硬生生“捏”出一个带有
SeTcbPrivilege特权的 SYSTEM 主令牌,并把它拍到自己头上。
至此,一个从浏览器沙箱出发,仅仅依靠内核记账逻辑在“加减法”上的疏忽,最终演变成了对整台 Windows 主机的完全接管。
已关注
关注
重播 分享 赞
关闭
观看更多
更多
退出全屏
切换到竖屏全屏退出全屏
Ots安全已关注
分享视频
,时长00:33
0/0
00:00/00:33
切换到横屏模式
继续播放
[ ]
进度条,百分之0
播放
00:00
/
00:33
00:33
倍速
全屏
倍速播放中
0.5倍0.75倍1.0倍1.5倍2.0倍
超清流畅
继续观看
代码审计视角下的 CVE-2026-40369:12 字节内核写与沙箱逃逸的底层触发点剖析
观看更多
转载
,
代码审计视角下的 CVE-2026-40369:12 字节内核写与沙箱逃逸的底层触发点剖析
Ots安全已关注
分享点赞在看
已同步到看一看写下你的评论
视频详情
参考:
- https://voidsec.com/cve-2026-40369-browser-sandbox-escape/
END
公众号内容都来自国外平台-所有文章可通过点击阅读原文到达原文地址或参考地址
排版 编辑 | Ots 小安
采集 翻译 | Ots Ai牛马
公众号 | AnQuan7 (Ots安全)
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论