14 KiB
Windows11 0day内核提权漏洞分析与利用(CVE-2026-62737)
写这篇文章,是想在 AI 爆发的当下,给想要挖掘 0day 的师傅或者小白提供一个思路。该漏洞为原创并本人挖掘出的漏洞,实测能在Windows11 25H2最新版本使用,后续我会提供poc源码和dump文件。
漏洞编号:CVE-2026-62737 漏洞产品:Windows 11最新版本 利用效果:本地提权 漏洞约束条件:需要一个泄露内核基址的漏洞结合才能绕过KASLR实现本地提权 poc效果:dump文件已经贴出,直接蓝屏终止。
1. 漏洞原理
一个低权限本地用户,本应无法打开受 ACL 保护的 ExecutionContext 设备,却可以借助 ndis.sys 暴露的 KLoader 代理路径绕到设备背后;随后,攻击者可把两个完全受控的 64 位值写入内核任务队列,其中一个被解释为参数,另一个被解释为函数指针。更致命的是,内核工作线程只检查这个指针是否高于 MmSystemRangeStart,便直接执行 Callback(Argument)。该内核漏洞已经被确认为高危内核提权漏洞,该漏洞存在于Windows自带的Kernel Driver,ExecutionContext.sys驱动中,是高性能I/O的CPU调度器,也是为网络栈高性能数据路径提供的上下文调度器,跟NDIS密切相关。我也会详细的讲述整体的漏洞挖掘思路和分析过程。
2. 漏洞挖掘和漏洞分析
其实在AI爆发的时间段,对安全研究,漏洞挖掘的冲击力是最大的,如果说用古法技术挖掘漏洞或者说fuzz技术上挖掘,时间更长,反而容易撞洞。该漏洞是我在研究内核模块NDIS的时候,发现了一条用户态可达的KLoader路径,随着这个路径去找,发现可以关联上ExecutionContext.sys驱动,于是针对该驱动详细的代码审计,发现了在该驱动中的任务队列机制中,IoctlExecutionContextQueueTask(IOCTL 0x22AC54)把用户输入的两个QWORD 原样写进内核任务(task->Argument、task->Callback),消费端 KernelModeExecutionContext::KernelThread 取任务后,只检查 Callback >= MmSystemRangeStart 就直接执行 Callback(Argument)。从而造成内核间接调用目标可被用户控制。实现RIP劫持,最终会导致本地提权。
在如今AI时代中,对于代码缺陷的整体调用路径已经可以让AI完全取代,不需要自己手搓poc,因为找调用关系是非常耗时间并且最终不一定能够成功构造poc,在挖掘内核0day的时候,入口点就必须是用户态低权限的整体可达内核权限的路径,随后去让AI去仔细的审计代码上的严重缺陷,针对低阶模型的审计和高阶模型的审计,不是一个量级的,举个例子,deepseek-V4-pro-预览版,glm5.2对比K3,并不是一个量级的,低阶模型需要skill和插件的引导,但高阶模型不需要,反而在高阶模型中,人的引导会降低他的思考发散程度。 接下来将详细的讲述如何一步一步挖掘到此的
2.1 起点:为什么是 NDIS,而不是某个"看起来好挖"的驱动
挖内核洞的第一步不是打开 IDA,而是选方向。筛选标准只有一条:入口必须是低权限本地用户真实可达的内核路径。管理员才能触发的洞,评级和价值都要打折;而"低权限可达"这四个字,决定了要去盯系统和用户态之间的所有交界——设备对象、ALPC、RPC、过滤驱动回调,还有就是 NDIS 这种承上启下的网络栈枢纽。
选择 NDIS 还有一个现实考虑:它是 Windows 里最老牌的组件之一,承载了 Miniport、协议驱动、Filter、NetAdapterCx 等大量第三方和微软自己的模块,模块之间的注册、代理、转发关系非常复杂。凡是"一个组件替另一个组件接待调用方"的架构,都是访问控制最容易出系统性错误的地方,因为 ACL 往往只绑在最终对象上,而代理层经常忘记重新做授权判断。这个洞最后就是出在这个问题上。
2.2 在 ndis.sys 里翻到 KLoader
在 ndis.sys 里梳理入口函数时,一组 KLoader 相关的函数引起注意:
ndisKLoaderCreateControlDeviceObject
ndisKLoaderIrpCreateHandler
ParseModuleID
顺着读下去,逻辑很清晰:NDIS 会创建 \Device\kloader 和对应的 DOS 设备符号链接,并且支持一种按 GUID 访问已注册模块的路径语法:
\\.\kloader\{GUID}
ndisKLoaderIrpCreateHandler 会解析这个路径里的 GUID(ParseModuleID 干的就是这件事),找到注册模块,把 create/IOCTL 请求转发给目标模块的设备对象。
值得停下来想的是代理层这个位置:代理层替目标模块接待调用方,如果 KLoader 不做和目标设备一致的访问检查,那么任何"本来打不开"的设备,只要它注册成了 KLoader 模块,就等于被代理从背后重新暴露给了低权限用户。
2.3 注册表坐实目标:GUID 指向 ExecutionContext
有了代理路径,下一步是看哪些模块注册在里面。注册表给出了答案。直接搜索显示的是没有权限,但键是真实存在的,用完整路径直接查子键就能读到,父键只是限制了枚举:
HKLM\System\CurrentControlSet\Control\KLoader\Modules\
{9C0B898D-6275-48EC-81B4-E5EDBE44B535}
ServiceName = ExecutionContext
打开 \\.\kloader\{9C0B898D-6275-48EC-81B4-E5EDBE44B535},低权限下成功,还能正常下发 IOCTL。
到这里,该路径就确认了:ExecutionContext 这个驱动,被 KLoader 以低权限可达的方式重新暴露了。设备有 ACL 不等于不可达,ACL 只保护了直接打开的那一条路。
2.4 枚举 IOCTL 面:从 WDF 分发函数入手
确定可达之后,进入对 ExecutionContext.sys 的代码审计。它是 WDF 驱动,入口很自然地从 EvtIoDeviceControl 开始。分发函数处理的 IOCTL 并不多,逐个列出:
排查优先级按风险排:凡是接收用户输入并创建/注册内核对象的排最前,纯配置读写的 knob 类排最后。所以第一个细读的就是 Initialize 和 QueueTask。
2.5 QueueTask:两个 QWORD 的原样写入
IoctlExecutionContextQueueTask 的反编译,核心部分如下:
用户输入缓冲区的前两个 64 位值,不经任何处理写进一个 64 字节的池对象,然后入队。单看这一段还不能定性,"写入两个 QWORD"本身不危险,危险取决于消费端怎么解释它们。所以继续追消费端。
2.6 KernelThread 里那行"唯一的检查"
消费端是 KernelModeExecutionContext::KernelThread,一个由 PsCreateSystemThread 创建的系统线程,循环从队列取任务:
对照 QueueTask 的写入布局,两个字段的语义就清楚了:
task + 0x00 = Argument → 调用时进 rcx
task + 0x08 = Callback → 间接调用目标
而执行前唯一的校验是 Callback >= MmSystemRangeStart。
问题就出在这行比较上。MmSystemRangeStart 回答的只是"这个数值落在哪段地址空间",它回答不了"这个地址是谁注册的、属于哪个模块、当前调用方有没有权让内核执行它"。数值范围检查只是一种分类,不能当成信任证明。攻击者不需要提供任何真实函数,只要给一个数值上大于 0xffff8000'00000000 的地址,驱动就会以系统线程的身份、在 PASSIVE_LEVEL 上跳过去,同时把可控的 Argument 放进 rcx。
数据流到这里就完整了:
低权限用户输入(IOCTL 0x22AC54)
→ 原样写入 task->Argument / task->Callback(无校验)
→ 内核工作线程消费(唯一的 MmSystemRangeStart 比较)
→ Callback(Argument):RIP 与第一参数同时受控
2.7 不是一处失误,同类模式还有两处
找到主路径后没有停,把同驱动里所有"用户输入 → 回调指针"的路径都过了一遍,结果发现这不是单个函数的疏忽:
● EnterKernel(Fast I/O 路径进入):同样的 MmSystemRangeStart 比较,同样的 v8(*(_QWORD *)v10) 直接调用:

● SetKmNotifications:IoctlExecutionContextRegisterNotification(0x22AC4C) 注册的节点里,callback 和 arg 都来自用户输入,注册时没有任何校验,驱动会以 callback(arg, flag) 的形式直接调用:

一个驱动里同一类设计错误出现至少三处,说明是"用户协议允许携带原始内核函数指针"这个设计本身,出现在哪个函数里,哪个函数就出问题。
3. PoC 构造
3.1 触发链:真正的坎在唤醒工作线程
静态证据链完整,不等于 BSOD 能打出来。真正的困难在于:任务入队了,但内核线程不醒。
KernelThread 先在一组事件上等待,队列事件本身并不足以让它开始消费。构造 PoC 时在这里卡了一阵,逐个条件试:
● Initialize(0x22EC40)有严格的前置校验:输入 48 字节里,+0x08/+0x10 是两个用户 buffer 指针(驱动会做 MDL 映射,必须是有效用户地址),+0x18 是线程句柄、+0x20/+0x28 是事件句柄(都过 ObReferenceObjectByHandle,必须给真实句柄)。这一关不过,上下文根本建不起来;
●上下文建好后发 QueueTask,任务确实入队了,但线程不消费;
● 转机来自 IoctlExecutionContextRegisterUserThreadMonitor(0x226C5C):它会经 NdisRegisterUserThreadExitCallback 注册一个 NDIS 用户线程退出回调。让一个短生命周期的线程去发这个 IOCTL,线程退出时回调进入 ExecutionContextUserThreadExit → FreeUserModeResources,清理路径会置位上下文事件,工作线程就这样被唤醒了,随即消费队列里那个恶意任务。
这条"线程退出唤醒"路径(记为 route B)稳定之后,触发链闭合:打开 KLoader 路径 → init 建上下文 → queue 注入任务 → monitor 线程退出唤醒 → KernelThread 执行回调。
3.2 BSOD 与转储分析
PoC 里刻意把 Callback 设为 0xFFFFF80041414141,数值上在内核范围、能通过比较,但没有任何映射,而且 41414141 在转储里一眼可辨;Argument 设为 0x1122334455667788。运行,蓝屏,分析转储:
rip = fffff800`41414141 ; 内核线程精确跳到注入的地址
rcx = 11223344`55667788 ; 注入的参数原样进入第一参数寄存器
栈:
nt!KiPageFault
0xfffff800`41414141
ExecutionContext!KernelModeExecutionContext::KernelThread+0xd7
...
nt!PspSystemThreadStartup
静态分析只能证明危险代码存在,崩溃栈才能证明低权限输入真实抵达了它。RIP 和 RCX 同时命中预设的两个标记值,证据闭环。
4. exp 利用
针对0day漏洞,这里不会给出完整的exp利用代码,只给出poc代码。单单从这个漏洞如果想实现提权还是有些限制,因为在最新版本24H2和25H2中,NtQuerySystemInformation函数,本地用户已经无法成功调用,该漏洞适合搭配一个内核基址泄露绕过KASLR去进行实现本地提权,整体思路直接献上。
exp.exe(本地低权限)
│
│ [准备] leakNtBase() ← 其他信息泄露漏洞组合,得到 ntoskrnl 基址
│ [准备] pChain = VirtualAlloc(0x2000) + VirtualLock ← ROP 链驻留用户内存(SMAP 关,内核可读)
│ [准备] 布局铺 ROP 链(含我们的 PID、槽位地址)
│
│ [触发] 与 poc.cpp 完全相同的 route B:
│ 1. CreateFileW("\\.\kloader\{9C0B898D-6275-48EC-81B4-E5EDBE44B535}", OVERLAPPED)
│ 2. 0x22EC40 INIT (userAction/kernelAction buffer、dummy 线程、eventA/eventB)
│ 3. 0x22AC54 QUEUE task.Argument = pChain (用户态地址)
│ task.Callback = nt!<pivot gadget> (mov rsp, rcx; ret)
│ 4. monitor 线程发 0x226C5C 后退出 → NDIS 回调 → FreeUserModeResources
│ → 唤醒 KernelThread → 消费任务 → 执行 Callback(Argument)
│
│ [内核侧] pivot: rsp := pChain → 纯内核 ROP(全程不碰用户代码页,SMEP 无感):
│ PsLookupProcessByProcessId(我们的PID, &slot) → slot = 我们的 EPROCESS
│ rax = [nt!PsInitialSystemProcess] → System EPROCESS
│ rax = [rax + EPROCESS.Token] → System Token(fast-ref 原值照拷)
│ [我们EPROCESS + Token] = rax → ★ Token 替换
│ PsTerminateSystemThread(STATUS_SUCCESS) → 工作线程干净退出
│
└─ [用户侧] Sleep 后 OpenProcessToken 验证 → CreateProcess("cmd.exe") → SYSTEM shell
5. 总结
在现如今的AI爆发中,0day已经是高密集的爆出,希望我的文章能够给一些想要挖掘内核漏洞的师傅提供一些思路。














