Utilizing hardware breakpoints to evade monitoring by Endpoint Detection and Response platforms and other monitoring/control systems is not a new idea. Many threat actors and researchers alike have discovered the ability to invoke breakpoints to inject commands to perform unexpected, unwanted, or malicious operations. Proof of Concept (PoC) techniques using Event Tracking for Windows (ETW) and the Windows Anti-Malware Scanning Interface (AMSI) have been available for some time. These include in-depth work such as that created by @rad9800 TamperingSyscalls and In-Process Patchless AMSI Bypass created by EthicalChaos – but in both cases the attack is performed by hooking a specific function in the current process memory to manipulate it for an unintended purpose.
The Cymulate Offensive Research Group, and one of its lead researchers, Ilan Kalendarov, were able to extend the methodology into a new technique named “Blindside” to allow for the method to work on a broader scale. Instead of hooking a specific function, the Blindside technique instead loads a non-monitored and unhooked DLL and leverages debug techniques that could allow for running arbitrary code. This permits more flexibility in what code can be executed outside the watchful eye of many commercial EDR and XDR platforms.
An Overview of Hardware Breakpoints and Debug Registers
As the technique in question is tightly tied to the use of hardware breakpoints, it would be useful to review this component of the OS/CPU and what it does.
Hardware breakpoints are available on both x86 and x64 processors. They contain eight debug registers, DR0 – DR7. These registers are 32- and 64- bits long on x86 and x64, respectively. The debug registers control and allows monitoring of the processor’s debugging operations.
Unlike the software breakpoints Windows programmers and administrators may be more aware of; hardware breakpoints can be used to set “memory breakpoints” or a breakpoint that is fired when any instruction attempts to read, write, or execute a specific memory address (depending on breakpoint configuration). Hardware breakpoints have some limitations, the main limit being restrictions on the number of hardware breakpoints you may have active at any given time.
The primary function of these debug registers is to set up and monitor up to 4 numbered 0 through 3. For each breakpoint, the following information can be specified:
- The linear address where the breakpoint is to occur.
- The length of the breakpoint location (1, 2, or 4 bytes).
- The operation will be performed at the address for a debug exception to be generated.
- Whether the breakpoint is enabled.
- Whether the breakpoint condition was present when the debug exception was generated.
DR0 through DR3 holds the linear address of a breakpoint. They are referenced as “Debug Address Registers”. A breakpoint will be triggered when an address inside one of these registers matches the instruction.
These debug registers are referred to as “Reserved Debug Registers.” These are not used in the technique.
The debug status register (DR6) reports debug conditions sampled when the last debug exception was generated. Updates to this register only occur when an exception is generated.
DR7 is referred to as the “Debug Control Register”. For the Blindside technique, DR7 is the most critical register as it controls each breakpoint and sets breakpoint conditions.
When it comes to exceptions in hardware breakpoints, there are two consequences here: a debug exception (#DB) and a breakpoint exception (#BP). For the purposes of the Blindside technique, the debug exception (#DB) is most important. When a breakpoint is triggered, the execution will be redirected to a handler – which is usually a debugger program or part of a more extensive software system. It is important to note that exceptions in the Blindside technique will only be triggered if they are a single-step exception.
Technique Setup Part 1: The Breakpoint Handler
Preparing to utilize the technique, the first requirement is that a Breakpoint Handler is established. Here is an example of a handler in C++:
The function first checks if the exception code in the ExceptionRecord member of the EXCEPTION_POINTERS structure is EXCEPTION_SINGLE_STEP, which indicates that a single-step exception has occurred. If this is the case, the function then checks if the instruction pointer (Rip) in the ContextRecord member of the structure is equal to the value of the first debug register (Dr0). If this is also true, the function prints some information about the exception, including the exception address, the values of certain registers, and the value of the stack pointer (Rsp).
Finally, the function sets the resume flag (RF), and returns EXCEPTION_CONTINUE_EXECUTION to indicate that the execution should continue. If the exception code is not EXCEPTION_SINGLE_STEP, the function returns EXCEPTION_CONTINUE_SEARCH to indicate that the search for a handler should continue.
Technique Setup Part 2: Setting the Breakpoint
With the handler configured to deal with the exception, the next step in preparation is to create the actual breakpoint.
Using C++ as before, here is an example of the breakpoint configuration:
This function takes two parameters. The first is the address where the system should breakpoint on, and the second is to enable the breakpoint or disable it. The technique then takes the current context of the specified thread being acted on and stores it inside of a context variable. If the setBP variable is true, the code sets DR0 to the address the attacker wishes to break on. Note that the technique could have also been used Dr1, Dr2, or Dr3 to store the address if necessary. Following this, the execution sets the first bit of Dr7 to 1 to enable the breakpoint, and clears bits 16 and 17 to break the execution.
Conversely; If the setBP variable is false, the code will clear Dr0 and do the same for the first bit of Dr7. Finally, the code sets the context of the thread for it to be updated.
While researching this topic, Cymulate reviewed the significant work on the general methodology that has been created by numerous research professionals. The Cymulate Offensive Research Group realized that a different technique to those already known could create a new process in debug mode, place a breakpoint on LdrLoadDll, and force only ntdll.dll to load. This creates a situation where the result is a clean version of ntdll, without hooks. An attacker could then copy the memory of the clean ntdll to an existing process and unhook all previously hooked syscalls.
When a process is first created, ntdll.dll will automatically be loaded, but additional dll’s will also come into play. By utilizing this technique, the breakpoint blocks the loading of the additional dll’s by hooking LdrLoadDLL and creates a process with only the ntdll in a stand-alone, unhooked state.
Walking Through the Blindside Technique
When looking at the entire process, application of the Blindside technique can allow for an unmonitored process to run within the context of the Windows session as follows:
Step 1: Create a new process in debug mode.
Step 2: Find the process address for LdrLoadDll
Because the process created is a targeted child process, it will have the same ntdll base address, and the same address for LdrLoadDll. This means that the address for LdrLoadDll must be identified.
Step 3: Set the breakpoint
After finding the LdrLoadDll address, the next function required is to put a breakpoint on the remote process.
The function takes two arguments: the address at which the breakpoint should be set and a handle to the thread on which the breakpoint should be set.
The function first initializes a CONTEXT structure and sets its ContextFlags member to the bitwise OR of CONTEXT_DEBUG_REGISTERS and CONTEXT_INTEGER, which specifies that the structure should be filled with the current debug registers and integer registers of the thread. It then sets the value of the first debug register (Dr0) to the specified address and sets the 0th bit of the Dr7 register to enable the breakpoint.
Step 4: Wait for the breakpoint to trigger
Next, the function calls the SetThreadContext() function to apply the updated context to the thread. It then enters an infinite loop that waits for debug events using the WaitForDebugEvent() function. When a debug event is received, the function checks if it is an exception debug event with an exception code of EXCEPTION_SINGLE_STEP. If this is the case, the function retrieves the current context of the thread using the GetThreadContext() function and checks if the exception address matches the specified address.
If the exception address matches the specified address, the function will reset the Dr0, Dr6 and Dr7 registers and will return nothing, this is done to block the LdrLoadDll from loading other DLLs. Otherwise, it resets the breakpoint and continues execution by calling the ContinueDebugEvent() function with the DBG_CONTINUE argument. This loop continues until WaitForDebugEvent() returns 0, indicating that no more debug events are available.
Step 5: Memory loading and unhooking
It is then necessary to copy the memory of ntdll into the target process and unhook any syscalls.
This function one parameter, a handle, to the debugged process created. The base address of the debugged process will be identical to the ntdll base address. After reading the memory of ntdll using NtReadVirtualMemory, freshNtdll (the allocated buffer will store that memory information. It is now safe to terminate the original process as there is no further need for it.
Step 6: Overwrite hooks
Next, it is necessary to iterate through all to find the virtual address of the .text section of ntdll, change the protection to PAGE_EXECUTE_READWRITE, and copy the .text section of the new mapped buffer (freshNtdll) to the original hooked version of ntdll, which will result in the hooks being overwritten.
Step 7: Clean up
The last step to conclude this technique is to restore the original protection.
The Blindside technique is not immune to mitigation. Though it can bypass monitoring by an EDR that relies on hooks to detect behaviors, there are methods that can reduce the technique’s effectiveness and alert when it is attempted.
The first method of mitigation is to monitor the use of the SetThreadContext function. This function is often used for malicious purposes, and inspecting the context will provide vital information, such as if a threat actor puts an address inside one of the Debug Address Registers (DR0-DR3). The unexpected writing of data into those registers is a strong indicator of compromise for detecting misuse of hardware breakpoints. When coupled with evidence of new dll instances and other indicators of compromise, this can trigger action on the part of anti-malware technologies.
Monitoring certain debug functions is also an indicator of malicious activity. While a debug function is used, an EDR can check the debug registers (Dr0-Dr3) for suspicious functions. If any are found, it is a strong indicator of malicious activity.
While the technique may bypass EDR platforms in their standard configuration, changing protocols/settings/profiles to have the EDR check the status of the DR0-DR3 registers would allow these tools to identify the technique and halt the attack. If one of the registers noted contains an address, this means some process or action is attempting to hook it. EDR technologies can correlate the attempt to create the unhooked dll along-side other activity to reach the conclusion that the behavior is malicious and terminate it; with the correct settings and configuration.
While researching the viability of this technique, the Cymulate Offensive Research Group verified the efficacy of the technique against multiple EDR and XDR platforms commercially available. The result of this experimentation is that many – but not all – EDR/XDR systems could be bypassed using Blindside. Cymulate will not be naming specific EDR/XDR tools that are vulnerable or not vulnerable due to confidentiality requirements, but vendors who were tested against were notified. Additionally, before the publication of the details of the technique, Cymulate filed a Microsoft Security Response Center report to allow Microsoft to be aware of the Blindside technique. As of this publication, Microsoft has declined to comment.
It is the hope of the Cymulate Offensive Research Group that the identification of the viability and efficacy of the Blindside technique against current versions of Windows (Desktop and Server) and multiple EDR/XDR products will result in a re-examination of hardware breakpoint handling in future.