Implementing Debug Event Handlers

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

0x1

EXCEPTION_DEBUG_EVENT

u.Exception

0x2

CREATE_THREAD_DEBUG_EVENT

u.CreateThread

0x3

CREATE_PROCESS_DEBUG_EVENT

u.CreateProcessInfo

0x4

EXIT_THREAD_DEBUG_EVENT

u.ExitThread

0x5

EXIT_PROCESS_DEBUG_EVENT

u.ExitProcess

0x6

LOAD_DLL_DEBUG_EVENT

u.LoadDll

0x7

UNLOAD_DLL_DEBUG_EVENT

u.UnloadDll

0x8

OUPUT_DEBUG_STRING_EVENT

u.DebugString

0x9

RIP_EVENT

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.

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.


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.