_KTRAP_FRAME

_KTRAP_FRAME.Rip

_KTRAP_FRAME.Rip

int 3

ret

0xC3

nt!KiBreakpointTrap

swapgs

_KTRAP_FRAME

_KTRAP_FRAME.Rip

nt!KiBreakpointTrap

nt!KiExceptionDispatch

EXCEPTION_BREAKPOINT

0x80000003

nt!KiExceptionDispatch

nt!KiExceptionDispatch

nt!KiBreakpointTrap

ecx

edx

r8

r9

r10

r11

rbp

_KTRAP_FRAME

+0x80

nt!KiExceptionDispatch

_KEXCEPTION_FRAME

_KTRAP_FRAME

_KEXCEPTION_FRAME

nt!KiExceptionDispatch

_EXCEPTION_RECORD.ExceptionAddress

_EXCEPTION_RECORD.ExceptionAddress

r8

nt!KiExceptionDispatch

nt!KiBreakpointTrap

_KTRAP_FRAME.Rip

_CONTEXT.Rip

nt!KiExceptionDispatch

nt!KiDispatchException

_EXCEPTION_RECORD

_KEXCEPTION_FRAME

_CONTEXT

_KTRAP_FRAME

_KEXCEPTION_FRAME

KeContextFromKFrame

_CONTEXT

_EXCEPTION_RECORD.ExceptionCode

nt!KiExceptionDispatch

STATUS_BREAKPOINT

0x80000003

_CONTEXT.Rip

_CONTEXT.Rip

The anti-debug trick

Knowing what we know about how Windows handles the different types of int 3s, is it possible to leverage this discrepancy in a useful way? The answer is yes.





Debuggers display the state of the program at the time of an exception. Since Windows will incorrectly assume that our int 3 exception was generated from the single-byte variant, it is possible to confuse the debugger into reading "extra" memory. We leverage this inconsistency to trip a "guard page" of sorts.









int 3 occurs, the _EXCEPTION_RECORD.ExceptionAddress and _CONTEXT.Rip values will lie in the middle of our multi-byte instruction instead of at the start. This means that the debugger will incorrectly determine that the instruction which threw the software breakpoint begins with the opcode 0x03 . Referring to the trusty Intel manual, we can see that this opcode represents a 2-byte add instruction:



From the Intel Instruction Set Reference (Volume 2, Chapter 3, Section 3.2).

What would happen if we positioned our multi-byte int 3 near the end of a page of memory?



When the operating system notifies our attached debugger of the breakpoint exception, the instruction pointer will point to memory that will be misinterpreted as the start of an add ( 0x03 ) instruction. This will cause the debugger to disassemble data on the adjacent page (since this instruction is 2 bytes long), and effectively read one byte past our "valid" memory range.



Our trick relies on the fact that Windows, as an optimization, will not commit virtual memory to physical RAM unless it absolutely needs it. That is to say that most memory, especially in usermode, is paged. When memory needs to be made available for use that is not currently in physical RAM, a page fault occurs. To learn more about memory management, check out the following articles on our site:



So, we can detect the memory read on this adjacent page by inspecting the corresponding PTE (Page Table Entry) using the _PSAPI_WORKING_SET_EX_BLOCK

PoC||GTFO As we saw in our first example (at the start of the article), when a multi-byteoccurs, theandvalues will lie in the middle of our multi-byte instruction instead of at the start. This means that the debugger will incorrectly determine that the instruction which threw the software breakpoint begins with the opcode. Referring to the trusty Intel manual, we can see that this opcode represents a 2-byteinstruction:What would happen if we positioned our multi-bytenear the end of a page of memory?When the operating system notifies our attached debugger of the breakpoint exception, the instruction pointer will point to memory that will be misinterpreted as the start of an) instruction. This will cause the debugger to disassemble data on the adjacent page (since this instruction is 2 bytes long), and effectively read one byte past our "valid" memory range.Our trick relies on the fact that Windows, as an optimization, will not commit virtual memory to physical RAM unless it absolutely needs it. That is to say that most memory, especially in usermode, is paged. When memory needs to be made available for use that is not currently in physical RAM, a page fault occurs. To learn more about memory management, check out the following articles on our site: Introduction to IA-32e hardware paging and Exploring Windows virtual memory management So, we can detect the memory read on this adjacent page by inspecting the corresponding PTE (Page Table Entry) using the QueryWorkingSetEx API. If the page is resident in our process' working set (e.g. mapped into our process by the debugger), the Valid bit in thewill be set.

A full example can be found below:



As always, if you have any questions or comments, please feel free to send us a message below. Happy hacking 😎.