For our debugger to take action upon certain events, we need to establish handlers for each debugging event that can occur. If we
refer back to the WaitForDebugEvent()
function, we
know that it returns a populated DEBUG_EVENT
structure whenever a debugging event occurs. Previously we were ignoring
this struct and just automatically continuing the process, but now we
are going to use information contained within the struct to determine
how to handle a debugging event. The DEBUG_EVENT
structure is defined like this:
typedef struct DEBUG_EVENT { DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union { EXCEPTION_DEBUG_INFO Exception; CREATE_THREAD_DEBUG_INFO CreateThread; CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; EXIT_THREAD_DEBUG_INFO ExitThread; EXIT_PROCESS_DEBUG_INFO ExitProcess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO DebugString; RIP_INFO RipInfo; }u; };
There is a lot of useful information in this struct. The
dwDebugEventCode
is of particular interest, as it
dictates what type of event was trapped by the
WaitForDebugEvent()
function. It also dictates the
type and value for the u
union. The various
debug events based on their event codes are shown in Table 3-1.
Table 3-1. Debugging Events
Event Code | Event Code Value | Union u Value |
---|---|---|
|
| u.Exception |
|
| u.CreateThread |
|
| u.CreateProcessInfo |
|
| u.ExitThread |
|
| u.ExitProcess |
|
| u.LoadDll |
|
| u.UnloadDll |
|
| u.DebugString |
|
| u.RipInfo |
By inspecting the value of dwDebugEventCode
, we
can then map it to a populated structure as defined by the value stored
in the u
union. Let's modify our debug loop to show
us which event has been fired based on the event code. Using that
information, we will be able to see the general flow of events after we
have spawned or attached to a process. We'll update
my_debugger.py as well as our
my_test.py test script.
... class debugger(): def __init__(self): self.h_process = None self.pid = None self.debugger_active = False self.h_thread = None self.context = None ... def get_debug_event(self): debug_event = DEBUG_EVENT() continue_status= DBG_CONTINUE if kernel32.WaitForDebugEvent(byref(debug_event),INFINITE): # Let's obtain the thread and context information self.h_thread = self.open_thread(debug_event.dwThreadId) self.context = self.get_thread_context(self.h_thread) print "Event Code: %d Thread ID: %d" % (debug_event.dwDebugEventCode, debug_event.dwThreadId) kernel32.ContinueDebugEvent( debug_event.dwProcessId, debug_event.dwThreadId, continue_status )
import my_debugger debugger = my_debugger.debugger() pid = raw_input("Enter the PID of the process to attach to: ") debugger.attach(int(pid)) debugger.run() debugger.detach()
Again, if we use our good friend calc.exe, the output from our script should look similar to Example 3-2.
Example 3-2. Event codes when attaching to a calc.exe process
Enter the PID of the process to attach to: 2700
Event Code: 3 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 2 Thread ID: 3912
Event Code: 1 Thread ID: 3912
Event Code: 4 Thread ID: 3912
So based on the output of our script, we can see that a
CREATE_PROCESS_EVENT (0x3)
gets fired first,
followed by quite a few LOAD_DLL_DEBUG_EVENT
(0x6)
events and then a CREATE_THREAD_DEBUG_EVENT
(0x2)
. The next event is an EXCEPTION_DEBUG_EVENT
(0x1)
, which is a Windows-driven breakpoint that allows a
debugger to inspect the process's state before resuming execution.
The last call we see is EXIT_THREAD_DEBUG_EVENT
(0x4)
, which is simply the thread with TID
3912
ending its execution.
The exception event is of particular interest, as exceptions can include breakpoints, access violations, or improper access permissions on memory (attempting to write to a read-only portion of memory, for example). All of these subevents are important to us, but let's start with catching the first Windows-driven breakpoint. Open my_debugger.py and insert the following code.
... class debugger(): def __init__(self): self.h_process = None self.pid = None self.debugger_active = False self.h_thread = None self.context = None self.exception = None self.exception_address = None ... def get_debug_event(self): debug_event = DEBUG_EVENT() continue_status= DBG_CONTINUE if kernel32.WaitForDebugEvent(byref(debug_event),INFINITE): # Let's obtain the thread and context information self.h_thread = self.open_thread(debug_event.dwThreadId) self.context = self.get_thread_context(self.h_thread) print "Event Code: %d Thread ID: %d" % (debug_event.dwDebugEventCode, debug_event.dwThreadId) # If the event code is an exception, we want to # examine it further. if debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT: # Obtain the exception code exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress if exception == EXCEPTION_ACCESS_VIOLATION: print "Access Violation Detected." # If a breakpoint is detected, we call an internal # handler. elif exception == EXCEPTION_BREAKPOINT: continue_status = self.exception_handler_breakpoint() elif ec == EXCEPTION_GUARD_PAGE: print "Guard Page Access Detected." elif ec == EXCEPTION_SINGLE_STEP: print "Single Stepping." kernel32.ContinueDebugEvent( debug_event.dwProcessId, debug_event.dwThreadId, continue_status ) ... def exception_handler_breakpoint(): print "[*] Inside the breakpoint handler." print "Exception Address: 0x%08x" % self.exception_address return DBG_CONTINUE
If you rerun your test script, you should now see the output
from the soft breakpoint exception handler. We have also created
stubs for hardware breakpoints
(EXCEPTION_SINGLE_STEP
) and memory breakpoints
(EXCEPTION_GUARD_PAGE
). Armed with our new
knowledge, we can now implement our three different breakpoint types
and the correct handlers for each.