diff --git a/_includes/footer.html b/_includes/footer.html index 2f4aa62..614a929 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -2,11 +2,11 @@ diff --git a/_techniques/assembly.md b/_techniques/assembly.md index b7fa9f4..9a07928 100644 --- a/_techniques/assembly.md +++ b/_techniques/assembly.md @@ -12,11 +12,12 @@ tags: assembly * [1. INT 3](#int3) * [2. INT 2D](#int2d) -* [3. ICE](#ice) -* [4. Stack Segment Register](#ss_register) -* [5. Instruction Counting](#instruction-counting) -* [6. POPF and Trap Flag](#popf_and_trap_flag) -* [7. Instruction Prefixes](#instruction_prefixes) +* [3. DebugBreak](#debugbreak) +* [4. ICE](#ice) +* [5. Stack Segment Register](#ss_register) +* [6. Instruction Counting](#instruction-counting) +* [7. POPF and Trap Flag](#popf_and_trap_flag) +* [8. Instruction Prefixes](#instruction_prefixes) * [Mitigations](#mitigations) @@ -123,7 +124,38 @@ bool IsDebugged() -3. ICE +3. DebugBreak +As written in DebugBreak documentation, "DebugBreak causes a breakpoint exception to occur in the current process. This allows the calling thread to signal the debugger to handle the exception". + +If the program is executed without a debugger, the control will be passed to the exception handler. Otherwise, the execution will be intercepted by the debugger. + + + +C/C++ Code + + +{% highlight c %} + +bool IsDebugged() +{ + __try + { + DebugBreak(); + } + __except(EXCEPTION_BREAKPOINT) + { + return false; + } + + return true; +} + +{% endhighlight %} + + + + +4. ICE "ICE" is one of Intel's undocumented instructions. Its opcode is 0xF1. It can be used to detect if the program is traced. If ICE instruction is executed, the EXCEPTION_SINGLE_STEP (0x80000004) exception will be raised. @@ -155,7 +187,7 @@ bool IsDebugged() -4. Stack Segment Register +5. Stack Segment Register This is a trick that can be used to detect if the program is being traced. The trick consists of tracing over the following sequence of assembly instructions: {% highlight asm %} @@ -200,7 +232,7 @@ movss_not_being_debugged: -5. Instruction Counting +6. Instruction Counting This technique abuses how some debuggers handle EXCEPTION_SINGLE_STEP exceptions. The idea of this trick is to set hardware breakpoints to each instruction in some predefined sequence (e.g. sequence of NOPs). Execution of the instruction with a hardware breakpoint on it raises the EXCEPTION_SINGLE_STEP exception which can be caught by a vectored exception handler. In the exception handler, we increment a register which plays the role of instruction counter (EAX in our case) and the instruction pointer EIP to pass the control to the next instruction in the sequence. Therefore, each time the control is passed to the next instruction in our sequence, the exception is raised and the counter is incremented. After the sequence is finished, we check the counter and if it is not equal to the length of our sequence, we consider it as if the program is being debugged. @@ -299,7 +331,7 @@ bool IsDebugged() -6. POPF and Trap Flag +7. POPF and Trap Flag This is another trick that can indicate whether a program is being traced. There is a Trap Flag in the Flags register. When the Trap Flag is set, the exception SINGLE_STEP is raised. However, if we traced the code, the Trap Flag will be cleared by a debugger so we won't see the exception. @@ -337,7 +369,7 @@ bool IsDebugged() -7. Instruction Prefixes +8. Instruction Prefixes This trick works only in some debuggers. It abuses the way how these debuggers handle instruction prefixes. If we execute the following code in OllyDbg, after stepping to the first byte F3, we'll immediately get to the end of try block. The debugger just skips the prefix and gives the control to the INT1 instruction. diff --git a/_techniques/debug-flags.md b/_techniques/debug-flags.md index 94ee82d..85defa0 100644 --- a/_techniques/debug-flags.md +++ b/_techniques/debug-flags.md @@ -26,6 +26,7 @@ tags: debug-flags * [2.2. NtGlobalFlag](#manual-checks-ntglobalflag) * [2.3. Heap Flags](#manual-checks-heap-flags) * [2.4. Heap Protection](#manual-checks-heap-protection) + * [2.5. Check KUSER_SHARED_DATA structure](#kuser_shared_data) * [Mitigations](#mitigations-manual-checks) @@ -711,7 +712,7 @@ bool Check() {% endhighlight %} -2.3. Heap Protection +2.4. Heap Protection If the HEAP_TAIL_CHECKING_ENABLED flag is set in NtGlobalFlag, the sequence 0xABABABAB will be appended (twice in 32-Bit and 4 times in 64-Bit Windows) at the end of the allocated heap block. If the HEAP_FREE_CHECKING_ENABLED flag is set in NtGlobalFlag, the sequence 0xFEEEFEEE will be appended if additional bytes are required to fill in the empty space until the next memory block. @@ -738,6 +739,35 @@ bool Check() {% endhighlight %} + +2.5. Check KUSER_SHARED_DATA structure +This technique was originally described as an issue for TitanHide, a kernel driver to hide debuggers from detection. The detailed documentation for the structure KUSER_SHARED_DATA and its fields is available here. + +Here is what the author of the issue wrote in the post regarding the features of the structure and its appropriate field: +"0x7ffe02d4 is actually 0x7ffe0000 + 0x2d4. 0x7ffe0000 is the fixed user mode address of the KUSER_SHARED_DATA structure that contains data that is shared between user mode and the kernel (though user mode doesn't have write access to it). The struct has some interesting properties: +its address is fixed and has been in all Windows versions since it was introduced +its user mode address is the same in 32 bit and 64 bit mode +all offsets and sizes are strictly fixed, and new fields are only ever appended or added in place of unused padding space + + +Hence this program will work in 32 bit Windows 2000 and 64 bit Windows 10 without recompiling". + + + + +C/C++ Code + + +{% highlight c %} + +bool check_kuser_shared_data_structure() +{ + unsigned char b = *(unsigned char*)0x7ffe02d4; + return ((b & 0x01) || (b & 0x02)); +} + +{% endhighlight %} + Mitigations @@ -835,3 +865,8 @@ for (SIZE_T offset = 0; offset < nDwordsToPatch; offset++) *((PDWORD)pHeapEnd + offset) = 0; {% endhighlight %} + + +For KUSER_SHARED_DATA: + +For a possible mitigation, please check the link when the technique is described (with the issue for TitanHide) and also a draft code for patching kdcom.dll here. diff --git a/_techniques/interactive.md b/_techniques/interactive.md index 1b0cba3..ed2bc71 100644 --- a/_techniques/interactive.md +++ b/_techniques/interactive.md @@ -390,7 +390,7 @@ The idea is simple. If a debugger is not present and kernel32!OutputDebugStr -C/C++ Code +C/C++ Code (variant1) {% highlight c %} @@ -401,7 +401,7 @@ bool IsDebugged() return false; DWORD dwLastError = GetLastError(); - OutputDebugString(L"AntiDebug_OutputDebugString"); + OutputDebugString(L"AntiDebug_OutputDebugString_v1"); return GetLastError() != dwLastError; } @@ -409,6 +409,26 @@ bool IsDebugged() +C/C++ Code (variant2) + + +{% highlight c %} + +bool IsDebugged() +{ + if (IsWindowsVistaOrGreater()) + return false; + + DWORD dwErrVal = 0x666; + SetLastError(dwErrVal); + OutputDebugString(L"AntiDebug_OutputDebugString_v2"); + return GetLastError() != dwErrVal; +} + +{% endhighlight %} + + + Mitigations During debugging, it is better to skip suspicious function calls (e.g. fill them with NOPs). @@ -426,5 +446,4 @@ If you write an anti-anti-debug solution, all the following functions can be hoo * user32!SwitchDesktop * kernel32!OutputDebugStringW -Hooked functions can check input arguments and modify the original function behavior. - +Hooked functions can check input arguments and modify the original function behavior. \ No newline at end of file diff --git a/_techniques/misc.md b/_techniques/misc.md index df6d5ea..864d4d5 100644 --- a/_techniques/misc.md +++ b/_techniques/misc.md @@ -18,6 +18,7 @@ tags: misc * [4. DbgPrint()](#dbgprint) * [5. DbgSetDebugFilterState()](#dbgsetdebugfilterstate) * [6. NtYieldExecution() / SwitchToThread()](#switchtothread) +* [7. VirtualAlloc() / GetWriteWatch()](#getwritewatch) * [Mitigations](#mitigations) @@ -44,12 +45,16 @@ The following functions can be used: {% highlight c %} const std::vector vWindowClasses = { + "antidbg", + "ID", // Immunity Debugger + "ntdll.dll", // peculiar name for a window class + "ObsidianGUI", "OLLYDBG", + "Rock Debugger", + "SunAwtFrame", + "Qt5QWindowIcon" "WinDbgFrameClass", // WinDbg - "ID", // Immunity Debugger "Zeta Debugger", - "Rock Debugger", - "ObsidianGUI", }; bool IsDebugged() @@ -363,6 +368,124 @@ bool IsDebugged() + +7. VirtualAlloc() / GetWriteWatch() +This technique was described as a suggestion for a famous al-khaser solution, a tool for testing VMs, debuggers, sandboxes, AV, etc. against many malware-like defences. + +The idea is drawn from the documentation for GetWriteWatch function where the following is stated in a "Remarks" section: + +"When you call the VirtualAlloc function to reserve or commit memory, you can specify MEM_WRITE_WATCH. This value causes the system to keep track of the pages that are written to in the committed memory region. You can call the GetWriteWatch function to retrieve the addresses of the pages that have been written to since the region has been allocated or the write-tracking state has been reset". + +This feature can be used to track debuggers that may modify memory pages outside the expected pattern. + + + +C/C++ Code (variant 1) + + +{% highlight c %} + +bool Generic::CheckWrittenPages1() const { + const int SIZE_TO_CHECK = 4096; + + PVOID* addresses = static_cast(VirtualAlloc(NULL, SIZE_TO_CHECK * sizeof(PVOID), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (addresses == NULL) + { + return true; + } + + int* buffer = static_cast(VirtualAlloc(NULL, SIZE_TO_CHECK * SIZE_TO_CHECK, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE)); + if (buffer == NULL) + { + VirtualFree(addresses, 0, MEM_RELEASE); + return true; + } + + // Read the buffer once + buffer[0] = 1234; + + ULONG_PTR hits = SIZE_TO_CHECK; + DWORD granularity; + if (GetWriteWatch(0, buffer, SIZE_TO_CHECK, addresses, &hits, &granularity) != 0) + { + return true; + } + else + { + VirtualFree(addresses, 0, MEM_RELEASE); + VirtualFree(buffer, 0, MEM_RELEASE); + + return (hits == 1) ? false : true; + } +} + +{% endhighlight %} + + + +C/C++ Code (variant 2) + + +{% highlight c %} + +bool Generic::CheckWrittenPages2() const { + BOOL result = FALSE, error = FALSE; + + const int SIZE_TO_CHECK = 4096; + + PVOID* addresses = static_cast(VirtualAlloc(NULL, SIZE_TO_CHECK * sizeof(PVOID), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (addresses == NULL) + { + return true; + } + + int* buffer = static_cast(VirtualAlloc(NULL, SIZE_TO_CHECK * SIZE_TO_CHECK, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE)); + if (buffer == NULL) + { + VirtualFree(addresses, 0, MEM_RELEASE); + return true; + } + + // Make some calls where a buffer *can* be written to, but isn't actually edited because we pass invalid parameters + if (GlobalGetAtomName(INVALID_ATOM, (LPTSTR)buffer, 1) != FALSE + || GetEnvironmentVariable("This variable does not exist", (LPSTR)buffer, 4096 * 4096) != FALSE + || GetBinaryType("This name does not exist", (LPDWORD)buffer) != FALSE + || HeapQueryInformation(0, (HEAP_INFORMATION_CLASS)69, buffer, 4096, NULL) != FALSE + || ReadProcessMemory(INVALID_HANDLE_VALUE, (LPCVOID)0x69696969, buffer, 4096, NULL) != FALSE + || GetThreadContext(INVALID_HANDLE_VALUE, (LPCONTEXT)buffer) != FALSE + || GetWriteWatch(0, &result, 0, NULL, NULL, (PULONG)buffer) == 0) + { + result = false; + error = true; + } + + if (error == FALSE) + { + // A this point all calls failed as they're supposed to + ULONG_PTR hits = SIZE_TO_CHECK; + DWORD granularity; + if (GetWriteWatch(0, buffer, SIZE_TO_CHECK, addresses, &hits, &granularity) != 0) + { + result = FALSE; + } + else + { + // Should have zero reads here because GlobalGetAtomName doesn't probe the buffer until other checks have succeeded + // If there's an API hook or debugger in here it'll probably try to probe the buffer, which will be caught here + result = hits != 0; + } + } + + VirtualFree(addresses, 0, MEM_RELEASE); + VirtualFree(buffer, 0, MEM_RELEASE); + + return result; +} + +{% endhighlight %} + + + Mitigations During debugging: Fill anti-debug pr anti-traced checks with NOPs. @@ -385,3 +508,5 @@ During debugging: Fill anti-debug pr anti-traced checks with NOPs. 5. For DbgSetDebugFilterState(): Hook ntdll!NtSetDebugFilterState(). If the process is running with debug privileges, return unsuccessfully from the hook. 6. For SwitchToThread: Hook ntdll!NtYieldExecution() and return an unsuccessful status from the hook. + +7. For GetWriteWatch: Hook VirtualAlloc() and GetWriteWatch() to track if VirtualAlloc() is called with MEM_WRITE_WATCH flag. If it is the case, check what is the region to track and return the expected value in GetWriteWatch().