找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4796

积分

0

好友

623

主题
发表于 11 小时前 | 查看: 6| 回复: 0

在之前的文章里,我们聊过 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,但函数继续硬着头皮往下走!

从这段代码里,能抓出两个致命缺陷:

  1. 指针无条件混同:当 a5 == 253 时,全局指针变量 v99 被直接赋值为 v95,也就是用户态传入的 a1。之后没有任何中间层对 v99 指向的地址合法性做二次核对。
  2. 校验逻辑“形同虚设”:代码检查了 a2 < v86,但没有立即 return,而是把错误状态暂存到 v13,接着继续执行主循环。也就是说,Length0 也不会在这里被拦住。

三、触发点引爆:硬编码的 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 内部的页触摸和范围检查会直接变成空操作:

void __stdcall ProbeForWrite(volatilevoid *Address, SIZE_T Length, ULONG Alignment)
{
    if ( Length ) { // <-- 只要 Length 为 0,大门直接敞开,根本不进循环检查!
        ...
    }
}

这就形成了一个完整的逻辑闭环:沙箱内攻击者调用 NtQuerySystemInformation,伪造类 253Length=0,顶层检查放行,底层却误以为存在合法的 12 字节空间,进而向任意可写内核地址疯狂叠加计数器。


四、从 12 字节到 SYSTEM:攻击者的提权闭环

拿到 12 字节写原语后,利用链没有直接走传统内核劫持,而是玩了一套更现代的“组合拳”:

  1. 破除 KASLR:利用累加写入翻转内核中的某些状态标志位,比如 Feature_RestrictKernelAddressLeaks,把原本隐藏的内核地址指针“挤”出来。
  2. 绕过经典窃取:不修改常规进程的 Token 指针,而是直接调用 NtCreateToken 系统调用。
  3. 凭空造令牌:借助系统原生 API 的合法通道,构造一个带 SeTcbPrivilege 特权的 SYSTEM 主令牌,扣到自己头上。

至此,从浏览器沙箱出发,仅靠内核记账逻辑在“加减法”上的疏忽,最终演变成对整台 Windows 主机的完全接管。

参考:
[1] https://voidsec.com/cve-2026-40369-browser-sandbox-escape/




上一篇:Claude Code 给 DeepSeek V4 Pro 接视觉:claude-vision-skill 安装与使用全记录
下一篇:BroadcastChannel 原生跨 Tab 通信实战:别再用 localStorage 监听
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-23 12:19 , Processed in 0.854604 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表