Anti-debugging and anti-VM techniques and anti-emulation by Blackhat Pakistan 2023
Today we will learn about Anti-debugging and anti-VM techniques and anti-emulation.
Anti-debugging and anti-VM techniques and anti-emulation
Nowadays, malware is becoming more and more advanced. Malware analysts use a lot of debugging software and applications to analyze malware and spyware. Malware authors use some techniques to detect the presence of automated analysis systems such as debuggers and virtual machines. In this article, we’ll explore some of these commonly used techniques and practices for avoiding malware debugging software and sandboxes.
Tools needed:
- The immune debugger
- C/C++ compiler (msvc or GCC)
- Virtual Machine (Vmware of Vbox)
Introduction to debuggers
A debugger is software used to analyze and instrument executable files. To analyze and capture machine code debuggers use system calls and APIs normally provided by the operating system. To trap a single block of code, debuggers use a single stepping operation that can be turned on by setting the TRAP flag in the EFLAGS register. Debugger programs use many types of breakpoints to stop at a specific memory address. The following are the types of breakpoints used by debuggers.
- Software breakpoint.
- Hardware breakpoint.
- Memory breakpoints.
- Conditional breakpoints.
Software breakpoints are a type of breakpoint where the debugger replaces the original instruction with an INT 0xcc instruction that calls the software interrupt routine and returns to the debugger to process it. In the immunity debugger, you can display a software breakpoint by pressing ALT + b.
Also Read:Contemporary UEFI Bootkits by Blackhat Pakistan 2023
Breakpoints:
[simple]
Address module active disassembly comment00401FF0 extract It is always SHORT extract.00401FF7
00401FFC extractor Always MOV EBP,ESP
0040200A extract Always CALL DWORD PTR DS:[<&KERNEL32.ExitPr
[/simple]
Hardware breakpoints use four of the debug registers provided by the process to be incepted at a particular breakpoint. These registers include DR0, DR1, DR2, DR3
We then flip the appropriate bits in the DR7 register to enable the breakpoint and set its type and length.
Once a hardware breakpoint is set and reached, the OS will raise an INT 1 single-step event interrupt.
The debuggers then set up the appropriate handlers to catch these exceptions.
Memory breakpoint:
In the breakpoint memory we use guard pages to set the handler and if this page is accessed the exception handler is called.
Debugger supports many types of memory buffers
- memory breakpoint on BYTE access.
- memory breakpoint on WORD access.
- memory breakpoint on DWORD access.
Conditional stops:
Conditional breakpoints are managed by the debugger and are displayed to users only when certain conditions are met.
For example, you can set conditional breakpoints in an immunity debugger that has the following syntax:
STATUS = [ESP] = 0x0077ff89
Which will only be caught if the value at the top of the stack is 0x0077ff89.
Memory breakpoints are only useful if you want to monitor calls to a specific API with only certain parameters.
API debugging on Windows
By default, Windows provides debugging APIs that are used by debuggers to debug applications. The API provided by Windows is known as the Window Debug API.
The following is sample code for debugging an application using the Windows Debug API.
[simple] void EnterDebugLoop(const LPDEBUG_EVENT DebugEv){
DWORD dwContinueStatus = DBG_CONTINUE; // continue the exception
char buffer[100];
CONTEXT lcContext;
for(;;)
{
// Wait for the debug event to occur. The second parameter indicates
// that the function will not return until the debug event occurs.
WaitForDebugEvent(DebugEv, INFINITE);
// Process the debug event code.
switch (DebugEv->dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
// Handle the exception code. When handling
// exceptions, don’t forget to set continuation
// status parameter (dwContinueStatus). This value
// uses the ContinueDebugEvent function.
switch(DebugEv->u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION:
// First chance: Pass it to the system.
// Last chance: Displays the appropriate error.
break;
case EXCEPTION_BREAKPOINT:
if (!fChance)
{
dwContinueStatus = DBG_CONTINUE; // continue the exception
fChance = 1;
break;
}
lcContext.ContextFlags = CONTEXT_ALL;
GetThreadContext(pi.hThread, &lcContext);
ReadProcessMemory(pi.hProcess , (LPCVOID)(lcContext.Esp ),(LPVOID)&rtAddr, sizeof(void *), NULL );
if (DebugEv->u.Exception.ExceptionRecord.ExceptionAddress == pEntryPoint)
{
printf(“n%sn”, “Entry point reached”);
WriteProcessMemory(pi.hProcess ,DebugEv->u.Exception.ExceptionRecord.ExceptionAddress,&OrgByte, 0x01, NULL);
lcContext.ContextFlags = CONTEXT_ALL;
GetThreadContext(pi.hThread, &lcContext);
lcContext.Eip–; // Move back one byte
SetThreadContext(pi.hThread, &lcContext);
FlushInstructionCache(pi.hProcess,DebugEv->u.Exception.ExceptionRecord.ExceptionAddress,1);
dwContinueStatus = DBG_CONTINUE ; // continue the exception
putBP();
break;
}
// First chance: Displays the current one
// instruction and register values.
break;
case EXCEPTION_DATATYPE_MISALIGNMENT:
// First chance: Pass it to the system.
// Last chance: Displays the appropriate error.
dwContinueStatus = DBG_CONTINUE ;
break;
case EXCEPTION_SINGLE_STEP:
printf(“%s”, “One-step event “);
dwContinueStatus = DBG_CONTINUE ;
break;
case DBG_CONTROL_C:
// First chance: Pass it to the system.
// Last chance: Displays the appropriate error.
break;
default:
// Handle other exceptions.
break;
}
break;
case CREATE_THREAD_DEBUG_EVENT:
//dwContinueStatus = OnCreateThreadDebugEvent(DebugEv);
break;
case CREATE_PROCESS_DEBUG_EVENT:
printf(“%s”, GetFileNameFromHandle(DebugEv->u.CreateProcessInfo.hFile));
break;
case EXIT_THREAD_DEBUG_EVENT:
// Display the exit code of the thread.
//dwContinueStatus = OnExitThreadDebugEvent(DebugEv);
break;
case EXIT_PROCESS_DEBUG_EVENT:
// Display the exit code of the process.
return;
//dwContinueStatus = OnExitProcessDebugEvent(DebugEv);
break;
case LOAD_DLL_DEBUG_EVENT:
char *sDLLName;
sDLLName = GetFileNameFromHandle(DebugEv->u.LoadDll.hFile);
printf(“nDLl Loaded = %s Base Address 0x%pn”, sDLLName, DebugEv->u.LoadDll.lpBaseOfDll);
//dwContinueStatus = OnLoadDllDebugEvent(DebugEv);
break;
case UNLOAD_DLL_DEBUG_EVENT:
// Display a message that the DLL has been unloaded.
//dwContinueStatus = OnUnloadDllDebugEvent(DebugEv);
break;
case OUTPUT_DEBUG_STRING_EVENT:
// Display the debug output string.
//dwContinueStatus = OnOutputDebugStringEvent(DebugEv);
break;
case RIP_EVENT:
//dwContinueStatus = OnRipEvent(DebugEv);
break;
}
// Continue running the thread that reported the debug event.
ContinueDebugEvent(DebugEv->dwProcessId,
DebugEv->dwThreadId,
dwContinueStatus);
}
}
int main(int argc ,char **argv)
{
DEBUG_EVENT debug_event = {0};
STARTUPINFO si;
FILE *fp = fopen(argv[1], “rb”);
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
CreateProcess ( argv[1], NULL, NULL, NULL, FALSE,
DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi );
printf(“Argument passed is %sn”, Name Org);
pEntryPoint = GetEP(fp); // GET the application entry point
fclose(fp);
ReadProcessMemory(pi.hProcess ,pEntryPoint, &OrgByte, 0x01, NULL); // read the original byte at the entry point
WriteProcessMemory(pi.hProcess ,pEntryPoint,”xcc”, 0x01, NULL); // Replace byte at entry point int 0xcc
EnterDebugLoop(&debug_event); // User defined function, not API
return 0;
}
int main(int argc ,char **argv)
{
DEBUG_EVENT debug_event = {0};
STARTUPINFO si;
FILE *fp = fopen(argv[1], “rb”);
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
CreateProcess ( argv[1], NULL, NULL, NULL, FALSE,
DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi );
printf(“Argument passed is %sn”, Name Org);
pEntryPoint = GetEP(fp); // GET the application entry point
fclose(fp);
ReadProcessMemory(pi.hProcess ,pEntryPoint, &OrgByte, 0x01, NULL); // read the original byte at the entry point
WriteProcessMemory(pi.hProcess ,pEntryPoint,”xcc”, 0x01, NULL); // Replace byte at entry point int 0xcc
EnterDebugLoop(&debug_event); // User defined function, not API
return 0;
}
[/simple]
Anti-debugging techniques
Now, to frustrate the malware analyst, malware can be detected in the presence of debuggers and appear in unexpected events. In order to detect the presence of a debugger, the malware can either read some values or use the API present to detect whether the malware is being debugged or not.
One simple trick to detect the debugger is to use the winAPI function known as KERNEL32.IsDebuggerPresent.
[simple]define WIN32_LEAN_AND_MEAN
include
include
int main(int argc, char **argv)
{
if (IsDebuggerPresent())
{
MessageBox(HWND_BROADCAST, “Debugger Detected”, “”Debugger Detected””, MB_OK);
exit();
}
MessageBox(HWND_BROADCAST, “Debugger not detected”, “”Debugger not detected””, MB_OK);
return 0;
}
[/simple]
If you want more information about anti-debugging, check out this article!
Debugger detection using PEB:
When a process is created using the CreateProcess API and the creation flag is set to DEBUG_ONLY_THIS_PROCESS, a special field is set in the PEB data structure in memory.
define WIN32_LEAN_AND_MEAN
include
include
int __naked detectDebugger()
{
__asm
{
ASSUME FS:NOTHING
MOV EAX,DWORD PTR FS:[18] MOV EAX,DWORD PTR DS:[EAX+30] MOVZX EAX,BYTE PTR DS:[EAX+2] LIP
}
}
int main(int argc, char **argv)
{
if (detectDebugger())
{
MessageBox(HWND_BROADCAST, “Debugger Detected”, “”Debugger Detected””, MB_OK);
exit();
}
MessageBox(HWND_BROADCAST, “Debugger not detected”, “”Debugger not detected””, MB_OK);
return 0;
}
[/simple]
Detection using HEAP flags:
When a program is running under a debugger and is created using the debug process’s creation flags. The heap flags will change. These symptoms terminate in a different place depending on the version of the operating system.
On Windows NT-based systems, these flags exist at offset 0x0c from the base of the heap.
On Windows Vista-based systems and later, they exist at location 0x40 offset from the base of the heap.
The two initialized flags are “Power Flags” and “Symptoms”.
The ProcessHeap base points towards the _HEAP structure are defined as:
Link: http://www.nirsoft.net/kernel_struct/vista/HEAP.html
[simple] typedef struct _HEAP{
HEAP_ENTRY entry;
ULONG segment signature;
ULONG SegmentFlags;
LIST_ENTRY SegmentListEntry;
Heap PHEAP;
PVOID BaseAddress;
ULONG Number of pages;
PHEAP_ENTRY FirstEntry;
PHEAP_ENTRY LastValidEntry;
ULONG NumberOfUnCommittedPages;
ULONG NumberOfUnCommittedRanges;
WORD SegmentAllocatorBackTraceIndex;
WORD reserved;
LIST_ENTRY UCRSegmentList;
ULONG flags;
ULONG ForceFlags;
ULONG CompatibilityFlags;
ULONG EncodeFlagMask;
HEAP_ENTRY encoding;
ULONG PointerKey;
ULONG Interceptor;
ULONG VirtualMemoryThreshold;
ULONG signature;
ULONG SegmentReserve;
ULONG Segment Commit;
ULONG DeCommitFreeBlockThreshold;
ULONG DeCommitTotalFreeThreshold;
ULONG TotalFreeSize;
ULONG MaximumAllocationSize;
WORD ProcessHeapsListIndex;
Word HeaderValidateLength;
PVOID HeaderValidateCopy;
WORD NextAvailableTagIndex;
WORD MaximumTagIndex;
PHEAP_TAG_ENTRY TagEntries;
LIST_ENTRY URLlist;
ULONG AlignRound;
ULONG AlignMask;
LIST_ENTRY VirtualAllocdBlocks;
LIST_ENTRY SegmentList;
WORD AllocatorBackTraceIndex;
ULONG NonDedicatedListLength;
PVOID BlocksIndex;
PVOID UCRIndex;
PHEAP_PSEUDO_TAG_ENTRY PseudoTagEntries;
LIST_ENTRY FreeLists;
PHEAP_LOCK LockVariable;
LONG * CommitRoutine;
PVOID FrontEndHeap;
WORD FrontHeapLockCount;
UCHAR FrontEndHeapType;
HEAP_COUNTERS Counters;
HEAP_TUNING_PARAMETERS TuningParameters;
} HEAP, *HEAP;
[/simple]
The following C program can be used to detect the presence of a debugger using heap flags
[C]int main(int argc, char* argv[])
{
unsigned int var;
__asm
{
MOV EAX, FS:[0x30];
MOV EAX, [EAX + 0x18];
MOV EAX, [EAX + 0x0c];
MOV var, EAX
}
if(var != 2)
{
printf(“Debugger detected”);
}
return 0;
}
[/C]
Virtual machine detection or emulation detection
Malware samples are typically analyzed by analysts in an isolated environment such as a virtual machine. To thwart sample analysis inside a virtual machine, include malware anti-vm protection or simply terminate when the malware is running in an isolated environment.
The following techniques can be used to determine if a sample is running inside a virtual machine.
- Based on timing.
- Artifacts based.
Timing based detection
“The Time Stamp Counter (TSC) is a 64-bit register present on all x86 processors from Pentium. It counts the number of cycles since reset”. (Wikipedia)
If the code is then emulated, the timestamp will change in the meantime.
The result is saved in EDX:EAX format
Now the time difference on a real host computer would usually be less than 100, but if the code is emulated the difference will be huge.
[C]int main(int argc, char* argv[])
{
unsigned int time1 = 0;
unsigned int time2 = 0;
__asm
{
RDTSC
MOV time1, EAX
RDTSC
MOV time2, EAX
}
if ((time2 – time1) > 100)
{
printf(“%s”, “VM detected”);
return 0;
}
printf(“%s”, “VM not present”);
return 0;
}
[/C]
The above program uses timestamp instructions to detect the presence of a virtual machine.
Artifact based detection
Malware exploits the presence of virtual machine configuration based on file, network, or device artifacts. Malware typically checks for these artifacts to detect the presence of a debugger or virtual environment.
The best case would be registry artifacts, Vmware will create registry keys for the virtual disk controller that can be found in the registry using the following key.
HKLMSYSTEMCurrentControlSetServicesDiskEnum�
like “SCSIDisk&Ven_VMware_&Prod_VMware_Virtual_S&Rev_1.04&XXX&XXX”
int main(int argc, char **argv)
{
char lszValue[100];
HKEY hKey;
int i=0;
RegOpenKeyEx (HKEY_LOCAL_MACHINE, “SYSTEMCurrentControlSetServicesDiskEnum”, 0L, KEY_READ , &hKey);
RegQueryValue(hKey,”0″,lszValue,sizeof(lszValue));
printf(“%s”, lszValue);
if (strstr(lszValue, “VMware”))
{
printf(“Vmware detected”);
}
RegCloseKey(hKey);
return 0;