Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

Win11最新最潮SSTD hook手法 - HellsHollow

本文更倾向于学习该技术后对本项技术发现者0xflux对该技术的介绍原文的__翻译__, 因此会省略发现该技术的过程, 直接进入原理和利用的部分

技术原理

用IDA打开24H2 Windows11 的ntoskrnl.exe, 来到发起系统调用后内核 routine 入口KiSystemServiceUser:

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
60
61
62
63
64
65
66
// local variable allocation has failed, the output may be wrong!
void __fastcall KiSystemServiceUser(double a1, double a2, __int64 a3, __int64 a4)
{
__int64 v4; // rbp
__int64 v5; // r10
__int128 v6; // xmm2
__int128 v7; // xmm3
__int128 v8; // xmm4
__int128 v9; // xmm5
struct _KTHREAD *CurrentThread; // rbx
bool v11; // zf
char v12; // al
__int64 SystemCallNumber; // rax
void *FirstArgument; // rcx
KTRAP_FRAME KTRAP_FRAME_; // [rsp+0h] [rbp+0h] BYREF

*(_BYTE *)(v4 - 85) = 2;
*(_BYTE *)(v4 - 88) = 1;
CurrentThread = KeGetCurrentThread();
CurrentThread->PreviousMode = 1;
_m_prefetchw(&CurrentThread->TrapFrame);
*(_DWORD *)(v4 - 84) = _mm_getcsr();
_mm_setcsr(KeGetPcr()->Prcb.MxCsr);
*(_QWORD *)(v4 - 56) = a3;
*(_QWORD *)(v4 - 48) = a4;
*(_QWORD *)(v4 - 32) = v5;
*(_QWORD *)(v4 - 40) = v5;
*(_OWORD *)(v4 - 16) = *(_OWORD *)&a1;
*(_OWORD *)v4 = *(_OWORD *)&a2;
*(_OWORD *)(v4 + 16) = v6;
*(_OWORD *)(v4 + 32) = v7;
*(_OWORD *)(v4 + 48) = v8;
*(_OWORD *)(v4 + 64) = v9;
KiSynchronizeUserIsolationDomainExit();
v11 = CurrentThread->Header.DebugActive == 0;
*(_WORD *)(v4 + 128) = 0;
if ( !v11 )
{
if ( (CurrentThread->Header.Reserved1 & 3) != 0 )
KiSaveDebugRegisterState();
if ( (CurrentThread->Header.DebugActive & 0x24) != 0 )
{
_enable();
CurrentThread->TrapFrame = (_KTRAP_FRAME *)&KTRAP_FRAME_;
v12 = PsSyscallProviderDispatch((_KTRAP_FRAME *)&KTRAP_FRAME_);
if ( v12 != 1 )
{
if ( v12 >= 1 )
{
KiExceptionDispatch(3221225500LL, 0, *(_QWORD *)(v4 + 232));
__debugbreak();
}
if ( (CurrentThread->Header.Reserved1 & 4) != 0 )
JUMPOUT(0x140686AABLL); // KiSystemServiceExitPico
JUMPOUT(0x140686660LL); // KiSystemServiceExit
}
}
}
SystemCallNumber = *(_QWORD *)(v4 - 80);
FirstArgument = *(void **)(v4 - 72);
_enable();
CurrentThread->FirstArgument = FirstArgument;
CurrentThread->SystemCallNumber = SystemCallNumber;
CurrentThread->TrapFrame = (_KTRAP_FRAME *)&KTRAP_FRAME_;
JUMPOUT(0x1406864D4LL); // KiSystemServiceRepeat
}

关注其中的PsSyscallProviderDispatch分支, 当发起系统调用的线程的_DISPATCHER_HEADER Header(offset = 0)成员的Altsyscall字段被置位后系统调用将进入PsSyscallProviderDispatch()routine, 这是启用Alternative Syscall的__第一个__条件

而这条线导向了让系统调用进入用户逻辑的关键入口:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
__int64 __fastcall PsSyscallProviderDispatch(_KTRAP_FRAME *KTRAP_FRAME)
{
__int64 v1; // rdx
__int64 v2; // r8
__int64 v3; // r9
struct _KTHREAD *CurrentThread; // rbp
__int64 result; // rax
_EPROCESS *_EPROCESS; // rax
unsigned int SSN; // edi
__int64 slot; // rcx
ServiceDescriptorRow *PspServiceDescriptorRow; // r15
AltSyscallDispatchTable *dispatchtable; // rbx
__int64 _ssn; // r14
AltSyscallDispatchDescriptor descriptor; // ebx
int v14; // eax
unsigned int v15; // ecx
unsigned __int64 v16; // rdx
char *callback_addr; // rdx
unsigned __int64 v18; // [rsp+70h] [rbp+18h] BYREF

v18 = 0;
CurrentThread = KeGetCurrentThread();
if ( (CurrentThread->Header.DebugActive & 4) != 0 )
{
PsPicoSystemCallDispatch(KTRAP_FRAME, v1, v2, v3);
return 0;
}
_EPROCESS = (_EPROCESS *)IoThreadToProcess(CurrentThread);
SSN = KTRAP_FRAME->Rax & 0xFFFF9FFF;
slot = _EPROCESS->SyscallProviderDispatchContext.Slot;
if ( (unsigned int)slot >= 0x20 )
KeBugCheckEx(0x1E0u, 5u, (unsigned int)slot, (ULONG_PTR)_EPROCESS->SyscallProvider, 0);
PspServiceDescriptorRow = &PspServiceDescriptorGroupTable[slot];
dispatchtable = (&PspServiceDescriptorRow->ssn_dispatch_table)[(SSN >> 12) & 7];
if ( !dispatchtable )
return 1;
if ( (KTRAP_FRAME->Rax & 0xFFF) >= dispatchtable->count )
{
KTRAP_FRAME->Rax = 0xC000001CLL;
return 0;
}
_ssn = KTRAP_FRAME->Rax & 0xFFF;
*(_QWORD *)&descriptor = (unsigned int)dispatchtable->descriptors[_ssn];
if ( descriptor )
{
if ( descriptor != 1 )
{
if ( (KTRAP_FRAME->Rax & 0x1000) == 0x1000
&& (v14 = PspEnsureGuiThreadAndBatchFlush(CurrentThread), v15 = v14, v14 < 0) )
{
if ( v14 == 0xC0000001 )
{
v15 = *(char *)(_ssn + 4LL * (unsigned int)xmmword_140FC52B0 + xmmword_140FC52A0);
if ( v15 == 1 )
v15 = -1073741796;
}
v16 = v15;
}
else
{
callback_addr = (char *)PspServiceDescriptorRow->driver_base
+ ((*(_QWORD *)&descriptor >> 4) & 0xFFFFFFFFFFFFFF0LL);
if ( (*(_BYTE *)&descriptor & 0x10) != 0 )
{
result = PspSyscallProviderServiceDispatchGeneric(
KTRAP_FRAME,
(__int64)callback_addr,
*(_BYTE *)&descriptor & 0xF,
SSN,
&v18);
if ( (_DWORD)result )
return result;
v16 = v18;
goto LABEL_22;
}
v16 = PspSyscallProviderServiceDispatch(KTRAP_FRAME, callback_addr, *(_BYTE *)&descriptor & 0xF);
}
result = 0;
LABEL_22:
KTRAP_FRAME->Rax = v16;
return result;
}
return 2;
}
else
{
return 1;
}
}

PsSyscallProviderDispatch首先检查了发起系统调用的线程的所属进程 0x7d0 处的成员SyscallProviderDispatchContext, 它的类型为_PSP_SYSCALL_PROVIDER_DISPATCH_CONTEXT, 只有两个成员, 这里检查的是第 2 个Slot, 这个成员代表了进程希望让自己的线程在发起系统调用时, 内核要用哪一个分发器来分发这个系统调用, 而这个分发器就是攻击者可以插入到内核中某个结构体的, 只要将指向攻击者自身驱动中的恶意代码的分发器插入了这个结构体就能成功达成Alternative Syscall, 也就是__另一个__条件.

接下来的原理说明会涉及到大量未文档化的结构体, 依照代码深入层度来一个个介绍, 首先就是上面说的用来存放分发器的位于内核中的未导出结构PspServiceDescriptorGroupTable, 它的类型为:ServiceDescriptorRow[0x20], ServiceDescriptorRow就是分发器本身, 包含了提供分发器的驱动的基址, 分发器本身(AltSyscallDispatchTable).

分发器又包含了能分发的SSN上限以及相应数量的描述器, 当Alternative Syscall发生时就会拿着SSN当下标来进行分发选择到这个系统调用对应的描述器(AltSyscallDispatchDescriptor) .

描述器整体就是一个u32类型, 低 4 位保留了这个系统调用放在栈中的参数个数(也就是超过 4 个的个数), 每个参数默认为u64类型, 本质是将栈当作额外的寄存器使用, 高 28 位保留了(目标回调相对于驱动基址的偏移 | Alternative Syscall回路选择标志位), 实际上应该分为 27 + 1 bit, 只是PE文件被映射到内存中后会保证每个函数起始不会为奇数, 最低位永远是0, 为了编写代码时的便利直接合并这两个成员.

上述几个结构体的C定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace ssn_dispatch {
typedef struct _AltSyscallDispatchDescriptor {
u32 stack_args_num_in_qword : 4;
u32 offset_and_path_flag : 28;
} AltSyscallDispatchDescriptor;

typedef struct _AltSyscallDispatchTable {
u32 count;
AltSyscallDispatchDescriptor descriptor[ANYSIZE_ARRAY];
} AltSyscallDispatchTable, * pAltSyscallDispatchTable;

typedef struct _ServiceDescriptorRow {
PVOID driver_base;
pAltSyscallDispatchTable ssn_dispatch_table;
PVOID _reserved;
} ServiceDescriptorRow, * pServiceDescriptorRow;
}

上面提到了一个回路选择的标志位, 这里只会使用原文提到的PspSyscallProviderServiceDispatchGeneric, 当标志位置位时触发, 即(*(_BYTE *)&descriptor & 0x10) != 0)

利用手法

原文作者使用rust实现了demo, 为了深入理解原理和利用我自己用 C++ 进行了重写

原理分析中提到了 3 个需要攻击者设置的关键数据:

  1. 向内核中的PspServiceDescriptorGroupTable插入分发器
  2. 向受害者进程设置想要的使用的分发器
  3. 向受害者进程下所有线程设置AltSyscall标志位

下面对每个过程进行解释

找到内核中的PspServiceDescriptorGroupTable

但是上面说过, 这个结构是内核中未导出的, 我们需要自己找到

交叉引用该结构会发现有两处调用:

QQ_1776250342926

原文使用的方法是匹配PsSyscallProviderDispatch的函数头然后找到将该结构送入寄存器的那条指令, 从指令中解析出该结构的偏移进而进算出地址

构建分发器结构

拿到了分发器表的地址后就应该准备插入其中的分发器了, 在此之前要根据上面分析出的几层结构构建好分发器, 从最底层的结构开始, 查询 Windows 系统调用表, Windows10/11 的系统调用号在 0x200 以下, 在单纯为了拦截系统调用的目的下可以将AltSyscallDispatchTable的系统调用号上限设为 0x200, 然后需要填入描述器的 3 个成员, 这里栈上参数的个数可以先都设置为0, 根据要 hook 的目标Nt函数所需参数可以针对某个SSN设置, 然后是回路标志位和SSN对应的处理函数偏移, 这里可以选择全部都设置为同一个函数作为跳板, 在跳板中实现具体的分发逻辑.

另外需要注意的是, 因为结构大小的限制, 处理函数的偏移不能大于u32上限.

构建好最底层的逻辑后剩下的就是不断包装到可以插入PspServiceDescriptorGroupTable的结构位置.

整体代码如下:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
alt_syscall_manager::alt_syscall_manager() : 
installed_slot(0),
ssn_count(0x200),
generic_path(TRUE),
PspServiceDescriptorGroupTable(NULL),
g_AltSyscall_mode(switch_mode::off),
row_to_insert{ 0 } {
u64 handler_offset;
size_t altsyscall_dispatch_table_size = sizeof(AltSyscallDispatchTable) + sizeof(u32) * ssn_count;
u8* key_insn_addr;
PVOID PsSyscallProviderDispatch;
u32 disp32;
u8 PsSyscallProviderDispatch_header_pattern[] = {
// nt!PsSyscallProviderDispatch
0x48, 0x89, 0x5c, 0x24, 0x08, // mov qword ptr [rsp+8], rbx
0x55, // push rbp
0x56, // push rsi
0x57, // push rdi
0x41, 0x56, // push r14
0x41, 0x57, // push r15
0x48, 0x83, 0xec, 0x30, // sub rsp, 30h
0x48, 0x83, 0x64, 0x24, 0x70, 0x00, // and qword ptr [rsp+70h], 0
0x48, 0x8b, 0xf1, // mov rsi, rcx
0x65, 0x48, 0x8b, 0x2c, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rbp, qword ptr gs:[188h]
0xf6, 0x45, 0x03, 0x04 // test byte ptr [rbp+3], 4
};

get_kernel_module_base(L"AltSyscall_in_cpp.sys", &mydriver);
get_kernel_module_base(KERNEL_SYM, &kernel);
handler_addr = &alt_syscall_manager::syscall_handler;
handler_offset = (ULONG_PTR)handler_addr - (ULONG_PTR)mydriver.base;
ASSERT(handler_offset < MAXUINT32);

altsyscall_dispatch_table = (pAltSyscallDispatchTable)ExAllocatePool2(POOL_FLAG_NON_PAGED, altsyscall_dispatch_table_size, 'tdsA');
if(!altsyscall_dispatch_table) {
return;
}

altsyscall_dispatch_table->count = ssn_count;
for(u32 i = 0; i < ssn_count; i++) {
altsyscall_dispatch_table->descriptor[i].stack_args_num_in_qword = 0;
altsyscall_dispatch_table->descriptor[i].offset_and_path_flag = handler_offset | generic_path;
}

row_to_insert.driver_base = mydriver.base;
row_to_insert.ssn_dispatch_table = altsyscall_dispatch_table;
row_to_insert._reserved = NULL;

PsSyscallProviderDispatch = scan_module_pattern(&kernel, PsSyscallProviderDispatch_header_pattern, sizeof(PsSyscallProviderDispatch_header_pattern));
if(!PsSyscallProviderDispatch) {
return;
}

key_insn_addr = (u8*)((ULONG_PTR)PsSyscallProviderDispatch + 0x77); // lea rcx, PspServiceDescriptorGroupTable
disp32 = leunpackU32(
key_insn_addr[3],
key_insn_addr[4],
key_insn_addr[5],
key_insn_addr[6]
);
PspServiceDescriptorGroupTable = (pServiceDescriptorRow)(key_insn_addr + 7 + disp32);
PspServiceDescriptorGroupTable[installed_slot] = row_to_insert;
g_AltSyscall_mode = switch_mode::on;
configure_active_processes();
}

alt_syscall_manager::~alt_syscall_manager() {
PspServiceDescriptorGroupTable[installed_slot] = { 0 };
if (altsyscall_dispatch_table) {
ExFreePool(altsyscall_dispatch_table);
}
return;
}

编写处理函数

这里要说明PspSyscallProviderServiceDispatchGeneric回路跳转到分发器中的处理函数时传入的几个参数的含义

直接拉出ntoskrnl.exe后让IDA 自动下载其符号的话静态分析会发现PspSyscallProviderServiceDispatchGeneric会调用guard_dispatch_icall_no_overrides使用类似push; ret的方式跳转到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
29
30
31
32
33
34
35
36
37
38
__int64 __fastcall PspSyscallProviderServiceDispatchGeneric(
_KTRAP_FRAME *_KTRAP_FRAME,
__int64 callback_addr,
unsigned __int8 metadata,
unsigned int SSN,
_QWORD *a5)
{
unsigned __int64 SSN_1; // rbx
int v6; // eax
int v8; // ecx
__int128 *p_KeServiceDescriptorTableShadow; // rdx
_QWORD args[4]; // [rsp+30h] [rbp-C8h] BYREF
_BYTE v11[128]; // [rsp+50h] [rbp-A8h] BYREF

args[0] = _KTRAP_FRAME->Rcx;
args[1] = _KTRAP_FRAME->Rdx;
args[2] = _KTRAP_FRAME->R8;
args[3] = _KTRAP_FRAME->R9;
SSN_1 = SSN;
if ( metadata && (v6 = PspCaptureSystemServiceInMemoryArgs((void *)(_KTRAP_FRAME->Rsp + 40), v11, metadata), v6 < 0) )
{
*a5 = (unsigned int)v6;
return 0;
}
else
{
v8 = *((_DWORD *)&KeGetCurrentThread()->0 + 1) & 0x200000;
if ( (SSN_1 & 0x7000) != 0x1000 || (p_KeServiceDescriptorTableShadow = &KeServiceDescriptorTableFilter, !v8) )
p_KeServiceDescriptorTableShadow = &KeServiceDescriptorTableShadow;
return guard_dispatch_icall_no_overrides(
*(_QWORD *)&p_KeServiceDescriptorTableShadow[2 * ((SSN_1 >> 12) & 7)]
+ ((__int64)*(int *)(*(_QWORD *)&p_KeServiceDescriptorTableShadow[2 * ((SSN_1 >> 12) & 7)]
+ 4 * (SSN_1 & 0xFFF)) >> 4),
(unsigned int)SSN_1,
args,
a5);
}
}

QQ_1776252279896

实际上调试就会发现调用的是:

QQ_1776252502301

对应的操作是直接跳转到 RAX 存的地址:

QQ_1776252519895

那我们就直接分析call发生时的参数即可, 可以看到Generic线查了原SSDT中应该被调用的Nt函数地址当作第 1 个参数传入了处理函数, 第 2 个参数就是本次系统调用的系统调用号, 第 3 个参数是存入内存中的系统调用发生时传入的前 4 个参数的地址, 最后一个参数就是当前上下文(也就是系统调用发出线程的上下文)栈的偏移, 原文分析是P3Home的地址

讲完了参数还要说说处理函数的返回值, 当处理函数返回0时将直接返回系统调用的结果, 如果是非0值则会继续调用原Nt函数, 至此可以编写出一个简单的拦截NtCloase的示例:

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
u64 alt_syscall_manager::ntclose_callback(pargs_t args_base) noexcept {
PKTHREAD thread = KeGetCurrentThread();
_KTRAP_FRAME* trap_frame = (_KTRAP_FRAME*)((NPETHREAD)thread)->TrapFrame;
PCWSTR process_name = thread_to_process_name(thread);
if (args_base->rcx == 0) {
trap_frame->P3Home = 0xDEAD;
kprintf("[AltSyscall] NtClose called with NULL handle by thread of process %wZ, return fake value: 0xDEAD\n", process_name);
return 0;
}else if(args_base->rcx == 0x1234) {
trap_frame->P3Home = 0xBEEF;
kprintf("[AltSyscall] NtClose called with handle 0x1234 by thread of process %wZ, return fake value: 0xBEEF\n", process_name);
return 0;
}
else {
kprintf("[AltSyscall] NtClose called with handle 0x%llx by thread of process %wZ, head to system service routine\n", args_base->rcx, process_name);
return 1;
}
}

u64 alt_syscall_manager::syscall_dispatcher(
PVOID original_Ntfunc_ptr,
u32 ssn,
pargs_t args_base,
PVOID p3_home
) noexcept {
switch (ssn) {
case NtCloseSSN:
return ntclose_callback(args_base);
default:
return 1;
}
}

运行测试:

QQ_1776253858721

完整源码放在https://github.com/1K0CT/AltSyscall_in_cpp