0%

Syscall Detect

Syscall检测

在ntdll.dll中的系统调用一般都会遵循结构代码

1
2
3
4
mov r10, rcx				// 0x4c, 0x8b, 0xd1
mov eax, 「Syscall Number」 // 0xb8, 0x00, 0x00, 0x00, 0x00
syscall // 0x0f, 0x05
ret // 0xc3

当通过API调用Syscall时,流程如下所示:

img

可以观察到当从内核返回进入用户模式代码时,RIP在ntdll中。在syscall指令之后通常会有一个ret,它讲执行返回给调用者。

而当使用SysWhispers调用函数时,syscall指令会直接在程序的主模块执行,流程如下所示:

img

利用InstrumentationCallback

https://www.codeproject.com/Articles/543542/Windows-x64-system-service-hooks-and-advanced-debu

https://winternl.com/detecting-manual-syscalls-from-user-mode/

https://pre.empt.blog/2022/implementing-syscall-detection-into-fennec

具体思路:

利用KPROCESS!InstrumentationCallback 字段在每次有内核到用户模式切换是执行回调。其主要思想上保存RIP,并对其进行分析,以确定当执行返回到用户模式时,它是否在ntdll.dll地址空间中。

每当内核遇到返回用户级代码时,它都会检查KPROCESS!InstrumentationCallback成员是否为NULL,如果它不为NULL且指向有效内存,内核将交换掉 陷阱帧上的RIP,并将其替换为InstrumentationCallback字段中存储的值。

相关项目:

https://github.com/jackullrich/syscall-detect

https://github.com/paranoidninja/Process-Instrumentation-Syscall-Hook

1
2
3
4
5
6
7
8
9
10
0:003> dt _kprocess
ntdll!_KPROCESS
// ...
+0x3d8 InstrumentationCallback : Ptr64 Void
typedef struct _PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION
{
ULONG Version;
ULONG Reserved;
PVOID Callback;
} PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION, *PPROCESS_INSTRUMENTATION_CALLBACK_INFORMATION;

KPROCESS!InstrumentationCallback 可通过调用NtSetInformationProcess来设置

可以使用PROCESSINFOCLASS 值和一个指向PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION的指针来调用NtSetInformationProcess

1
2
3
4
5
6
NTSTATUS NtSetInformationProcess(
HANDLE hProcess,
ULONG ProcessInfoClass,
void *InputBuffer,
ULONG size
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define ProcessInstrumentationCallback  40

typedef struct _PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION
{
ULONG Version;
ULONG Reserved;
PVOID Callback;
} PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION, * PPROCESS_INSTRUMENTATION_CALLBACK_INFORMATION;


PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION Callback = { 0 };
Callback.Version = 0; // version 0表示为x64 1表示为x86
Callback.Reserved = 0; // reserved always 0
Callback.Callback = InstrumentationCallbackThunk;

NtSetInformationProcess(
GetCurrentProcess(),
(PROCESS_INFORMATION_CLASS)ProcessInstrumentationCallback,
&Callback,
sizeof(Callback)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
include ksamd64.inc

extern InstrumentationCallback:proc
EXTERNDEF __imp_RtlCaptureContext:QWORD

.code

InstrumentationCallbackThunk proc
mov gs:[2e0h], rsp ; Win10 TEB InstrumentationCallbackPreviousSp
mov gs:[2d8h], r10 ; Win10 TEB InstrumentationCallbackPreviousPc
mov r10, rcx ; Save original RCX
sub rsp, 4d0h ; Alloc stack space for CONTEXT structure
and rsp, -10h ; RSP must be 16 byte aligned before calls
mov rcx, rsp
call __imp_RtlCaptureContext ; Save the current register state. RtlCaptureContext does not require shadow space
sub rsp, 20h ; Shadow space
call InstrumentationCallback
int 3
InstrumentationCallbackThunk endp

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VOID InstrumentationCallback(CONTEXT *context)
{
ULONG_PTR pTEB = (ULONG_PTR)NtCurrentTeb();

context->Rip = *((ULONG_PTR*)(pTEB + 0x02D8)); // TEB->InstrumentationCallbackPreviousPc
context->Rsp = *((ULONG_PTR*)(pTEB + 0x02E0)); // TEB->InstrumentationCallbackPreviousSp
context->Rcx = context->R10;

// Prevent recursion TEB->InstrumentationCallbackDisabled
BOOLEAN bInstrumentationCallbackDisabled = *((BOOLEAN*)pTEB + 0x1b8);

if (!bInstrumentationCallbackDisabled) {
bInstrumentationCallbackDisabled = TRUE;

// Do whatever you want


// Enabling so we can catch next callback.
bInstrumentationCallbackDisabled = FALSE;
}

RtlRestoreContext(context, NULL);
}
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
BOOL SetInstrumentationCallback() {
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION Callback = { 0 };
HANDLE hProcess = GetCurrentProcess();
NTSTATUS Status = { 0 };

Callback.Version = 0;
Callback.Reserved = 0;
Callback.Callback = NULL;

HMODULE hNtdll = GetModuleHandleA("ntdll");
if (hNtdll == NULL)
{
return FALSE;
}

pNtSetInformationProcess NtSetInformationProcess = (pNtSetInformationProcess)GetProcAddress(hNtdll, "NtSetInformationProcess");

if (NtSetInformationProcess == NULL)
{
return FALSE;
}

Status = NtSetInformationProcess(
hProcess,
(PROCESS_INFORMATION_CLASS)ProcessInstrumentationCallback,
&Callback,
sizeof(Callback)
);

if (NT_SUCCESS(Status))
{
return TRUE;
}
else
{
return FALSE;
}
}
img

在以下几种情况下 InstrumentationCallback不会产生任何结果

  • NtTerminateProcess 和 NtTerminateThread (如果是调用自身)

调用方不会从这些调用中返回

  • NtContinue

该函数接受提供的上下文参数,并直接应用于当前陷阱帧 trap frame,然后不使用KeSystemServiceExit执行IRET

  • NtRaiseExeception

和NtContinue类似,该函数接受提供的上下文参数,并将其应用于当前陷阱帧 trap frame,但是,如果没有处理 KiUserExceptionDispatcher,那么将调用它,从而给予我们拦截的机会。


利用frida对syscall进行检测

https://passthehashbrowns.github.io/detecting-direct-syscalls-with-frida

pip3 install frida

pip3 install frida-tools

安装完成后可以使用frida-ps检测

frida C:-Main_calc.exe -l 1.js

img
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
var modules = Process.enumerateModules()
var ntdll = modules[1]

var ntdllBase = ntdll.base
send("[*] Ntdll base: " + ntdllBase)
var ntdllOffset = ntdllBase.add(ntdll.size)
send("[*] Ntdll end: " + ntdllOffset)

const mainThread = Process.enumerateThreads()[0];
Process.enumerateThreads().map(t => {
Stalker.follow(t.id, {
events: {
call: false, // CALL instructions: yes please
// Other events:
ret: false, // RET instructions
exec: false, // all instructions: not recommended as it's
// a lot of data
block: false, // block executed: coarse execution trace
compile: false // block compiled: useful for coverage
},
onReceive(events) {
},
transform(iterator){
let instruction = iterator.next()
do{
//I think this reduces overhead
if(instruction.mnemonic == "mov"){
//Should provide a good filter for syscalls, might need further filtering
if(instruction.toString() == "mov r10, rcx"){
iterator.keep() //keep the instruction
instruction = iterator.next() //next instruction should have the syscall number
//This helps to clear up some false positives
if(instruction.toString().split(',')[0] == "mov eax"){
var addrInt = instruction.address.toInt32()
//If the syscall is coming from somewhere outside the bounds of NTDLL
//then it may be malicious
if(addrInt < ntdllBase.toInt32() || addrInt > ntdllOffset.toInt32()){
send("[+] Found a potentially malicious syscall: " + instruction.toString())
}
}
}
}

iterator.keep()
} while ((instruction = iterator.next()) !== null)
}
})
})

HWBP

在syscall/ret 处设置一个硬件断点, 将间接系统调用 用 call/jmp 到我们的指令。

如果它来自 kernel32、kernelbase 说明是一个合法函数 否则为非法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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
//https://fool.ish.wtf/2022/11/detecting-indirect-syscalls.html
#include <windows.h>
#include <stdio.h>
#include "c_syscalls.h" // janoglezcampos/c_syscalls

#define SINGLE_STEP_COUNT 2

uintptr_t k32_h;
uintptr_t kb_h;
DWORD k32_s;
DWORD kb_s;

uintptr_t find_gadget(
_In_ const uintptr_t function,
_In_ const BYTE* stub,
_In_ const UINT size
) {
for (unsigned int i = 0; i < 25u; i++)
{
if (memcmp((LPVOID)(function + i), stub, size) == 0) {
return (function + i);
}
}
return NULL;
}

BOOL set_hardware_breakpoint(
_In_ const DWORD tid,
_In_ const uintptr_t address,
_In_ const UINT pos,
_In_ const BOOL init
) {
CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };

HANDLE thd = INVALID_HANDLE_VALUE;

BOOL res = FALSE;

if (tid == GetCurrentThreadId())
{
thd = GetCurrentThread();
}
else
{
thd = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
}

res = GetThreadContext(thd, &context);

if (init && res)
{
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else
{
if ((&context.Dr0)[pos] == address)
{
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = 0ull;
}
}

res = SetThreadContext(thd, &context);

if (thd != INVALID_HANDLE_VALUE) CloseHandle(thd);

return res;
}


LONG WINAPI exception_handler(const PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)

{
static unsigned short count = SINGLE_STEP_COUNT;

if (count > 0)
{
ExceptionInfo->ContextRecord->EFlags |= 1 << 8; // TF
count--;
}

else if (count == 0)
{
printf("[%u] syscall -> ret -> 0x%p\n", count, (PVOID)ExceptionInfo->ContextRecord->Rip);

uintptr_t address = ExceptionInfo->ContextRecord->Rip;

BOOL legit = FALSE;

// syscall -> ret -> ...

if (address >= k32_h && address <= k32_h + k32_s || address >= kb_h && address <= kb_h + kb_s)
{
// check opcode is not ret opcode
char opcode = *(char*)ExceptionInfo->ContextRecord->Rip;

if (opcode != 0xC3 && opcode != 0xCB && opcode != 0xC2 && opcode != 0xCA)
legit = TRUE;
}

printf("\n[+] %s SYSCALL DETECTED\n\n", legit ? "LEGIT" : "INDIRECT");

count = SINGLE_STEP_COUNT;

}

ExceptionInfo->ContextRecord->EFlags |= 1 << 16; // RF

return EXCEPTION_CONTINUE_EXECUTION;
}

}


DWORD WINAPI test_thread(_In_ LPVOID lpParameter) {
return 0;
}


uintptr_t set_module_values(_In_ uintptr_t module, _Out_ DWORD* size) {

PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(module + ((PIMAGE_DOS_HEADER)module)->e_lfanew);

for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {

const PIMAGE_SECTION_HEADER section = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(nt) + (DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i);

if ((*(ULONG*)section->Name | 0x20202020) == 'xet.') {

ULONG dw;

module = module + section->VirtualAddress;

*size = section->Misc.VirtualSize;

break;

}

}

return module;
}


int main() {

// Register our exception handler

const PVOID handler = AddVectoredExceptionHandler(1, exception_handler);

// Set the global values of the DLL .text sections VA and SZ

k32_h = set_module_values(GetModuleHandleA("KERNEL32.dll"), &k32_s);

kb_h = set_module_values(GetModuleHandleA("KERNELBASE.dll"), &kb_s);

// Find address to breakpoint on and set it.

const uintptr_t syscall_address1 = find_gadget(GetProcAddress(GetModuleHandleA("NTDLL.dll"), "NtTestAlert"), "\x0F\x05", 2);

set_hardware_breakpoint(GetCurrentThreadId(), syscall_address1, 1, TRUE);

const uintptr_t syscall_address2 = find_gadget(GetProcAddress(GetModuleHandleA("NTDLL.dll"), "NtCreateThreadEx"), "\x0F\x05", 2);

set_hardware_breakpoint(GetCurrentThreadId(), syscall_address2, 2, TRUE);

// Test cases

printf("[-] Testing indirect syscall.\n");

NTSTATUS status = Syscall(NT_TEST_ALERT);

printf("[-] Testing legitimate syscall.\n");

const HANDLE t = CreateThread(NULL, 0, test_thread, NULL, 0, NULL);

if (t) {

WaitForSingleObject(t, INFINITE); CloseHandle(t);

}

// Disable the hardware breakpoint

set_hardware_breakpoint(GetCurrentThreadId(), syscall_address1, 1, FALSE);

set_hardware_breakpoint(GetCurrentThreadId(), syscall_address2, 2, FALSE);

// Remove our registered VEH

if (handler != NULL) RemoveVectoredExceptionHandler(handler);

}

References

https://winternl.com/detecting-manual-syscalls-from-user-mode/

https://pre.empt.blog/2022/implementing-syscall-detection-into-fennec

https://www.codeproject.com/Articles/543542/Windows-x64-system-service-hooks-and-advanced-debu

https://passthehashbrowns.github.io/detecting-direct-syscalls-with-frida

欢迎关注我的其它发布渠道

------------- 💖 🌞 本 文 结 束 😚 感 谢 您 的 阅 读 🌞 💖 -------------