diff --git a/bin/monitor.c b/bin/monitor.c index 292bcde06..0c2f66990 100644 --- a/bin/monitor.c +++ b/bin/monitor.c @@ -86,6 +86,10 @@ void monitor_init(HMODULE module_handle) destroy_pe_header(module_handle); misc_set_monitor_options(cfg.track, cfg.mode, cfg.trigger); + + // This is the second part of the UM hook protection + // Register our exception handler + register_veh(); } void monitor_hook(const char *library, void *module_handle) diff --git a/inc/hooking.h b/inc/hooking.h index 4df774f09..3fd3e211a 100644 --- a/inc/hooking.h +++ b/inc/hooking.h @@ -206,6 +206,7 @@ int wmi_win32_process_create_pre( IWbemServices *services, IWbemClassObject *args, uint32_t *creation_flags ); void ole_enable_hooks(REFCLSID refclsid); +void register_veh(); extern uintptr_t g_monitor_start; extern uintptr_t g_monitor_end; diff --git a/sigs/process_native.rst b/sigs/process_native.rst index 9078399e5..b744bd6fb 100644 --- a/sigs/process_native.rst +++ b/sigs/process_native.rst @@ -502,7 +502,31 @@ Logging:: i heap_dep_bypass exploit_makes_heap_executable(ProcessHandle, orig_base_address, NewAccessProtection) i process_identifier pid_from_process_handle(ProcessHandle) +Middle:: + // This is first part of the implementation to tackle cuckoo's usermode hook removal by malware + if (ProcessHandle == GetCurrentProcess()) + { + if (OldAccessProtection != NULL) + { + MEMORY_BASIC_INFORMATION_CROSS mbi; + memset(&mbi, 0, sizeof(mbi)); + + if (virtual_query(*BaseAddress, &mbi)) + { + // TODO: Include other module where the UM hooks need to be protected + if ((size_t)mbi.AllocationBase == (size_t)GetModuleHandle("ntdll.dll") || + (size_t)mbi.AllocationBase == (size_t)GetModuleHandle("kernel32.dll")) + { + // What we are trying to do here is to prevent all *write* to *read* on page protection + //__debugbreak(); + if (NewAccessProtection == PAGE_EXECUTE_READWRITE || NewAccessProtection == PAGE_READWRITE) + virtual_protect(*BaseAddress, *NumberOfBytesToProtect, PAGE_EXECUTE_READ); + } + } + } + } + NtFreeVirtualMemory =================== diff --git a/src/hooking.c b/src/hooking.c index a2c3e147b..0b87c7e5b 100644 --- a/src/hooking.c +++ b/src/hooking.c @@ -1071,3 +1071,149 @@ int hook_missing_hooks(HMODULE module_handle) log_debug("Finished missing hooks @ %p\n", module_handle); return 0; } + +// +// This is the second part of the UM hook protection +// This VEH handler attempts to resume the AV triggered +// when the hook restoration is failed as we deliberately protect our +// hooked API from being written via NtProtectVirtualMemory hook +// +static LONG WINAPI vector_handler_skip( EXCEPTION_POINTERS *ExceptionInfo) +{ + PCONTEXT context; + uint32_t exception_code = 0; + + if(ExceptionInfo != NULL) + { + context = ExceptionInfo->ContextRecord; + exception_code = ExceptionInfo->ExceptionRecord->ExceptionCode; + } + + if(exception_code == STATUS_ACCESS_VIOLATION) + { + do + { + uintptr_t pc = 0; + #if __x86_64__ + pc = context->Rip; + #else + pc = context->Eip; + #endif + + // Bail out if the exception address is from cuckoo's own monitor.dll + if (pc >= g_monitor_start && pc < g_monitor_end) + copy_return(); + + // TODO: We should cover all the MOV opcode instructions + // F3 A4: repe movsb byte ptr es:[edi], byte ptr [esi] + // F0 0F B0/B1: lock cmpxchg [mem], reg + // 86/87 : xchg [mem], reg + // 66 89 : mov [mem], regword + uint8_t *target = (uint8_t*)pc; + if (*target == 0xC6 || *target == 0xC7 || *target == 0xF3 || + (*target == 0x66 && *(target+1) == 0x89) || + *target == 0x86 || *target == 0x87 || *target == 0x88 || *target == 0x89 || + (*target == 0xF0 && *(target+1) == 0x0F && (*(target+2) == 0xB0 || *(target+2) == 0xB1))) + { + // Determine if the AV faulty instruction is related to UM hooks bypass (ie: restore the UM hook by checking the destination address) + char insn[DISASM_BUFSIZ]; + if (disasm((void*)pc, insn) == 0) + { + MEMORY_BASIC_INFORMATION_CROSS mbi; + BOOLEAN skipped = FALSE; + void *write_to_address = NULL; + char *temp_insn = strchr(insn, ','); + *temp_insn = '\0'; + + if (!strchr(insn, '[') && !strchr(insn, ']')) + // Bail out + break; + + memset(&mbi, 0, sizeof(mbi)); + + if (strstr(insn, "eax") || strstr(insn, "rax")) + #if __x86_64__ + write_to_address = (void *)context->Rax; + #else + write_to_address = (void *)context->Eax; + #endif + else if (strstr(insn, "ebx") || strstr(insn, "rbx")) + #if __x86_64__ + write_to_address = (void *)context->Rbx; + #else + write_to_address = (void *)context->Ebx; + #endif + else if (strstr(insn, "ecx") || strstr(insn, "rcx")) + #if __x86_64__ + write_to_address = (void *)context->Rcx; + #else + write_to_address = (void *)context->Ecx; + #endif + else if (strstr(insn, "edx") || strstr(insn, "rdx")) + #if __x86_64__ + write_to_address = (void *)context->Rdx; + #else + write_to_address = (void *)context->Edx; + #endif + else if (strstr(insn, "esi") || strstr(insn, "rsi")) + #if __x86_64__ + write_to_address = (void *)context->Rsi; + #else + write_to_address = (void *)context->Esi; + #endif + else if (strstr(insn, "edi") || strstr(insn, "rdi")) + #if __x86_64__ + write_to_address = (void *)context->Rdi; + #else + write_to_address = (void *)context->Edi; + #endif + + if (write_to_address != NULL) + { + if(virtual_query(write_to_address, &mbi)) + { + if ((size_t)mbi.AllocationBase == (size_t)GetModuleHandle("ntdll.dll") || + (size_t)mbi.AllocationBase == (size_t)GetModuleHandle("kernel32.dll")) + // TODO: Should we report it to front end about the UM hook bypass? + skipped = TRUE; + } + + // Finally we want to skip the UM hook bypass instruction + if (skipped) + { + // Skip to next instruction + int length = lde((void*)pc); + #if __x86_64__ + context->Rip += length; + #else + context->Eip += length; + #endif + return EXCEPTION_CONTINUE_EXECUTION; + } + } + } + else + { + // Failed in diassembly + break; + } + } + + } while (0); + } + + return EXCEPTION_CONTINUE_SEARCH; +} + + +void register_veh() +{ + // We don't need to save the handle to the exception handler + //__debugbreak(); + OutputDebugStringA("Entered register_veh"); + if (AddVectoredExceptionHandler(1, vector_handler_skip) == NULL) + { + pipe("CRITICAL:Failed to register VEH"); + return; + } +} diff --git a/test/bypass_um_hooks.c b/test/bypass_um_hooks.c new file mode 100644 index 000000000..d12b4de69 --- /dev/null +++ b/test/bypass_um_hooks.c @@ -0,0 +1,43 @@ +/* To compile: cl.exe bypass_um_hooks.c user32.lib */ +#include +#include + +#define CAPTION "Bypass UM Hook" +typedef unsigned char uint8_t; + +void main() +{ + uint8_t *func = (uint8_t*)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenThread"); + uint8_t buffer[10] = { 0 }; + SIZE_T read; + DWORD oldprotect = 0; + + if (func == NULL) + { + MessageBoxA(NULL, "Failed to resolve NtOpenThread", CAPTION, MB_OK); + return; + } + + // The VirtualProtect call should not be able to modify the target address to WRITE + if (!VirtualProtect(func, 0x10, PAGE_EXECUTE_READWRITE, &oldprotect)) + { + MessageBoxA(NULL, "Failed to modify protection", CAPTION, MB_OK); + return; + } + + ReadProcessMemory(GetCurrentProcess(), func, buffer, sizeof(buffer), &read); + + // Simulate the malware's behavior attempting to detect the UM's hooks and then restore the hooked instructions + // Found PUSH xxxxx opcode + if (buffer[0] == 0x68 || buffer[0] == 0xe9) + { + // With the UM hooks protection enabled, it should trigger AV as the target address does not have write permission + *func = 0xb8; + } + + // The UM hook protection should skip the AV above and proceed its main payload + // In other words, + // Protection enabled: Observed message box prompt + // Protection disabled: No message box will be prompted + MessageBoxA(NULL, "Done", CAPTION, MB_OK); +} \ No newline at end of file