Anti-Debug: Process Memory
Contents
Process Memory
A process can examine its own memory to either detect the debugger presence or interfere with the debugger.
This section includes the process memory and examining the thread contexts, searching for breakpoints, and function patching as anti-attaching methods.
1. Breakpoints
It is always possible to examine the process memory and search for software breakpoints in the code, or check the CPU debug registers to determine if hardware breakpoints are set.
1.1. Software Breakpoints (INT3)
The idea is to identify the machine code of some functions for 0xCC byte which stands for INT 3 assembly instruction.
This method can generate many false-positive cases and should therefore be used with caution.
C/C++ Code
1.2. Anti-Step-Over
Debuggers allow you to step over the function call. In such a case, the debugger implicitly sets a software breakpoint on the instruction which follows the call (i.e. the return address of the called function).
To detect if there was an attempt to step over the function, we can examine the first byte of memory at the return address. If a software breakpoint (0xCC) is located at the return address, we can patch it with some other instruction (e.g. NOP). It will most likely break the code and crash the process. On the other hand, we can patch the return address with some meaningful code instead of NOP and change the control flow of the program.
1.2.1. Direct Memory Modification
It is possible to check from inside a function if there is a software breakpoint after the call of this function. We can read one byte at the return address and if the byte is equal to 0xCC (INT 3), it can be rewritten by 0x90 (NOP). The process will probably crash because we damage the instruction at the return address. However, if you know which instruction follows the function call, you can rewrite the breakpoint with the first byte of this instruction.
C/C++ Code
1.2.2. ReadFile()
The method uses the kernel32!ReadFile() function to patch the code at the return address.
The idea is to read the executable file of the current process and pass the return address as the output buffer to kernel32!ReadFile(). The byte at the return address will be patched with ‘M’ character (the first byte of PE image) and the process will probably crash.
C/C++ Code
1.2.3. WriteProcessMemory()
This method uses the kernel32!WriteProcessMemory() function for patching the code at the return address.
C/C++ Code
1.2.4. Toolhelp32ReadProcessMemory()
The function kernel32!Toolhelp32ReadProcessMemory() allows you to read the memory of other processes. However, it can be used for checking an anti-step-over condition.
C/C++ Code
1.3. Memory Breakpoints
Memory breakpoints are implemented by using guard pages (at least, in OllyDbg and ImmunityDebugger). A guard page provides a one-shot alarm for memory page access. When a guard page is executed, the exception STATUS_GUARD_PAGE_VIOLATION is raised.
A guard page can be created by setting the PAGE_GUARD page protection modifier in the kernel32!VirtualAlloc(), kernel32!VirtualAllocEx(), kernel32!VirtualProtect(), and kernel32!VirtualProtectEx() functions.
However, we can abuse the way the debuggers implement memory breakpoints to check whether the program is executed under a debugger. We can allocate an executable buffer which contains only one byte 0xC3 which stands for RET instruction. We then mark this buffer as a guard page, push the address where the case if a debugger is present is handled to the stack, and jump to the allocated buffer. The instruction RET will be executed and if the debugger (OllyDbg or ImmunityDebugger) is present, we’ll get to the address we had pushed to the stack. If the program is executed without the debugger, we’ll get to an exception handler.
C/C++ Code
1.4. Hardware Breakpoints
Debug registers DR0, DR1, DR2 and DR3 can be retrieved from the thread context. If they contain non-zero values, it may mean that the process is executed under a debugger and a hardware breakpoint was set.
C/C++ Code
2. Other memory checks
This section contains techniques which directly examine or manipulate the virtual memory of running processes to detect or prevent the debugging.
2.1. NtQueryVirtualMemory()
Memory pages of the process where code is located are shared between all processes until a page is written. Afterward, the OS makes a copy of this page and map it to the process virtual memory so this page is no longer “shared”.
Therefore, we can query the Working Set of the current process and check the Shared and ShareCount fields of the Working Set Block for the page with code. If there were software breakpoints in the code, these fields must not be set.
NTDLL declarations
C/C++ Code
Credits for this technique: Virus Bulletin
2.2. Detecting a function patch
A popular way to detect a debugger is to call kernel32!IsDebuggerPresent(). It’s simple to mitigate this check e.g. to change the result in the EAX register or to patch the kernel32!IsDebuggerPresent() function’s code.
Therefore, instead of examining the process memory for breakpoints, we can verify if kernel32!IsDebuggerPresent() was modified. We can read the first bytes of this function and compare them to these bytes of the same function from other processes. Even with enabled ASLR, Windows libraries are loaded to the same base addresses in all the processes. The base addresses are changed only after a reboot, but for all the processes they will stay the same during the session.
C/C++ Code
Credits for this technique: Rouse_
2.3. Patch ntdll!DbgBreakPoint()
The function ntdll!DbgBreakPoint() has the following implementation:
It is called when a debugger attaches to a running process. It allows the debugger to gain control because an exception is raised which it can intercept. If we erase the breakpoint inside ntdll!DbgBreakPoint(), the debugger won’t break in and the thread will exit.
C/C++ Code
2.4. Patch ntdll!DbgUiRemoteBreakin()
When a debugger calls the kernel32!DebugActiveProcess(), a debugger calls ntdll!DbgUiRemoteBreakin() correspondingly. To prevent the debugger from attaching to the process, we can patch ntdll!DbgUiRemoteBreakin() code to invoke the kernel32!TerminateProcess().
In the example below we patch ntdll!DbgUiRemoteBreakin() with the following code:
As the result, the application will terminate itself once we try to attach the debugger to it.
C/C++ Code
Credits for this technique: Rouse_
2.5 Performing Code Checksums
Verifying code checksum is a reliable way to detect software breakpoints, debugger’s step-overs, functions’ inline hooks, or data modification.
The example below shows how it is possible to verify the checksum of a function.
C/C++ Code
Mitigations
- During debugging:
- For Anti-Step-Over tricks: Step in the function which performs the Step-Over check and execute it till the end (Ctrl+F9 in OllyDbg/x32/x64dbg).
- The best way to mitigate all the “memory” tricks (including Anti-Step-Over) is to find the exact check and patch it with NOPs, or set the return value which allows the application to execute further.
- For anti-anti-debug tool development:
- Breakpoints scan:
- Software Breakpoint & Anti-Step-Over: There is no possibility of interfering with these checks as they don’t need to use API and they access memory directly.
- Memory Breakpoints: In general, it is possible to track the sequence of function that are called to apply this check.
- Hardware Breakpoints: Hook kernel32!GetThreadContext() and modify debug registers.
- Other checks: No mitigations.
- Breakpoints scan: