首页
社区
课程
招聘
[原创] HEVD03-07
发表于: 4天前 932

[原创] HEVD03-07

4天前
932

前言

本文将围绕 HEVD 中的两种漏洞类型展开,它们均与非分页内存池有关:释放后使用(UAF)与池溢出。在低版本系统上,这类漏洞的利用相对直接;对于高版本下的利用方式,我原本也有一些初步的设想,但仔细考虑后,觉得有必要说明一下——HEVD 毕竟是一个模拟环境,而在真实场景中,针对内核池的利用几乎总是围绕真实的内核对象展开,很少有机会让我们主动申请一块池内存来存放受控数据(不考虑 BYOVD 的情况)。既然这是学习环境,我们就不在复杂的版本对抗上过多纠缠了,先把基础手法梳理清楚。

UAF

漏洞成因

NTSTATUS
FreeUaFObjectNonPagedPool(
    VOID
)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    PAGED_CODE();
    __try
    {
        if (g_UseAfterFreeObjectNonPagedPool)
        {
            ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
            Status = STATUS_SUCCESS;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
    }
    return Status;
}

NTSTATUS
UseUaFObjectNonPagedPool(
    VOID
)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    PAGED_CODE();
    __try
    {
        if (g_UseAfterFreeObjectNonPagedPool)
        {
            if (g_UseAfterFreeObjectNonPagedPool->Callback)
            {
                g_UseAfterFreeObjectNonPagedPool->Callback();
            }
            Status = STATUS_SUCCESS;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

全局池块在释放后并没有对指针进行清零,我们仍然可以在UseUaFObjectNonPagedPool中使用,所以,我们使用AllocateFakeObjectNonPagedPool把刚刚释放的池块重新申请出来,覆盖Callback指针为我们用户态的shellecode,完成利用。

高版本思考

  1. windows内核的非分页内存池是可以执行的,我们只需要把shellcode放在我们申请出来的内核池对象上,想办法泄露池块地址就行。
  2. 在我的上一篇文章,我发现NtQuerySystemInformation函数在win11 24H2之前几乎可以泄露所有的内核对象地址,结合本题思考,我们在较高版本下,仍然是可以利用的。

非分页内存池溢出

漏洞成因

NTSTATUS
TriggerBufferOverflowNonPagedPool(
    _In_ PVOID UserBuffer,
    _In_ SIZE_T Size
)
{
    PVOID KernelBuffer = NULL;
    NTSTATUS Status = STATUS_SUCCESS;
    PAGED_CODE();
    __try
    {
        DbgPrint("[+] Allocating Pool chunk\n");
        KernelBuffer = ExAllocatePoolWithTag(
            NonPagedPool,
            (SIZE_T)POOL_BUFFER_SIZE,
            (ULONG)POOL_TAG
        );

        if (!KernelBuffer)
        {
            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%zX\n", (SIZE_T)POOL_BUFFER_SIZE);
            DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);
        }

        ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));

        DbgPrint("[+] Triggering Buffer Overflow in NonPagedPool\n");
        RtlCopyMemory(KernelBuffer, UserBuffer, Size);

        if (KernelBuffer)
        {
            ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);
            KernelBuffer = NULL;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }
    return Status;
}

很明显,我们可以申请一个总大小为0x200的池块,然后进行池溢出。
这个低版本下的利用思路前辈们https://bbs.kanxue.com/thread-252665-1.htm#msg_header_h1_3已经讲的很详细了,我总结一下吧:

  1. 首先是对零页内存的巧妙利用,申请地址空间为零的内存,填充我们的shellcode。
  2. 对于TypeIndex为0的对象(原本不存在),我们就能劫持他的Close指针。
  3. 申请0x1000个Event对象,每个对象会在内核申请0x40的非分页内核池空间。
  4. 释放8个Event对象,触发合并,合并的池块大小为0x200
  5. DeviceIoControl实现池溢出,通过调试修改下面的池块(也就是Event)的_OBJECT_HEADER的TypeIndex为0.
  6. CloseHandle关闭Event句柄,执行ObjectType的关闭例程。因为我们设置的TypeIndex是0,执行的是我们的Shellcode

高版本思考

看了一些文章,高版本的内核池结构将变得非常复杂,【译】Scoop the Windows 10 Pool - 敬渊's Blog倾向于转化为任意地址递减漏洞,从而获得SeDebugPrivilege权限。
在我的文章[原创] 学习笔记:CVE-2014-1767漏洞利用思路与我的理解-二进制漏洞-看雪安全社区|专业技术交流与安全研究论坛中也有类似的思路,总的来说,就是利用内核对象保存的指针,获得在内核任意读写的能力,完成利用.

代码

放附件了,Github


[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

上传的附件:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回