Syscall检测
在ntdll.dll中的系统调用一般都会遵循结构代码
1 2 3 4 mov r10, rcx mov eax, 「Syscall Number」 syscall ret
当通过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 _kprocessntdll!_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 ; Callback.Reserved = 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:procEXTERNDEF __imp_RtlCaptureContext:QWORD .code InstrumentationCallbackThunk proc mov gs:[2e0 h], rsp ; Win10 TEB InstrumentationCallbackPreviousSp mov gs:[2 d8h], r10 ; Win10 TEB InstrumentationCallbackPreviousPc mov r10, rcx ; Save original RCX sub rsp, 4 d0h ; Alloc stack space for CONTEXT structure and rsp, -10 h ; 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, 20 h ; 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 )); context->Rsp = *((ULONG_PTR*)(pTEB + 0x02E0 )); context->Rcx = context->R10; BOOLEAN bInstrumentationCallbackDisabled = *((BOOLEAN*)pTEB + 0x1b8 ); if (!bInstrumentationCallbackDisabled) { bInstrumentationCallbackDisabled = TRUE; 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 (如果是调用自身)
调用方不会从这些调用中返回
该函数接受提供的上下文参数,并直接应用于当前陷阱帧 trap
frame,然后不使用KeSystemServiceExit执行IRET
和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 , ret : false , exec : false , block : false , compile : false }, onReceive (events ) { }, transform (iterator ){ let instruction = iterator.next () do { if (instruction.mnemonic == "mov" ){ if (instruction.toString () == "mov r10, rcx" ){ iterator.keep () instruction = iterator.next () if (instruction.toString ().split (',' )[0 ] == "mov eax" ){ var addrInt = instruction.address .toInt32 () 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 #include <windows.h> #include <stdio.h> #include "c_syscalls.h" #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 ; 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; if (address >= k32_h && address <= k32_h + k32_s || address >= kb_h && address <= kb_h + kb_s) { 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 ; 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 () { const PVOID handler = AddVectoredExceptionHandler (1 , exception_handler); k32_h = set_module_values (GetModuleHandleA ("KERNEL32.dll" ), &k32_s); kb_h = set_module_values (GetModuleHandleA ("KERNELBASE.dll" ), &kb_s); 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); 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); } set_hardware_breakpoint (GetCurrentThreadId (), syscall_address1, 1 , FALSE); set_hardware_breakpoint (GetCurrentThreadId (), syscall_address2, 2 , FALSE); 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