Windows System Software -- Consulting, Training, Development -- Engineering Excellent, Every Time.

Turning a Breakpoint into a Busypoint

Turning a Breakpoint into a Busypoint

Last reviewed and updated: 10 August 2020

During dynamic analysis, I often want to prevent a code path from continuing to execute beyond a certain point. For example, maybe I suspect a race between the read and write paths in a driver. In this case, I may want to allow the write path to proceed up to a point before executing the read path.

This type of analysis often requires modifying the code to check for a special condition that causes the code to spin or wait until some other condition is met. However, with a little bit of understanding of x86/x64 assembly language and some WinDbg commands we can dynamically stop any code path dead in its tracks and then resume it when we see fit.

The basic idea is that we will set a breakpoint in the location that we are interested in. Once we hit that breakpoint, we overwrite the current instruction with an infinite loop. Any thread that hits this instruction will loop forever, of course chewing up CPU time but never actually doing anything. Other paths through our code are then free to execute as we see fit. When we are done, we can then go back and restore the original instruction and allow the threads to execute.

For our busy loop, we use the standard x86/x64 JMP instruction and specify the start of the JMP instruction as the target of the jump. In other words, we will jump to the jump instruction, thus creating our infinite loop. Due to the fact that we are jumping such a short distance, an 8-bit relative jump is sufficient for our needs. Referring to the Intel reference manual, we can see what the opcode for this instruction would be:

Opcode Mnemonic Description
EB cb JMP rel8 Jump short, RIP = RIP + 8-bit displacement sign extended to 64-bits

 

From this we know that we need a two byte instruction, where the first byte is 0xEB and the second byte is the sign extended displacement. For our instruction, we want the jump target to be the jump instruction, thus we want negative two (0xFE) as our displacement. This causes the processor to jump to the next instruction minus two, which is the start of our jump!

The first thing we want to do is choose the location of our busypoint. For this example, we have chosen the write processing routine of NTFS:

1: kd> bp ntfs!ntfscommonwrite
1: kd> g
Breakpoint 0 hit
Ntfs!NtfsCommonWrite:
fffff880`016c2c00 4c8bdc mov r11,rsp
0: kd> u @$ip
Ntfs!NtfsCommonWrite:
fffff880`016c2c00 4c8bdc mov r11,rsp
fffff880`016c2c03 49895b18 mov qword ptr [r11+18h],rbx
fffff880`016c2c07 49897320 mov qword ptr [r11+20h],rsi
fffff880`016c2c0b 57 push rdi
fffff880`016c2c0c 4154 push r12
fffff880`016c2c0e 4155 push r13
fffff880`016c2c10 4156 push r14
fffff880`016c2c12 4157 push r15

Note that in the output we use the $ip pseudo register. This is a WinDbg pseudo register that will always give the current instruction pointer regardless of target architecture. This alleviates us from having to use either @eip or @rip depending on the target machine, instead we can always simply refer to @$ip.

The next step is to save the two bytes that we are going to overwrite so that we can restore them later. A user defined pseudo register is a convenient place to save this data, so we will use $t0:

0: kd> r @$t0 = low(poi(@$ip))
0: kd> r @$t0
$t0=0000000000008b4c

To break the expression down:

  1. poi($ip) – Dereference the current instruction pointer, returning a pointer sized result
  2. low(value) – Return the low word of the supplied value

Therefore we are simply dereferencing the current instruction pointer and masking off everything but the low word of the result.

Given this, we can now overwrite our instruction pointer with our magic sequence of 0xFEEB (x86/x64 are little endian!):

0: kd> ew @$ip 0xFEEB
0: kd> u @$ip
Ntfs!NtfsCommonWrite:
fffff880`016c2c00 ebfe jmp Ntfs!NtfsCommonWrite

You can now be sure that no subsequent threads will execute beyond this point. They will, however, continue to chew up CPU time. This either will or will not be an issue in your analysis, depending on the priority of the threads involved. Of course, dynamically changing the priority of threads or putting threads to sleep are topics for another day 🙂

To release the threads from this state, we can simply use our stored value in the pseudo register to put the original bytes back:

0: kd> ew @$ip @$t0
0: kd> u @$ip
Ntfs!NtfsCommonWrite:
fffff880`016c2c00 4c8bdc mov r11,rsp
fffff880`016c2c03 49895b18 mov qword ptr [r11+18h],rbx
fffff880`016c2c07 49897320 mov qword ptr [r11+20h],rsi
fffff880`016c2c0b 57 push rdi
fffff880`016c2c0c 4154 push r12
fffff880`016c2c0e 4155 push r13
fffff880`016c2c10 4156 push r14
fffff880`016c2c12 4157 push r15