Home > General x86, General x86-64, Programming, Reverse Engineering > Writing a Primitive Debugger: Part 1 (Basics)

Writing a Primitive Debugger: Part 1 (Basics)

November 27th, 2014 Leave a comment Go to comments

As software developers (or reverse engineers), debuggers are an invaluable tool. They allow for runtime analysis and introspection/understanding of the program in order to find out how it works — or oftentimes doesn’t. This series of posts will go in to how they work and how to begin developing a primitive debugger targeting the Windows platform running on the x86 or x64 architectures.

Attaching/Detaching
In order to debug a process, a debugger must be able to attach to it. This means that there should be a way for the debugger to interact with the process in such a way that the debugger will have access to the processes address space, the ability to halt and continue execution, modify registers, and so on. Likewise, a debugger should be able to safety detach from a process and let it continue running when a debugging session is finished. On the Windows platform this is achieved by calling the DebugActiveProcess function and specifying the process identifier of the target. Alternatively, it can also be accomplished by calling CreateProcess with the DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS creation flags. This latter method will create a new process and attach to it rather than attaching to one already running on the system. Once attached, the debugger can specify a policy on whether to kill the process on detach with DebugSetProcessKillOnExit. Lastly, the process for detaching is a straightforward call to DebugActiveProcessStop. Putting all of these together produces code similar to the following:

const bool Debugger::Start()
{
    m_bIsActive = BOOLIFY(DebugActiveProcess(m_dwProcessId));
    if (m_bIsActive)
    {
        const bool bIsSuccess = BOOLIFY(DebugSetProcessKillOnExit(m_bKillOnExit));
        if (!bIsSuccess)
        {
            fprintf(stderr, "Could not set process kill on exit policy. Error = %X\n", GetLastError());
        }
        return DebuggerLoop();
    }
    else
    {
        fprintf(stderr, "Could not debug process %X. Error = %X\n", m_dwProcessId, GetLastError());
    }
 
    return false;
}
 
const bool Debugger::Stop()
{
    m_bIsActive = BOOLIFY(DebugActiveProcessStop(m_dwProcessId));
    if (!m_bIsActive)
    {
        fprintf(stderr, "Could not stop debugging process %X. Error = %X\n", m_dwProcessId, GetLastError());
    }
 
    return m_bIsActive;
}

where m_dwProcessId and m_bKillOnExit are parameters provided through the Debugger constructor (see sample code). The BOOLIFY macro is also just a simple

#define BOOLIFY(x) !!(x)

definition to encourage type safety. At this point we have a simple debugger that can attach to and detach from a target process, but cannot handle any debug events. This is where the debugging loop comes in.

The Debugging Loop
The core component of any debugger is the debugging loop. This is the part responsible for waiting for a debug event, processing said event, and then waiting for the next event, hence the loop. On the Windows platform, this is pretty straightforward. It’s actually straightforward enough that Microsoft wrote up a short MSDN page on what needs to be done at this step. The steps involved are that (in a loop), the debugger calls WaitForDebugEvent which waits for a debug event from the process. These debug events are enumerated here and are commonly events related to process and thread creation/destruction, loading or unloading of DLLs, any exceptions raised, or debug strings output specifically for a debugger to see. Once this function returns, it will populate a DEBUG_EVENT structure with the information related to the particular debug event. At this point, it is the debuggers job to handle the event. After handling the event, the handlers must provide a continue status to ContinueDebugEvent, which is a code telling the thread that raised the event how to carry on execution after the event was handled. For most events, i.e. CREATE_PROCESS_DEBUG_EVENT, LOAD_DLL_DEBUG_EVENT, etc, you want to continue execution since these events do not reflect anything wrong with program behavior, but are events to notify the debugger of changing process state. This is done by choosing DBG_CONTINUE as the continue status. For the exceptional cases, such as exceptions which lead to undefined program behavior such as access violations, illegal instruction execution, divides by zero, etc, the process is nearing a point of no return. The debuggers job at this point is to gather and display information relating to the crash and in most cases terminate the process. This termination can happen inside the handler itself for these exceptions, or the debugger can choose to continue the debug event with the DBG_EXCEPTION_NOT_HANDLED continue status, meaning that the debugger is relinquishing responsibility of handling this exception properly. In almost all cases, this will lead to the program terminating on its own immediately afterwards. However, there are sometimes corner cases, particularly in malware, where the process will install its own runtime exception handler as an obfuscation technique and produce its own runtime exceptions to be handled within this handler to carry out some functionality. Continuing the exception in this case would not result in a crash since the process is able to handle its own exception after the debugger forwards it along the exception handler chain. Putting cases like those aside for now, a typical debugging loop may look like the following:

const bool Debugger::DebuggerLoop()
{
    DEBUG_EVENT dbgEvent = { 0 };
    DWORD dwContinueStatus = 0;
    bool bSuccess = false;
 
    while (m_bIsActive)
    {
        bSuccess = BOOLIFY(WaitForDebugEvent(&dbgEvent, INFINITE));
        if (!bSuccess)
        {
            fprintf(stderr, "WaitForDebugEvent returned failure. Error = %X\n", GetLastError());
            return false;
        }
 
        m_pEventHandler->Notify((DebugEvents)dbgEvent.dwDebugEventCode, dbgEvent);
        dwContinueStatus = m_pEventHandler->ContinueStatus();
 
        bSuccess = BOOLIFY(ContinueDebugEvent(dbgEvent.dwProcessId, dbgEvent.dwThreadId, dwContinueStatus));
        if (!bSuccess)
        {
            fprintf(stderr, "ContinueDebugEvent returned failure. Error = %X\n", GetLastError());
            return false;
        }
    }
 
    return true;
}

with m_pEventHandler being responsible for registering handlers and setting a continue status to be passed along to ContinueDebugEvent. The example code registers handlers for events/exceptions and outputs information relevant to each event/exception. The style below is followed for all events and exceptions:

Register(DebugEvents::eCreateThread, [&](const DEBUG_EVENT &dbgEvent)
{
    auto &info = dbgEvent.u.CreateThread;
    fprintf(stderr, "CREATE_THREAD_DEBUG_EVENT received.\n"
        "Handle: %X\n"
        "TLS base: %X\n"
        "Start address: %X\n",
        info.hThread, info.lpThreadLocalBase, info.lpStartAddress);
    SetContinueStatus(DBG_CONTINUE);
});
 
Register(DebugEvents::eCreateProcess, [&](const DEBUG_EVENT &dbgEvent)
{
    auto &info = dbgEvent.u.CreateProcessInfo;
    fprintf(stderr, "CREATE_PROCESS_DEBUG_EVENT received.\n"
        "Handle (image file): %X\n"
        "Handle (process): %X\n"
        "Handle (main thread): %X\n"
        "Image base address: %X\n"
        "Debug info file offset: %X\n"
        "Debug info size: %X\n"
        "TLS base: %X\n"
        "Start address: %X\n",
        info.hFile, info.hProcess, info.hThread, info.lpBaseOfImage,
        info.dwDebugInfoFileOffset, info.nDebugInfoSize, info.lpThreadLocalBase,
        info.lpStartAddress);
 
    m_hProcess = info.hProcess;
 
    SetContinueStatus(DBG_CONTINUE);
});

That should be all that is really needed to get  started on creating a basic debugger. This debugger features the ability attach/detach, handle debug events, and output information pertaining to these events. To test this out, we can create a simple program to generate an exception that will be caught by the debugger when it is attached.

#include <stdio.h>
#include <Windows.h>
 
int main(int argc, char *argv[])
{
    printf("Press enter to raise an exception.\n");
    (void)getchar();
    if (IsDebuggerPresent())
    {
        OutputDebugStringA("This should be seen by the debugger.\n");
        RaiseException(STATUS_ACCESS_VIOLATION, 0, 0, nullptr);
    }
    else
    {
        printf("Process was not being debugged.\n");
    }
 
    return 0;
}

Here is the output of the debugger upon attaching to the process and receiving the access violation:

CREATE_PROCESS_DEBUG_EVENT received.
Handle (image file): 4C
Handle (process): 48
Handle (main thread): 44
Image base address: EE0000
Debug info file offset: 0
Debug info size: 0
TLS base: 7F03F000
Start address: 0
LOAD_DLL_DEBUG_EVENT received.
Handle: 54
Base address: 77040000
Debug info file offset: 0
Debug info size: 0
Name: \\?\C:\Windows\SysWOW64\ntdll.dll
LOAD_DLL_DEBUG_EVENT received.
Handle: 5C
Base address: 76A00000
Debug info file offset: 0
Debug info size: 0
Name: \\?\C:\Windows\SysWOW64\kernel32.dll
LOAD_DLL_DEBUG_EVENT received.
Handle: 50
Base address: 765D0000
Debug info file offset: 0
Debug info size: 0
Name: \\?\C:\Windows\SysWOW64\KernelBase.dll
LOAD_DLL_DEBUG_EVENT received.
Handle: 60
Base address: F7F0000
Debug info file offset: 0
Debug info size: 0
Name: \\?\C:\Windows\SysWOW64\msvcr120d.dll
CREATE_THREAD_DEBUG_EVENT received.
Handle: 64
TLS base: 7F03C000
Start address: 770EBCFC
Received exception event.
First chance exception: 1
Exception code: 80000003
Exception flags: 0
Exception address: 770670BC
Number parameters (associated with exception): 1
Received breakpoint
EXIT_THREAD_DEBUG_EVENT received.
Thread 1B20 exited with code 0.
OUTPUT_DEBUG_STRING_EVENT received.
Debug string: This should be seen by the debugger.

Received exception event.
First chance exception: 1
Exception code: C0000005
Exception flags: 0
Exception address: 765E2F71
Number parameters (associated with exception): 0
Received access violation
Received exception event.
First chance exception: 0
Exception code: C0000005
Exception flags: 0
Exception address: 765E2F71
Number parameters (associated with exception): 0
Received access violation
EXIT_PROCESS_DEBUG_EVENT received.
Process 390 exited with code C0000005.

As you can see from the output, the debugger receives a CREATE_PROCESS_DEBUG_EVENT upon attaching. This event is always the first one triggered upon a debugger attaching and lets the debugger obtain a handle to the process that lets the debugger read/write from process memory, change the processes thread contexts, etc. It also may give information about any sort of debug information relevant to the process. Following that are the events related to any loaded DLLs in the process address space. Afterwards two interesting events come up. They are a CREATE_THREAD_DEBUG_EVENT and an exception with an exception code corresponding to EXCEPTION_BREAKPOINT. These are covered in the section below. Lastly, the debugger successfully displays the debug string provided by the process, and shows the access violation also being successfully received. Since the debugger sample code does not terminate the process, you can see the exception being raised multiple times. Initially it is raised as a first chance exception, but comes back around around as a second/last chance exception since the target process was not able to handle it. The process then terminates with a status code corresponding to EXCEPTION_ACCESS_VIOLATION.

What actually happens when a debugger is attached?
One current mystery about the debugger output may be where those CREATE_THREAD_DEBUG_EVENT, breakpoint, and EXIT_THREAD_DEBUG_EVENT events came from. These events are triggered as a result of the debugger attaching to the process. When the DebugActiveProcess is called, it forwards on to the NtDebugActiveProcess syscall. This syscall is responsible for setting up the process to be in a debugable state, which at the very least involves changing the BeingDebugged flag of the Process Environment Block for the target process — this is how the IsDebuggerPresent function works in the target process to check if it is being debugged. Afterwards, a thread will be created in the target process. This thread will have a start address that corresponds to an 0xCC (int 3) instruction, better known as a breakpoint on x86 and x64 architectures. This is why the debugger displays as having received a breakpoint. When execution is continued, this thread exits the process begins executing normally again.

Article Roadmap
Future posts will be related on topics closely following the items below:

  • Basics
  • Adding/Removing Breakpoints, Single-stepping
  • Call Stack, Registers, Contexts
  • Symbols
  • Miscellaneous Features

The full source code relating to this can be found here. C++11 features were used, so MSVC 2012/2013 is most likely required.

  1. November 28th, 2014 at 00:51 | #1

    It’s worth pointing out that on Windows a 32-bit debugger process can only debug 32-bit targets, and a 64-bit debugger process can only debug 64-bit processes. Knowing this can save a lot of frustration.

    Visual Studio hides this by transparently inserting a 64-bit broker process when debugging 64-bit processes.

  2. November 28th, 2014 at 13:13 | #2

    Note that on Windows 32-bit debuggers can only debug 32-bit processes, and similarly for 64-bit debuggers/processes. Visual Studio works around this by using a proxy process for debugging 64-bit processes.

    • kaze
      November 29th, 2014 at 02:37 | #3

      @Bruce: 64 bits debuggers can debug 32 bits processes.

  3. Sirmabus
    December 4th, 2014 at 19:22 | #4

    What would I’d like to see (on my “TODO” list) is debugger that is not a debugger in the conventional Windows sense.
    It would exist mainly as a DLL that you attach to the process you want to debug.

    Why:
    A) You get around the Windows debug weirdness. The Debug message/interrupt/exception spam, and the strange side effects of putting a process in “debugged mode”, etc.

    B) One need not worry much, or could at least mitigate, anti-debugger provisions that the target processes might have.
    To avoid problems with processes that have anti-debugger detection/protections, or for use with malware that changes it’s behavior when a debugger is attached, etc.
    Of all the checks that can be done to check for a debugger, you won’t have any.
    Although of course if there is a public pattern for it, detection could be added.

    You can do all the debugger in R3 by handling the various exceptions your self.
    You can drop int3 breakpoints, single step, use HWBP, what ever.
    And if you hook the exception entry point in ntdll.dll (“KiUserExceptionDispatcher”?? off the top of my head) this stuff will be mostly transparent to the process.

    Might be even better in R0 (as a driver) to sort of minimally emulate sort of what the existing Windows debug kernel API does (sans garbage and complexity).

    You could have the UI from the DLL, maybe popup with a hot key like Softice did, or use some sort of IPC to talk to a fancy front end, etc.

    • admin
      December 4th, 2014 at 20:42 | #5

      It’s certainly possible. With the ability to add runtime exception handlers via AddVectoredExceptionHandler, or even hooking at the base level at KiUserExceptionDispatcher to filter exceptions as you suggest, there is no reason why it wouldn’t be feasible. I’ve certainly done it a fair number of times and have detailed it in previous posts.
      With regards to it being less detectable, that is true on some level. However, its not a free lunch. You’re getting rid of having to worry about the target process having anti-debug protection such as seeing if a debugger is attached at the cost of being more invasive to the processes’ address space, since you’re loading in a DLL and probably setting up external communication with some client front-end. You can always take protective measures such as removing your loaded DLL from the linked list of loaded DLLs in the PEB, but that’s not a guarantee of undetectability either.
      You’re also not stopping anti-debug measures such as seeing the amount of time elapsed between sequences of instructions executed (these sequences will be much longer if you’re stepping them than if they were executing otherwise). Given the literally hundreds
      (PDF warning) of anti-debugging techniques, it’s a bit hard to tell.
      Overall, it seems like an interesting side project and I’d be interested in seeing the analysis success rate of a debugger designed as an injectable module rather than as a process that attaches to another one.

  1. No trackbacks yet.