Data Breakpoint Oddities

 

 

NOTE: This is going to be a very long, verbose, bizarre and wierd post. So try not to fall asleep. Before I start, a big thanks must go to my colleague Seva Titov for pointing this issue out to me.

 

Ok…………Here we go…………….

 

Consider this sorry piece of code that I cooked up -

#include <windows.h>

#include <iostream>

 

#define BUF_SIZE 256

 

void wmain ( int argc, wchar_t * argv[] )

{

    WCHAR szBuf[BUF_SIZE] ;

    HANDLE hFile = NULL ;

    DWORD dwBytesRead = 0 ;

 

    // zero out the buffer

    memset( (void*) szBuf, 0x30, BUF_SIZE ) ;

 

   // open existing file....

    hFile = CreateFile( TEXT("dummy.txt"), GENERIC_READ, FILE_SHARE_READ,

                        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ) ;

    if ( INVALID_HANDLE_VALUE == hFile )

    {

        wprintf( L"CreateFileW() failed with %d\n",

                 GetLastError() ) ;

        return ;

    }

   

    // read the first 100 bytes from the file ......

    if ( 0 == ReadFile( hFile, (LPVOID) szBuf, 256, & dwBytesRead, NULL ) )

    {

        wprintf( L"ReadFile() failed with %d\n",

                 GetLastError() ) ;

    }

   

    CloseHandle( hFile ) ;

}

Yes Yes. The code is overly simplistic, buggy and devoid of error-checking etc etc. But it will easily bring my point across………Read On………………

 

Let us see what is this code is doing. A buffer (“szBuf”) is being filled up with the text from the file “dummy.txt”. Let us try and use data breakpoints to break-in as soon as this buffer is written to. Assume that we have already put the executable under the debugger, fixed symbols, broken-in on the frame for Wmain() and already found the address of the buffer “szBuf”.

 

0:000> dv /v

0012fee4 argc = 1

0012fee8 argv = 0x00321660

0012fcc0 dwBytesRead = 0x2100210

0012fccc hFile = 0x02100210

0012fcd8           szBuf = unsigned short [256]

 

 

Now we will set the data breakpoint to trigger on any write-access to 0x0012fcd8 (i.e. the buffer). Theoretically the data-breakpoint should be triggered twice – Once by memset() and then by Kernel32!ReadFile() [This however will not be the case as we shall observe].

 

0:000> ba w4 0012fcd8

 

Now let us also set a breakpoint on Kernel32!CloseHandle() which is called from the last line of code before we exit Wmain(). This will give us a chance to break-in and examine the buffer before exiting Wmain(), irrespective of whether or not the data-breakpoint triggers.

0:000> bm kernel32!closehandle

  2: 7c809b77 @!"kernel32!CloseHandle"

 

At this point we have armed all the necessary breakpoints and now we will let the program run

 

0:000> g

 

Now the buffer will be zeroed out by the memset() function. This constitutes a write-access and hence our data-breakpoint will immediately trigger. Sure enough…….

 

 

Breakpoint 1 hit

eax=30303030 ebx=7ffdb000 ecx=0000003f edx=00000000 esi=0006fa08 edi=0012fcdc

eip=00411d55 esp=0012fbdc ebp=0012fedc iopl=0 nv up ei pl nz na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202

DataBreakpointOddity!memset+0x45:

00411d55 f3ab rep stosd es:0012fcdc=cccccccc

 

 

At this point we will note that memset() writes to the buffer in user mode. No kernel mode hanky-panky involved here. Let us dump out the buffer at this point and ensure that it has been indeed zeroed out –

 

 

0:000> db 0012fcd8

0012fcd8 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fce8 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fcf8 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fd08 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fd18 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fd28 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fd38 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

0012fd48 30 30 30 30 30 30 30 30-30 30 30 30 30 30 30 30 0000000000000000

 

 

Yup. Now we will jump out of memset() and back into Wmain().

 

0:000> gu

 

Again we will let the program continue its execution. We will be expecting it to break-in inside kernel32!ReadFile() when the buffer is written to.

           

            0:000> g

 

But you will observe that program execution continues straight onto kernel32!CloseHandle() where we had set a breakpoint earlier. It did not break-in at kernel32!ReadFile().

 

 

Breakpoint 2 hit

eax=000007e8 ebx=7ffdb000 ecx=7c801898 edx=7c90eb94 esi=0012fbf0 edi=0012fedc

eip=7c809b77 esp=0012fbe8 ebp=0012fedc iopl=0 nv up ei pl nz na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202

kernel32!CloseHandle:

7c809b77 8bff mov edi,edi

 

 

Hmmm, maybe the buffer has not been written into. Let us examine this possibility too. So we will move back to the wmain() frame and dump out the buffer -

 

0:000> .frame 1

01 0012fedc 0041378f DataBreakpointOddity!wmain+0x146

 

0:000> db 0012fcd8

0012fcd8 57 68 61 74 2e 74 69 6d-65 2e 69 73 2e 69 74 3f What.time.is.it?

0012fce8 2e 54 69 6d 65 2e 74 6f-2e 64 75 6d 70 2e 79 6f .Time.to.dump.yo

0012fcf8 75 72 2e 67 6f 6f 67 6c-65 2e 73 68 61 72 65 73 ur.google.shares

0012fd08 2e 57 68 61 74 2e 74 69-6d 65 2e 69 73 2e 69 74 .What.time.is.it

0012fd18 3f 2e 54 69 6d 65 2e 74-6f 2e 64 75 6d 70 2e 79 ?.Time.to.dump.y

0012fd28 6f 75 72 2e 67 6f 6f 67-6c 65 2e 73 68 61 72 65 our.google.share

0012fd38 73 2e 57 68 61 74 2e 74-69 6d 65 2e 69 73 2e 69 s.What.time.is.i

0012fd48 74 3f 2e 54 69 6d 65 2e-74 6f 2e 64 75 6d 70 2e t?.Time.to.dump.

 

 

Now clearly this buffer (which had been zeroed earlier) has been filled up. The data breakpoint was set to trigger on a write-access to the location 0x0012fcd8. However no breakpoint exception was generated even though this location was written to. Why did this happen? Is this a bug in the debugger?

 

 

Well….actually No. To understand why this happens, you’ll have to take a small trip to microprocessor land. For the sake of simplicity, let us just limit ourselves to 32 bit Intel Processors in this discussion.

 

Data breakpoints are implemented with the hardware support provided by your microprocessor. 32 bit Intel processors provide eight registers for exactly this purpose – Debug Registers DR0 – DR7. To give you a very brief overview, here is how it all works -

 

  • Each of the four registers DR0-DR3, hold 32 bit linear breakpoint addresses. This is the reason why you can only set a maximum of four data breakpoints on x86 machines (I do not know if this holds true for X64 and IA64 machines as well).
  • Registers DR4, DR5 are reserved registers.
  • The DR6 register is known as the Debug Status Register. It reports the conditions under which the breakpoint was triggered - Was the associated breakpoint condition (read/write/execute) met? Was the Debug Exception generated by Single-Stepping execution mode? Etc etc.
  • The DR7 is the big daddy who pulls all the strings and it is called the Debug Control register. It allows you to enable/disable any of the four breakpoints and to set the conditions under which the breakpoints trigger (read/write/execute).

 

As soon as you set the data-breakpoint, if you check your set of debug registers.

 

0:000> r dr0

dr0=0012fcd8

 

0:000> r dr7

dr7=000d0501

 

The DR0 register contains the 32 bit linear address of the buffer – 0x0012fcd8. Bit 0 of DR7 tells us that the breakpoint has been armed. Bit 16 of DR7 has been set, telling us that the data-breakpoint will only be triggered on a write access. Bits 18,19 indicate the size of the location to be monitored for access – in this case it is 4 bytes.

 

AS soon as any instruction makes a write-access to the 32 bit address being monitored (0x0012fcd8), the processor generates a debug exception and transfers control to the debugger’s debug loop. The debugger then checks the DR0-DR3, DR6 and DR7 registers to evaluate whether the breakpoint condition is met. If yes, then it breaks-in into the process and transfers control to the user; else it resumes execution.

 

 

However there is a small caveat- The processor cannot detect any accesses to a location if the physical address is used instead of the corresponding 32 bit virtual address. This means that if the linear address (0x0012fcd8) is translated to a physical address and accessed from kernel mode, then it will go undetected by the processor and the data-breakpoint will not trigger. Which also means the data-breakpoints will not trigger if there is DMA access. Coming to the point, APIs like ReadFile() and ReadProcessMemory() directly write to the buffer in kernel mode. Which is the reason why we our data-breakpoint did not trigger.

 

Errr……So what was the point of this post………………..?

 

“A user-mode data breakpoint will not be triggered if the location being watched is accessed from the kernel-mode”.