RCE Endeavors 😅

May 25, 2023

Function Hooking: Hardware Breakpoints (3/7)

Filed under: Programming,Reverse Engineering — admin @ 11:43 PM

Table of Contents:

Inline hooking is nice, but it has one noticeable drawback: you need to overwrite instructions in the program’s memory. While this is typically not an issue, you may run into an application that performs integrity checks on its executable code. Bypassing integrity checks can be a tedious process, but there may be an easier way.

Debug registers

The x86 and x64 architectures provide a set of registers that are useful for debugging. Four debug registers, Dr0, Dr1, Dr2, and Dr3 can hold an address to set a breakpoint on. These breakpoints can be enabled or disabled through the debug control register, Dr7. When a breakpoint has been hit, there is a debug status register, Dr6, that holds information about the breakpoint. The Dr6 and Dr7 registers are described in more detail in the tables below:

BitsDescription
0Breakpoint at address in Dr0 was hit
1Breakpoint at address in Dr1 was hit
2Breakpoint at address in Dr2 was hit
3Breakpoint at address in Dr3 was hit
4-13
14Single-step execution
15 – 63
The bit fields in the Dr6 debug status register that are of interest.
BitsDescription
0Local enable for breakpoint in Dr0
1Global enable for breakpoint in Dr0
2Local enable for breakpoint in Dr1
3Global enable for breakpoint in Dr1
4Local enable for breakpoint in Dr2
5Global enable for breakpoint in Dr2
6Local enable for breakpoint in Dr3
7Global enable for breakpoint in Dr3
8 – 12
13General Detect Enable. Set to 1 on a breakpoint hit.
14 – 15
16 – 17Breakpoint condition for breakpoint in Dr0
18 – 19Breakpoint length for breakpoint in Dr0
20 – 21Breakpoint condition for breakpoint in Dr1
22 – 23Breakpoint length for breakpoint in Dr1
24 – 25Breakpoint condition for breakpoint in Dr2
26 – 27Breakpoint length for breakpoint in Dr2
28 – 29Breakpoint condition for breakpoint in Dr3
30 – 31Breakpoint length for breakpoint in Dr3
32 -63
The bit fields in the Dr7 debug control register that are of interest.
Value (binary)Break condition
00Instruction execution
01Data writes
10I/O reads and writes
11Data reads and writes
The bit values and corresponding break conditions for the Dr7 debug control register condition fields.
Value (binary)Breakpoint length
001 byte
012 bytes
108 bytes
114 bytes
The bit values and corresponding lengths for the Dr7 debug control register length fields.

In Windows, you can see these registers when inspecting the CONTEXT structure, which contains processor-specific register data, among other fields used internally by Windows. The relevant debug register fields are shown below:

typedef struct DECLSPEC_ALIGN(16) DECLSPEC_NOINITALL _CONTEXT {

    // ...


    //
    // Debug registers
    //

    DWORD64 Dr0;
    DWORD64 Dr1;
    DWORD64 Dr2;
    DWORD64 Dr3;
    DWORD64 Dr6;
    DWORD64 Dr7;

    // ...

} CONTEXT, *PCONTEXT;

The supported debug registers in the CONTEXT structure.

Although it may seem unintuitive at first, setting a breakpoint on a function is a way to hook it. When your breakpoint gets hit, execution will transfer to an exception handler, which you will have written. Conceptually, this exception handler can be a hook function; from within the handler, you can completely take over execution just as was done in the inline hooking examples. Hardware breakpoints have a couple of caveats though: since there are only four debug registers, you are limited to hooking four functions at most. Also, hardware breakpoints can only be set by modifying the context of the main thread, which is not something that is provided out of the box by the Windows API.

Getting the main thread id

Fortunately, getting the context of the main thread is not too difficult. The Windows API provides helpful functions to enumerate all threads in a process, and to get additional information about them. One useful bit of information is the threads creation time. You can take advantage of the fact that the main thread will (likely) be the first thread that is created and run when an executable is loaded, meaning it will have the earliest creation time. Knowing this, it is a matter of writing the appropriate code to capture all of the threads, iterate through them, and keep track of the thread with the earliest creation time. The GetMainThreadId function below does just that:

DWORD GetMainThreadId(const HANDLE processHandle) {

    std::shared_ptr<HPSS> snapshot(new HPSS{}, [&](HPSS* snapshotPtr) {
        PssFreeSnapshot(processHandle, *snapshotPtr);
    });

    auto result{ PssCaptureSnapshot(processHandle,
        PSS_CAPTURE_THREADS, 0, snapshot.get()) };
    if (result != ERROR_SUCCESS) {
        PrintErrorAndExit("PssCaptureSnapshot");
    }

    std::shared_ptr<HPSSWALK> walker(new HPSSWALK{}, [&](HPSSWALK* walkerPtr) {
        PssWalkMarkerFree(*walkerPtr);
    });

    result = PssWalkMarkerCreate(nullptr, walker.get());
    if (result != ERROR_SUCCESS) {
        PrintErrorAndExit("PssWalkMarkerCreate");
    }

    DWORD mainThreadId{};
    FILETIME lowestCreateTime{ MAXDWORD, MAXDWORD };

    PSS_THREAD_ENTRY thread{};

    // Iterate through the threads and keep track of the one
    // with the lowest creation time.
    while (PssWalkSnapshot(*snapshot, PSS_WALK_THREADS,
        *walker, &thread, sizeof(thread)) == ERROR_SUCCESS) {
        if (CompareFileTime(&lowestCreateTime, &thread.CreateTime) == 1) {
            lowestCreateTime = thread.CreateTime;
            mainThreadId = thread.ThreadId;
        }
    }

    return mainThreadId;
}

The GetMainThreadId function iterates through all threads in a process and returns the thread ID with the earliest creation time.

This function works by calling PssCaptureSnapshot to capture a snapshot of all of the threads for the process handle that was passed in. After creating the snapshot, an iterator is created with PssWalkMarkerCreate; this iterator will be used in PssWalkSnapshot to traverse the list of threads. When traversing the thread list, the code uses CompareFileTime to compare the current thread’s creation time with the lowest known creation time. If the current thread has an earlier creation time, it becomes the candidate to be the main thread. This will continue until all threads have been traversed, at which point the value of mainThreadId will be the identifier of the main thread.

Installing the hook

Having obtained the main thread ID, the next step is to set and enable the appropriate debug registers to install the function hook. This is shown in the code below:

bool SetDebugBreakpoint(const HANDLE& mainThreadHandle,
    const void* const targetAddress) {

    CONTEXT mainThreadContext {
        .ContextFlags = CONTEXT_DEBUG_REGISTERS,

        // Set an address to break at on Dr0
        .Dr0 = reinterpret_cast<DWORD64>(targetAddress),
        
        // Set the debug control register to enable the breakpoint in Dr0
        .Dr7 = (1 << 0)
    };

    // Suspend the thread before setting its context
    SuspendThread(mainThreadHandle);

    // Set the main threads context
    auto result{ SetThreadContext(mainThreadHandle, &mainThreadContext) };
    if (result == 0) {
        PrintErrorAndExit("SetThreadContext");
    }

    // Resume the thread after setting its context
    ResumeThread(mainThreadHandle);

    return result != 0;
}

int main(int argc, char* argv[]) {

    std::async([]() {
        const auto mainThreadId{ GetMainThreadId(GetCurrentProcess()) };
        const auto mainThreadHandle{ OpenThread(
            THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME,
            false, mainThreadId) };

        if (mainThreadHandle == nullptr) {
            PrintErrorAndExit("OpenThread");
        }

        // Add a custom exception handler
        AddVectoredExceptionHandler(true, ExceptionHandler);

        if (!SetDebugBreakpoint(mainThreadHandle, DisplayMessageOnInterval)) {
            std::cerr << std::format("Failed to set hardware breakpoint on 0x{:X}",
                reinterpret_cast<DWORD_PTR>(DisplayMessageOnInterval))
                << std::endl;
        }

        CloseHandle(mainThreadHandle);

    }).wait();

    while (true) {
        DisplayMessageOnInterval("Hello World!");
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }

    return 0;
}

Setting a debug breakpoint on the DisplayMessageOnInterval function.

Starting at the main function, the first thing to notice is that installing the function hooks happens from a different thread. This is because a thread must be suspended to properly have its context changed, and the main thread cannot suspend and resume itself. After obtaining the main thread ID, a handle to the thread is opened with the THREAD_SET_CONTEXT and THREAD_SUSPEND_RESUME access rights. As their names imply, this grants the ability to set the threads context and suspend and resume the thread. Once the thread handle is opened, a custom exception handler is installed with the AddVectoredExceptionHandler call. This exception handler that will serve as the hook function and will be responsible for intercepting the call to the target function.

The SetDebugBreakpoint function performs the actual work of setting the hardware breakpoint. The function begins by initializing a custom CONTEXT structure. This structure sets the ContextFlags field to CONTEXT_DEBUG_REGISTERS, denoting that the debug registers will be modified. The Dr0 field is set to the target address to hook, and the breakpoint is enabled by setting the appropriate field in the Dr7 debug control register. Once this custom context is initialized, the main thread is suspended so that its context can be changed. After the main thread is suspended, the custom context is set with SetThreadContext, and the main thread is resumed.

Defining the exception handler

At this point the hardware breakpoint has been enabled and the custom exception handler will be called when the DisplayMessageOnInterval function is called. As in the previous examples, you can change the parameter being passed in to DisplayMessageOnInterval through the hook. The code for this is shown below:

LONG WINAPI ExceptionHandler(EXCEPTION_POINTERS* const exceptionInfo) {

    if (exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
        
        if (exceptionInfo->ContextRecord->Dr6 & 0x1) {
            static std::string replacementMessage{ "Hooked Hello World!" };

            // Write in replacement message
            auto* firstParameter{ reinterpret_cast<std::string*>(
                exceptionInfo->ContextRecord->Rcx) };
            *firstParameter = replacementMessage;

            // Set the resume flag before continuing execution
            exceptionInfo->ContextRecord->EFlags |= 0x10000;
        }

        return EXCEPTION_CONTINUE_EXECUTION;
    }

    return EXCEPTION_CONTINUE_SEARCH;
}

The custom exception handler that serves as the hook function entry point.

On Windows, when a hardware breakpoint is hit, the exception code will be EXCEPTION_SINGLE_STEP. The custom exception handler checks for this, then checks if the first bit is set in Dr6, which will be the case if the breakpoint in the Dr0 register has been hit. If this is so, then the DisplayMessageOnInterval function has been called. At this point, following the x64 calling convention, the first argument to the function will be in the RCX register. Knowing this, the address in RCX can be cast to a pointer-to-string, and the value can then be reassigned. Lastly, the resume flag is set in the EFlags field to indicate that execution can continue; failure to do this will result in an infinite loop.

Verifying the hook

After running this code, there should be a “Hooked Hello World” message displayed to the console, as was the case in the previous sections dealing with inline hooks. This demonstrates that function hooking can be done in a non-invasive way with hardware breakpoints; the assembly instructions of the DisplayMessageOnInterval function have not changed at all, but the function was successfully hooked.

Running the demo

The HardwareBreakpoint project provides the full implementation that was presented in this section. To see hardware breakpoints in action, set a breakpoint in Visual Studio on the first line that is executed after the Dr6 register is checked. This block will execute if the hardware breakpoint has been hit, which should happen almost immediately after launching the demo application.

The hardware breakpoint getting hit.

While inside this block, you can inspect the address stored in the Dr0 register to make sure that it matches the address of the DisplayMessageOnInterval function. Additionally, you can match up the set bits in the Dr6 register to validate that the hardware breakpoint was triggered with the correct flags. To get these values, hover over the ContextRecord field in the if statement. Visual Studio will display a context menu that shows the content of the CONTEXT structure at the time the breakpoint was hit.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

 

Powered by WordPress