Windows - Using the Debugger API

08 Apr, 21
Tags:
34
5
0
WINDBG_LOGO

The Windows Debugger API allows interacting with a dump file or active debugger session and using the symbols for each module. This lets us automate complicated operations that might be a pain to repeatedly do in WinDBG. We can also use the debugger API to write debugger extensions, which got a lot easier to write. The debugger API is documented here, but it can be hard to figure out where to start, so I'll include here just the basics. A couple of important things to know before you start:

  • To use the debugger API you need to link your C/C++ code against DbgEng.lib and include the header DbgEng.h.
  • Once the application is compiled, copy dbgeng.dll, dbghelp.dll and symsrv.dll from the SDK (they will be in the same folder as WinDBG) into the same directory as your app. This will make your app load those copied of the DLLs instead of the ones in System32 which are often broken.

To access the debugger API, declare and initialize 4 IDebug variables:

  • IDebugClient - gives us access to the debugger session or dump file.
  • IDebugSymbols - gives us access to symbols and allows us to use the types, functions and variables that are part of symbol files.
  • IDebugDataSpaces - gives us access to the data inside the dump file or debugger session. Lets us read and write to virtual memory, physical memory, IO and MSRs.
  • IDebugControl - gives us control over the debugger session. Lets us execute commands, set breakpoints (if this is an active debugger session), dump stack trace, load and unload extension libraries, etc.

To Initialize these variables:

debugClient = nullptr;
debugSymbols = nullptr;
dataSpaces = nullptr;
debugControl = nullptr;
result = DebugCreate(__uuidof(IDebugClient), (PVOID*)&debugClient);
if (!SUCCEEDED(result))
{
    printf("DebugCreate failed with error 0x%x\n", result);
    return;
}
result = debugClient->QueryInterface(__uuidof(IDebugSymbols), (PVOID*)&debugSymbols);
if (!SUCCEEDED(result))
{
    printf("QueryInterface for debug symbols failed with error 0x%x\n", result);
    return;
}
result = debugClient->QueryInterface(__uuidof(IDebugDataSpaces), (PVOID*)&dataSpaces);
if (!SUCCEEDED(result))
{
    printf("QueryInterface for debug data spaces failed with error 0x%x\n", result);
    return;
}
result = debugClient->QueryInterface(__uuidof(IDebugControl), (PVOID*)&debugControl);
if (!SUCCEEDED(result))
{
    printf("QueryInterface for debug control failed with error 0x%x\n", result);
    return;
}

Then you can either open a crash dump:

debugClient->OpenDumpFile("c:\\temp\\live.dmp");

Or connect to a debugger (in this case, a kernel debugger):

result = debugControl->AddEngineOptions(DEBUG_ENGOPT_INITIAL_BREAK);
if (!SUCCEEDED(result))
{
    printf("OpenDumpFile failed with error 0x%x\n", result);
    return;
}
result = debugClient->AttachKernel(DEBUG_ATTACH_KERNEL_CONNECTION, "net:port:50000,key:1.1.1.1,target:1.2.3.4");
if (!SUCCEEDED(result))
{
    printf("OpenDumpFile failed with error 0x%x\n", result);
    return;
}

And in both cases, wait for the session to be loaded together with all of the symbol files:

//
// You can choose some reasonable timeout, I chose INFINITE because waiting for symbol files can be slow
//
debugControl->WaitForEvent(DEBUG_WAIT_DEFAULT, INFINITE);

Now you can interact with your session. You can get the base addresses of modules, which is necessary to get types and symbols from them. Getting the base address of Ntoskrnl.exe is easy, since it's needed often enough that it received an index and can be queried through ReadDebuggerData:

ULONG64 kernBase;
result = dataSpaces->ReadDebuggerData(DEBUG_DATA_KernBase,
                                      &kernBase,
                                      sizeof(kernBase),
                                      nullptr);

We can do this because DEBUG_DATA_KernBase is common enough to have an index. For other modules we'd need to use debugSymbols->GetModuleByModuleName.

Once we have the base address of the module we are interested in, we can get symbols from it, such as the EPROCESS type from ntoskrnl.exe. To use types we call GetTypeId to retrieve the ID of the type:

ULONG EPROCESS;
debugSymbols->GetTypeId(kernBase, "_EPROCESS", &EPROCESS);

We can use it to search for field offsets in the structure:

ULONG imageFileNameOffset;
debugSymbols->GetFieldOffset(kernBase,
                             EPROCESS,
                             "ImageFileName",
                             &imageFileNameOffset);

Get its size:

ULONG eprocessSize;
debugSymbols->GetTypeSize(kernBase,
                          EPROCESS,
                          &eprocessSize);

Or read the whole structure (remember to allocate enough memory first!) - we don't have an address for an actual EPROCESS so we'll pretend that the EPROCESS structure we want to read is in 0x12345678:

ULONG64 eprocAddress;
PVOID localEprocBuffer;

eprocAddress = 0x12345678
localEprocBuffer = VirtualAlloc(NULL,
                                eprocessSize,
                                MEM_COMMIT,
                                PAGE_READWRITE);

debugSymbols->ReadTypedDataVirtual(eprocAddress,
                                   kernBase,
                                   EPROCESS,
                                   localEprocBuffer,
                                   eprocessSize,
                                   NULL);
                                   

We can also read untyped data with ReadVirtual and specify the number of bytes we want to read. We can use GetOffsetByName, which gives us the offset of a symbol, to get nt!PsInitialSystemProcess, read the address of the system process from it using ReadVirtual and then read the EPROCESS structure using ReadTypedDataVirtual:

ULONG64 pInitialSystemProcess;
ULONG64 eprocAddress;
PVOID localEprocBuffer;

//
// Get the address of nt!PsInitialSystemProcess - a pointer to the System process
//
result = dataSpaces->GetOffsetByName("nt!PsInitialSystemProcess",
                                      &pInitialSystemProcess);
if (!SUCCEEDED(result))
{
    printf("GetOffsetByName failed with error 0x%x\n", result);
    return;
}
//
// Read the address of the System process
//
result = dataSpaces->ReadVirtual(pInitialSystemProcess,
                                 &eprocAddress,
                                 sizeof(eprocAddress),
                                 nullptr);
if (!SUCCEEDED(result))
{
    printf("ReadVirtual failed with error 0x%x\n", result);
    return;
}
//
// Allocate a local buffer for the EPROCESS
//
localEprocBuffer = VirtualAlloc(NULL,
                                eprocessSize,
                                MEM_COMMIT,
                                PAGE_READWRITE);
if (localEprocBuffer == nullptr)
{
    printf("Failed allocating memory\n");
    return;
}
//
// Read the System process into local memory
//
result = debugSymbols->ReadTypedDataVirtual(eprocAddress,
                                            kernBase,
                                            EPROCESS,
                                            localEprocBuffer,
                                            eprocessSize,
                                            NULL);
if (!SUCCEEDED(result))
{
    printf("ReadTypedDataVirtual failed with error 0x%x\n", result);
    return;
}

There is a lot more you can do with the debugger API, but these few simple commands will get you pretty far and can be used to create relatively complex and very useful tools for forensics and memory analysis.