Take advantage of the memory page access flag, and set a new kind of breakpoint

Please Sign up or sign in to vote.

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.

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:

A .rdata section, where typically constants are stored. PAGE_READONLY

section, where typically constants are stored. A .data section, where your variables are stored. PAGE_READWRITE

section, where your variables are stored. A .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 section. The page access flag doesn't allow you. Any attempts will result in an access violation. But by adding and removing permissions on memory pages, we can create "breakpoints".

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 ; VirtualProtect(address, size, newProtection, &oldProtection); 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 - This function changes the protection flag of memory page(s).

address size protection - This function changes the protection flag of memory page(s). MemInfo address - This function displays information about a memory page, e.g. the current protection flag.

Using the Extension

Start by copying the extension to the Windbg extension folder.

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 "

of the module by running the command " " Use the obtained baseaddress , and execute the command " !dh baseaddress "

, and execute the command " " 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 would mark the page as non-accessible.

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.

History