Table of Contents:
- Inline Hooks (1/7)
- Trampolines & Detours (2/7)
- Hardware Breakpoints (3/7)
- Software Breakpoints (4/7)
- Virtual Table Hooks (5/7)
- Import Address Table Hooks (6/7)
- Export Address Table Hooks (7/7)
Software breakpoints are another technique to perform non-invasive function hooking. Unlike hardware breakpoints, there is no limit on the amount of software breakpoints that you can set. The downside, however, is that using software breakpoints can greatly reduce the performance of the application that is being monitored. As their name implies, these breakpoints are not set by any hardware registers; instead, they rely on changing the memory page permissions of executable code.
Installing the hook
To set a software breakpoint, the page that the target instruction is on must be a guard page, meaning that its page protection flags must have PAGE_GUARD set. Once an instruction in a guard page is executed, the program will raise a STATUS_GUARD_PAGE_VIOLATION exception. Doing this in code is rather straightforward since the Windows API provides convenient functions to retrieve and change page protections.
void SetMemoryBreakpoint(const void* const targetAddress) {
DWORD oldProtections{};
MEMORY_BASIC_INFORMATION memoryInfo{};
auto result{ VirtualQuery(targetAddress, &memoryInfo,
sizeof(MEMORY_BASIC_INFORMATION)) };
if (result == 0) {
PrintErrorAndExit("VirtualQuery");
}
result = VirtualProtect(memoryInfo.BaseAddress, memoryInfo.RegionSize,
memoryInfo.AllocationProtect | PAGE_GUARD, &oldProtections);
if (result == 0) {
PrintErrorAndExit("VirtualProtect");
}
}
The permissions of the page are retrieved and PAGE_GUARD is added to them.
Defining the exception handler
As with hardware breakpoints, a custom exception handler will need to be installed to catch this exception. Inside the exception handler, you will have access to the thread’s context and can perform the custom hooking logic. The software breakpoint exception handler has the same logical purpose as the hardware breakpoint version; however, the implementation details are a bit different. When a STATUS_GUARD_PAGE_VIOLATION is raised, the PAGE_GUARD protection on the entire page will be removed. This means that if you have a memory breakpoint at an address, the execution of any instruction on the same page as your target address will end up removing your breakpoint!
Overcoming the problem of having the PAGE_GUARD protection removed is a multi-step process. First, on a STATUS_GUARD_PAGE_VIOLATION exception, you can check to see whether the exception address matches the address of the function that you want to hook. If so, then carry out the hook logic as normal. However, before returning from the exception handler, you will need to set the trap flag in the EFlags register. This will put the CPU in single step mode for the next instruction execution. Now, when you return from the exception handler, you will immediately jump back into it with an EXCEPTION_SINGLE_STEP exception. Now you can set the page permissions to have PAGE_GUARD so that your memory breakpoint will continue to get hit in the future. While this may sound like a lot, the implementation is rather straightforward and is shown below:
LONG WINAPI ExceptionHandler(EXCEPTION_POINTERS* const exceptionInfo) {
if (exceptionInfo->ExceptionRecord->ExceptionCode ==
STATUS_GUARD_PAGE_VIOLATION) {
if (exceptionInfo->ExceptionRecord->ExceptionAddress ==
drawTextAddress) {
static std::string replacementMessage{ "Hooked Hello World!" };
// Set to replacement message address
exceptionInfo->ContextRecord->Rdx = reinterpret_cast<DWORD64>(
replacementMessage.c_str());
}
// Set single step flag so that memory breakpoints are re-enabled
// on the next instruction execution.
exceptionInfo->ContextRecord->EFlags |= 0x100;
return EXCEPTION_CONTINUE_EXECUTION;
}
if (exceptionInfo->ExceptionRecord->ExceptionCode ==
EXCEPTION_SINGLE_STEP) {
// Re-enable memory breakpoint since a different address might
// have caused the guard page violation.
SetMemoryBreakpoint(drawTextAddress);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
The implementation of an exception handler for memory breakpoints. This handler performs function hooking by overwriting a parameter that is being passed to the target function.
To make things a bit more exciting, this example will hook a Windows API, DrawTextA, instead of a custom function that was written. This is also done to prevent issues with the function to be hooked and the exception handler residing in the same memory page, as may be the case if you are hooking a function in your own code for some reason. The code for using DrawTextA is shown below:
int main(int argc, char* argv[]) {
// Add a custom exception handler
AddVectoredExceptionHandler(true, ExceptionHandler);
SetMemoryBreakpoint(drawTextAddress);
auto hDC{ GetDC(nullptr) };
const auto fontHandle{ GetStockObject(DEFAULT_GUI_FONT) };
LOGFONT logFont{};
GetObject(fontHandle, sizeof(LOGFONT), &logFont);
logFont.lfHeight = 200;
const auto newFontHandle{ CreateFontIndirect(&logFont) };
SelectObject(hDC, newFontHandle);
const std::string message{ "Hello World!" };
while (true) {
RECT rect{};
DrawTextA(hDC, message.c_str(), -1, &rect, DT_CALCRECT);
DrawTextA(hDC, message.c_str(), -1, &rect, DT_SINGLELINE | DT_NOCLIP);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return 0;
}
The main function of the example code.
Verifying the hook
Instead of outputting to a console, this program will draw the text “Hello World!” on the screen. However, when the memory breakpoint is set, the text will be replaced with “Hooked Hello World!” from the exception handler. This again demonstrates an additional technique to perform function hooking without any code modifications. With no limit set on the amount that you can set, memory breakpoints are the preferred way to hook functions of any application that may be doing integrity checks. As mentioned earlier though, the downside is performance. Since two exceptions (STATUS_GUARD_PAGE_VIOLATION and EXCEPTION_SINGLE_STEP) will be raised for every instruction execution on the memory page of the target address, the program may run noticeably slower.
Running the demo
The SoftwareBreakpoint project provides the full implementation that was presented in this section. As with hardware breakpoints, the best way to see software breakpoints in action is to set a breakpoint in the custom exception handler. Begin by placing two breakpoints: one on the if statement, and the other on the first statement inside of the if block.
After launching the application, look at the value of ExceptionAddress when the breakpoint gets hit. The first time that the breakpoint gets hit, ExceptionAddress should match drawTextAddress and the if block will be entered. Continue execution in the debugger and notice that the breakpoint on the if statement gets hit repeatedly, although ExceptionAddress will be different. This shows how the STATUS_GUARD_PAGE_VIOLATION exception gets raised for every instruction that is on the same memory page as drawTextAddress.