天天看点

Extending windbg with Page Fault Breakpoints (windbg页面断点)

Extending windbg with Page Fault Breakpoints (windbg页面断点)

Introduction

Have you ever needed to set a breakpoint on an entire module or memory region?

This is particularly useful if you want break on code execution in a module without specifying any function names directly. Let's say that between point A and point B, some code calls into module C, and corrupts its data.

In Visual Studio, you can set data breakpoints, which will break the debugger when a value changes. Very useful indeed, but you cannot break on a read access.

In the powerful Windbg, we are equipped with ​​hardware breakpoints​​​ via the "ba" command (break on access), which works more or less the same as the page fault breakpoint I will explain. It has unfortunately a very strict limitation regarding the size. According to the ​​documentation​​​, the size can only be 1, 2 or 4 bytes, except when it concerns ​

​PAGE_EXECUTE​

​, in that case the maximum size is 1 byte. This makes it harder to break over a large memory area.

In its strict sense, we are not going to set a breakpoint, but what we do can be used as a breakpoint, since we are making the debugger break on certain conditions which we can control.

What we will do is to change the access flag of memory pages, from ​

​PAGE_EXECUTE_* ​

​into ​

​PAGE_NOACCESS ​

​or ​

​PAGE_READONLY​

​. When a function call is made into non-executable pages, it results in an access violation, which will break the debugger. At that point, you can inspect the callstack, variables, and memory regions. If you want to continue executing, you simply restore the access flag to its previous value, and tell the debugger to continue executing.The same principle can be applied for data. Normally, memory pages where data is stored are marked ​

​PAGE_READWRITE​

​. If you suspect memory corruption, you can simply mark the pages ​

​PAGE_READONLY​

​, so whenever someone tries to modify the data, you will get an access violation.

Background

There exists a debugger called ​​Ollydbg​​ which is an exceptional tool for doing reverse-engineering, where this functionality is already built-in.

Extending windbg with Page Fault Breakpoints (windbg页面断点)

Unfortunately, Ollydbg doesn't work well in Windows 7. So I switched to using Windbg, but I noticed that Windbg was missing this functionality. What could I do about it? Well, I had to write a Windbg extension implementing the same functionality.

How It Works

In order to implement ​​memory protection​​​ and ​​virtual memory​​​, modern operating systems are organised around memory pages. Protection is implemented by marking these pages with an access flag, e.g. ​

​PAGE_READONLY ​

​or ​

​PAGE_EXECUTE​

​. Whenever the protection is violated, the program will halt (access violation).

The page size is the same for all pages, and is typically of a size between 4-32 kb, a 1 MB program is fitted into 256 4KB pages.

A program consists of several segments or sections:

​.rdata ​

  • section, where typically constants are stored. 

​PAGE_READONLY​

​.data ​

  • section, where your variables are stored. 

​PAGE_READWRITE​

​.text ​

  • section, where the executable code is stored. 

​PAGE_EXECUTE​

​Normally, you cannot modify the ​

​.text​

​ section, execute code in the ​

​.data​

​ section or modify constants in the ​

​.rdata​

The functions we will use to change permissions are all part of the Windows API.

BOOL WINAPI VirtualProtect(
  __in   LPVOID lpAddress,
  __in   SIZE_T dwSize,
  __in   DWORD flNewProtect,
  __out  PDWORD lpflOldProtect
);

BOOL WINAPI VirtualProtectEx(
  __in   HANDLE hProcess,
  __in   LPVOID lpAddress,
  __in   SIZE_T dwSize,
  __in   DWORD flNewProtect,
  __out  PDWORD lpflOldProtect
);

SIZE_T WINAPI VirtualQuery(
  __in_opt  LPCVOID lpAddress,
  __out     PMEMORY_BASIC_INFORMATION lpBuffer,
  __in      SIZE_T dwLength
);

SIZE_T WINAPI VirtualQueryEx(
  __in      HANDLE hProcess,
  __in_opt  LPCVOID lpAddress,
  __out     PMEMORY_BASIC_INFORMATION lpBuffer,
  __in      SIZE_T dwLength
);

Protection flags:
  PAGE_EXECUTE = 0x10
  PAGE_EXECUTE_READ = 0x20
  PAGE_EXECUTE_READWRITE = 0x40
  PAGE_EXECUTE_WRITECOPY = 0x80
  PAGE_NOACCESS = 0x01
  PAGE_READONLY = 0x02
  PAGE_READWRITE = 0x04
  PAGE_WRITECOPY = 0x08      

Using the Code

Below is some pseudo code for changing the access flag from C/C++:

DWORD oldProtection;
DWORD newProtection = 1; // PAGE_NOACCESS
// The call changes all pages that are affected by the size argument.
// oldProtection only contains the protection mask of the first page.
// So be careful :)
VirtualProtect(address, size, newProtection, &oldProtection);

// To restore the protection:
VirtualProtect(address, size, oldProtection, &oldProtection);      

Since a Windbg extension is running inside the process of Windbg, and not in the process I would like to modify. I had to use ​

​VirtualProtectEx​

​, which takes as its first argument, a process handle. We obtain a process handle by calling ​​​OpenProcess​​​ on the ​

​pid ​

​(process id) of the debugging target.

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pId);
BOOL result = VirtualProtectEx
  (hProcess, address, size, newProtection, &oldProtection);
CloseHandle(hProcess);      

Windbg Extension

I have made a windbg extension that implements the functions ​

​Protect ​

​and ​

​MemInfo​

​.​

​Protect ​

  • address size protection

​MemInfo ​

  • address

Using the Extension

Start by copying the extension to the Windbg

On my machine, it is C:\Program Files (x86)\Debugging Tools for Windows (x86)\winext.

In the following scenario, we will break on code execution.

Step by step instructions:

  • Locate the 

​baseaddress ​

  • of the module by running the command "

​lm​

  • "
  • Use the obtained 

​baseaddress​

  • , and execute the command "

​!dh baseaddress​

  • "
  • Locate the section named .text in the output of the previous command
  • Find the virtual address
  • Find the virtual size

​!Protect baseaddress+virtualaddress virtualsize 1​

Step by Step Example in windbg

We need to load the extension.

0:000> .load debugext.dll      

Then we run the command "​

​lm​

​" to get the list of loaded modules.

0:000> lm
start    end        module name
00dd0000 00dd6000   PFDebug    (private pdb symbols)
6e460000 6e503000   MSVCR90    (deferred)             
6eb00000 6eb8e000   MSVCP90    (deferred)             
6eb90000 6eb96000   BuggyLib   (deferred)             
751a0000 751e6000   KERNELBASE   (deferred)             
76660000 76760000   kernel32   (deferred)             
77440000 775c0000   ntdll      (export symbols)      

We obtained the start and end address of the modules. We are interested in the start address of module ​

​BuggyLib​

​, which is 6eb90000.Let's find the relative address of the code section and its size. We do this by looking in the ​​PE header​​​. All executables and DLLs have a ​​PE header​​​. Basically, it tells Windows how to load the module into memory. Among other things, it contains the addresses and sizes of the ​

​.text​

​, ​

​.data​

​, ​

​.rdata ​

​sections.The ​

​!dh ​

​command displays the PE Header:

0:000> !dh 6eb90000
..... cut down for readability .....
SECTION HEADER #2
   .text name
    960 virtual size
   1000 virtual address
     A00 size of raw data
     400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read
....      

We obtained the virtual size 0x960, the virtual address 0x1000, and the protection is Execute Read, but let's double-check that by calling ​

​MemInfo​

​:

0:000> !MemInfo 6eb90000+1000
MEMORY_BASIC_INFORMATION
  BaseAddress = 0x6EB91000
  AllocationBase = 0x6EB90000
  AllocationProtect = 0x80
  RegionSize = 0x1000 (4096)
  State = 0x1000
  Protect = 0x20
  Type = 0x1000000      

Protect flag is 0x20 (​

​PAGE_EXECUTE_READ​

​), which seems correct.Let's change the code section to ​

​PAGE_NOACCESS​

​:

0:000> !Protect 6eb90000+1000 960 1
New protection (1)
Old protection (20)      

Let's continue execution until our breakpoint is hit.

0:000> g
(db4.ad0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0031fc68 ebx=00000000 ecx=6e4fb6f8 edx=00000000 esi=00000001 edi=00dd3384
eip=6eb91000 esp=0031fc40 ebp=0031fcb8 iopl=0         nv up ei ng nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010286
BuggyLib!SetLimits:
6eb91000 ??              ???      

Bam! We hit our breakpoint. Let us now restore the flags and do some single stepping:

0:000> !Protect 6eb90000+1000 960 20

New protection (20)
Old protection (1)
0:000> p
(db4.ad0): Access violation - code c0000005 (!!! second chance !!!)
eax=0031fc68 ebx=00000000 ecx=6e4fb6f8 edx=00000000 esi=00000001 edi=00dd3384
eip=6eb91000 esp=0031fc40 ebp=0031fcb8 iopl=0         nv up ei ng nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010286
BuggyLib!SetLimits:
6eb91000 33c0            xor     eax,eax
0:000> p
eax=00000000 ebx=00000000 ecx=6e4fb6f8 edx=00000000 esi=00000001 edi=00dd3384
eip=6eb91002 esp=0031fc40 ebp=0031fcb8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
BuggyLib!SetLimits+0x2:
6eb91002 66a35833b96e    mov     word ptr [BuggyLib!data+0x4 (6eb93358)],
ax ds:002b:6eb93358=0000      

When single stepping, we stepped into a second-chance exception. The first-chance exception stopped the debugger, the second-chance exception is a left over from the first exception. That is why we have to do "double" step to continue.

Points of Interest

There exist tools for hunting down memory corruption issues. They use memory access flags as a means to implement it. In order to detect a memory overwrite, they allocate a new page for every allocation, no matter how small it is, and gives you an address relative to the end of the memory page. The following page they mark as ​

​PAGE_NOACCESS​

​. So when a buffer overrun is made, the next byte is in a non accessible page, resulting in an access violation.

继续阅读