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
OSR is Hiring! Click here to find out more.