PDA

View Full Version : The Zen of JIT Debugging


Kayaker
December 27th, 2005, 01:43
Hi All,

Chances are I might find a few more clues to this with some hands on tracing, but I throw this out as a general interest question.

A ring 3 debugger like Olly or MSDev can be registered as a JIT (Just in Time) debugger which will trigger on DebugBreak API calls (basically an embedded INT3). The default JIT debugger is registered under

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug]
"Debugger"="\"C:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe\" -p %ld -e %ld"

References to AeDebug leads to UnhandledExceptionFilter in kernel32.dll. The answer to my question may be buried in UnhandledExceptionFilter, or it might be lower, deeper and earlier in how the system handles DebugBreak. I figure there are others who have reversed UnhandledExceptionFilter in greater detail, for other reasons, and might have some clues.


The problem...
I've got a method of injecting code from ring0 at process or thread start, by intercepting a relevant syscall used during the act of creating an initial or secondary thread. The injected code, which executes immediately before thread start, is run from a mapped user mode MDL, generally in the 140000 memory region, returning to regular code say at 401000.

*In* this injected code I issue an option to send the DebugBreak API to the system debugger. No particular reason, I just wanted to do something funky to see what happened..
When issued from the context of the MDL code, running at 140000, DebugBreak wil NOT trigger the default JIT debugger. If DebugBreak is written into regular code it does, as it's supposed to.

If the process was *initially* loaded by a debugger, and not necessarily the system JIT debugger, the DebugBreak in the MDL code is recognized and is properly handled by the system (the attached debugger handles the INT3). (As does having Softice active with I3HERE ON).


What I'm trying to ascertain is *why* DebugBreak executed at 140000 in an MDL does not trigger a JIT debugger. The MDL *is* in the same context as the thread, but it's ignored. (I issue a MessageBox before DebugBreak is used, so the process now sits in an initial "GUI" thread, for whatever relevance there may be to win32k.sys/SSDT shadow table).

I'm wondering if it's as simple as an address check (>400000) in some naughty bit of kernel code, either the INT3 handler or UnhandledExceptionFilter, or it's some other restriction inherent in the system.


One thing I haven't tried, and might shed some light, is to try to execute DebugBreak from some memory injected in a more traditional way, such as CreateRemoteThread, where the memory is allocated by VirtualAllocEx, instead of an ntoskrnl handled MDL mapping.

In any case, a little holiday reversing mystery to ponder for those not overly inebriated...

Cheers,
Kayaker

disavowed
December 27th, 2005, 01:57
are the page protection flags the same between the 140000 memory region and the 401000 memory region? perhaps that has something to do with it? (just a guess... i have no real reason to believe this would cause the problem)

Kayaker
December 27th, 2005, 03:02
That certainly might be part of it. If I check the mappings with Softice QUERY (Display the virtual address space map of a process.)

Code:

:query <process>
Address Range Flags MMCI PTE Name
...
00140000-00140000 84080000 00000000 00000000
00150000-0024F000 84000005 Heap #01
00250000-0025F000 04000000 81617448 E1FA25E0 Heap #02
...
00400000-00404000 07100003 8156EDE8 E1CBDDA0 process.exe


Two things that don't exist for the 140000 range (which QUERY doesn't even recognize as a 1000 byte page) are:

MMCI Pointer to the memory management structure.
PTE Structure that contains the ProtoPTEs for the address range.


The page table information shows a slight difference in Attributes between 140000 and 401000 as well

Code:

age 140000
Linear Physical Attributes
00140000 016D0000 P RW U A D

age 401000
Linear Physical Attributes
00401000 0FAAA000 P RW U A

(before execution code is paged out as expected)
age 401000
Linear Physical Attributes
00401000 00000000 NP

P Present.
D Dirty.
S Supervisor.
RW Read/Write.
A Accessed.
U User.
R Read Only.
NP Not Present.


The lack of listed MMCI and "PTE Structure" might be part of it, however that figures into the equation. Mind you there *is* a valid PTE for 140000 because the code is certainly running and there's a Physical mapping. Besides whatever the Flags specify, the other significant difference is that there's no Name listed for the memory range, either as a named memory-mapped file or executable module.

There's also the point that the page is marked as "dirty", probably because I write to it after mapping, though I don't know what effect that would have either to this specific issue.

user
January 11th, 2006, 21:47
Quote:
[Originally Posted by Kayaker]What I'm trying to ascertain is *why* DebugBreak executed at 140000 in an MDL does not trigger a JIT debugger. The MDL *is* in the same context as the thread, but it's ignored. (I issue a MessageBox before DebugBreak is used, so the process now sits in an initial "GUI" thread, for whatever relevance there may be to win32k.sys/SSDT shadow table).
let me guess, your MDL doesn't show up in QueryVirtualMemory either?

Kayaker
January 13th, 2006, 01:29
Thanks for the reply

I had done some initial tracing of the Int3 handler KiTrap03, comparing the path of DebugBreak when called from regular user code, and from an MDL mapped region. Somewhere deep in KiDispatchException the code diverges. In the first case it triggers the JIT debugger, in the second it returns STATUS_INVALID_DISPOSITION.

I will have to carefully examine the path
KiUserCallbackDispatcher -> RtlDispatchException -> RtlpExecuteHandlerForException
to see if I can figure out exactly where and why the code differs.


The suggestion was good and may provide a method of attack. I hadn't thought of that when disavowed mentioned comparing page protections. I can try changing the protection flags on the MDL mapping and see if there is any difference. The results from executing ZwQueryVirtualMemory:

Code:

Normal user code:

MEMORY_BASIC_INFORMATION
BaseAddress: 401000
AllocationBase: 400000
AllocationProtect: 80 // PAGE_EXECUTE_WRITECOPY
RegionSize: 1000
State: 1000 // MEM_COMMIT
Protect: 20 // PAGE_EXECUTE_READ
Type: 1000000 // MEM_IMAGE


MDL mapping:

MEMORY_BASIC_INFORMATION
BaseAddress: 140000
AllocationBase: 140000
AllocationProtect: 4 // PAGE_READWRITE
RegionSize: 1000
State: 1000 // MEM_COMMIT
Protect: 4 // PAGE_READWRITE
Type: 20000 // MEM_PRIVATE


The results kind of speak for themselves, though I don't know specifically what might be causing the "problem".


I figured I had a few choices for getting this MEMORY_BASIC_INFORMATION info.
Either call VirtualQueryEx from my MDL (it being mapped/injected into the target process),
or call the underlying Ntdll ZwQueryVirtualMemory syscall from user mode somewhere,
or execute NtQueryVirtualMemory strictly from my driver in kernel mode, where I already
had full access to the targets ClientId and memory address values I wanted to check.

I went with the 3rd choice and for interest here is code I used.

Code:

/***********************************************************

QueryMemoryBasicInformation

Execute ZwQueryVirtualMemory to retrieve information about
virtual memory in the user mode address range using the
MemoryBasicInformation class. This is equivalent to
VirtualQuery/VirtualQueryEx.

This syscall *requires* that the buffer it dumps the
information to (in this case a MEMORY_BASIC_INFORMATION
structure) resides in user mode, not in kernel mode.
(_MmUserProbeAddress <= 0x7FFF0000)

So in order to "fool" the syscall we can map a copy
of the output buffer (MEMORY_BASIC_INFORMATION structure)
to user address space by using an MDL.

This same strategy can be used with other syscalls which
check that the calling function is from user mode and is
providing an I/O buffer based in a user address context.
For example, ZwGetContextThread / ZwSetContextThread.

// Kayaker

************************************************************/

NTSTATUS QueryMemoryBasicInformation(PVOID BaseAddress,
CLIENT_ID TargetClientId)
{


NTSTATUS ntstatus = STATUS_UNSUCCESSFUL;
HANDLE ProcessHandle = 0;
OBJECT_ATTRIBUTES ObjectAttributes;

PMDL pMdl = NULL;
MEMORY_BASIC_INFORMATION mbi;
PMEMORY_BASIC_INFORMATION pMapped_mbi;


//////////////////////////////////////////////////


__try {


// Prepare OBJECT_ATTRIBUTES structure

InitializeObjectAttributes(
&ObjectAttributes, // OUT POBJECT_ATTRIBUTES
NULL, // PUNICODE_STRING ObjectName
OBJ_KERNEL_HANDLE || \
OBJ_CASE_INSENSITIVE, // ULONG Attributes
NULL, // HANDLE RootDirectory
NULL // PSECURITY_DESCRIPTOR SecurityDescriptor
);


// Get a process handle

ZwOpenProcess(&ProcessHandle, PROCESS_QUERY_INFORMATION,
&ObjectAttributes, &TargetClientId);



/*****************************************************/

/* We must provide a user mode MEMORY_BASIC_INFORMATION structure for
ZwQueryVirtualMemory to operate, else it will raise an exception
*/

RtlZeroMemory(&mbi, sizeof MEMORY_BASIC_INFORMATION);



// Allocate an MDL large enough to hold our temporary
// MEMORY_BASIC_INFORMATION structure

pMdl = IoAllocateMdl(
&mbi, // PVOID VirtualAddress
sizeof MEMORY_BASIC_INFORMATION, // ULONG Length
FALSE, // BOOLEAN SecondaryBuffer
FALSE, // BOOLEAN ChargeQuota
NULL // IN OUT PIRP Irp OPTIONAL
);

if (!pMdl)
return ntstatus;


// Lock the physical pages mapped by the virtual address range into memory

MmProbeAndLockPages (
pMdl, // IN OUT PMDL MemoryDescriptorList
KernelMode, // KPROCESSOR_MODE AccessMode
IoWriteAccess // LOCK_OPERATION Operation
);

// Map the pages into the process

pMapped_mbi = (PMEMORY_BASIC_INFORMATION) MmMapLockedPagesSpecifyCache(
pMdl, // PMDL MemoryDescriptorList
UserMode, // KPROCESSOR_MODE AccessMode
MmCached, // MEMORY_CACHING_TYPE CacheType
NULL, // PVOID BaseAddress
FALSE, // ULONG BugCheckOnFailure
NormalPagePriority // MM_PAGE_PRIORITY Priority
);


/*****************************************************/

// Query the memory with MemoryInformationClass and our
// MDL mapped MEMORY_BASIC_INFORMATION buffer.

// Not exported by Win2K ntoskrnl, obtain address by
// parsing export directory of Ntdll.

pZwQueryVirtualMemory(
ProcessHandle, // IN HANDLE ProcessHandle
BaseAddress, // IN PVOID BaseAddress
MemoryBasicInformation, // IN MEMORY_INFORMATION_CLASS MemoryInformationClass
pMapped_mbi, // OUT PVOID MemoryInformation
sizeof MEMORY_BASIC_INFORMATION, // IN ULONG MemoryInformationLength
NULL // OUT PULONG ReturnLength OPTIONAL
);


//////////////////////////////////////////////////

// Output info

DbgPrint ("\nMEMORY_BASIC_INFORMATION\n";

DbgPrint (" BaseAddress: %x\n AllocationBase: %x\n AllocationProtect: %x\n \
RegionSize: %x\n State: %x\n Protect: %x\n Type: %x\n",
pMapped_mbi->BaseAddress,
pMapped_mbi->AllocationBase,
pMapped_mbi->AllocationProtect,
pMapped_mbi->RegionSize,
pMapped_mbi->State,
pMapped_mbi->Protect,
pMapped_mbi->Type
);



// CLEANUP:

// Unmap the pages and free the MDL

MmUnmapLockedPages(pMapped_mbi, pMdl);
MmUnlockPages(pMdl);
IoFreeMdl(pMdl);


// Close the process handle

/*
In Win2K, ZwClose returns error 0xC0000235:
"NtClose was called on a handle that was protected
from close via NtSetInformationObject"
STATUS_HANDLE_NOT_CLOSABLE equ 0C0000235h

In XP it crashes with INVALID_KERNEL_HANDLE

A similar problem occurs with ZwOpenThread as with
ZwOpenProcess. The problem with ZwClose returning
INVALID_KERNEL_HANDLE was mentioned in a newsgroup,
with no valid solution.
*/

// ZwClose(ProcessHandle); // not called

//=============================================================


} __except (EXCEPTION_EXECUTE_HANDLER) {

DbgPrint ("\n ERROR ExceptionCode: %x\n", GetExceptionCode() );
return ntstatus;

} // end __try{


return STATUS_SUCCESS;

}

/***********************************************************/

Code:

///////////////////////////////////////////////////////

/*
Header file for
NTSTATUS QueryMemoryBasicInformation(
PVOID BaseAddress,
CLIENT_ID TargetClientId);
*/


#define PROCESS_QUERY_INFORMATION (0x0400)

EXTERN_C
NTKERNELAPI
NTSTATUS
ZwOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);


typedef enum _MEMORY_INFORMATION_CLASS {
MemoryBasicInformation,
MemoryWorkingSetList,
MemorySectionName,
MemoryBasicVlmInformation
} MEMORY_INFORMATION_CLASS;


typedef struct _MEMORY_BASIC_INFORMATION { // Information Class 0
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
ULONG RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;


typedef NTSTATUS (*ZWQUERYVIRTUALMEMORY)(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN MEMORY_INFORMATION_CLASS MemoryInformationClass,
OUT PVOID MemoryInformation,
IN ULONG MemoryInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
extern ZWQUERYVIRTUALMEMORY pZwQueryVirtualMemory;

///////////////////////////////////////////////////////


Any further ideas/insights are welcomed..

Regards,
Kayaker

Kayaker
January 17th, 2006, 03:27
Well, you can't fool the system all the time

I decided to try to change the page protection on the injected MDL mapping to see if it would behave differently. The attempt failed, but it was an interesting enough exercise that I'll outline it for fun.

Calling ZwProtectVirtualMemory, using code similar to above where the buffer variables the syscall uses are mapped to a temporary usermode MDL mapping,

Code:

pZwProtectVirtualMemory(
ProcessHandle, // IN HANDLE ProcessHandle
&pMdl_Data->BaseAddress, // IN OUT PVOID *BaseAddress
&pMdl_Data->ProtectSize, // IN OUT PULONG ProtectSize
PAGE_EXECUTE_READ, // IN ULONG NewProtect
&pMdl_Data->OldProtect // OUT PULONG OldProtect
);


Here are a few pieces of the underlying call

ntoskrnl!_MiProtectVirtualMemory

...
:804A3994 MOV ECX,[EBP+14] // New protection
:804A3997 CALL @MiMakeProtectionMask
// for PAGE_EXECUTE_READ (0x20), returns mask of 0x3

...
:804A39F4 MOV EAX,FS:[00000124] // KTHREAD
:804A3A10 MOV EAX,[EAX+44] // KAPC_STATE.Process -> PTR EPROCESS
:804A3A13 PUSH DWORD PTR [EAX+00000194] // EPROCESS.VadRoot
:804A3A19 PUSH DWORD PTR [EBP-0098]
:804A3A1F PUSH ECX // Base address of memory region
:804A3A20 CALL _MiCheckForConflictingNode // "walks" the VAD table
:804A3A25 MOV EDI,EAX // struct vad {
:804A3A27 TEST EDI,EDI
:804A3A29 JZ STATUS_CONFLICTING_ADDRESSES

// STATUS_CONFLICTING_ADDRESSESES equ 0C0000018h
// The specified address range conflicts with the address space.


// If successful, returns VAD for memory region
// (Virtual Address Descriptor Table)

typedef struct vad {
void *StartingAddress;
void *EndingAddress;
struct vad *ParentLink;
struct vad *LeftLink;
struct vad *RightLink;
DWORD Flags;
}VAD, *PVAD;

...
:804A3A4C MOV EAX,[EDI+14] // VAD.Flags
:804A3A4F MOV EDX,EAX // i.e. 0x84080000
:804A3A51 MOV ECX,00200000 // check UserPhysicalPages flag
:804A3A56 AND EDX,ECX
:804A3A58 CMP EDX,ECX
:804A3A5A JZ STATUS_CONFLICTING_ADDRESSES
:804A3A60 MOV EDX,EAX
:804A3A62 MOV ECX,00080000 // check PhysicalMapping
:804A3A67 AND EDX,ECX
:804A3A69 CMP EDX,ECX
:804A3A6B JZ STATUS_CONFLICTING_ADDRESSES
:804A3A71 MOV ECX,00400000 // check NoChange
:804A3A76 AND EAX,ECX
:804A3A78 CMP EAX,ECX
:804A3A7A JZ @ _MiCheckSecuredVad
:804A3A80 MOV ECX,[EDI+14]
:804A3A83 TEST ECX,80000000 // check PrivateMemory
:804A3A89 JNZ @ _MiIsEntireRangeCommitted
...


For an MDL mapping, the above procedure fails on
:804A3A62 MOV ECX,00080000 // check PhysicalMapping



To go a little deeper into the VAD Flags... In a previous post bilbo had nicely found the MMVAD_FLAGS structure for us, taken from Windows XP ntoskrnl.pdb:

http://woodmann.net/forum/showthread.php?t=6458&

Code:

The bits are defined, from the least significant, as
(position:number_of_bits):

struct _MMVAD_FLAGS {
unsigned long CommitCharge:0:13;
unsigned long PhysicalMapping:13:1;
unsigned long ImageMap:14:1;
unsigned long UserPhysicalPages:15:1;
unsigned long NoChange:16:1;
unsigned long WriteWatch:17:1;
unsigned long Protection:18:5;
unsigned long LargePages:1d:1;
unsigned long MemCommit:1e:1;
unsigned long PrivateMemory:1f:1;
};



Similar to the Softice QUERY command, the LiveKD(/WinDbg) !VAD command also gives a useful memory map:

Code:

// list all processes:

kd> !process 0 0

PROCESS ff6f1480 SessionId: 0 Cid: 02f0 Peb: 7ffdf000 ParentCid: 02fc
DirBase: 0b68a000 ObjectTable: 80945208 TableSize: 16.
Image: target.exe

// get VadRoot from the EPROCESS structure:

kd> !process 0xff6f1480
PROCESS ff6f1480 SessionId: 0 Cid: 012c Peb: 7ffdf000 ParentCid: 02fc
DirBase: 0087d000 ObjectTable: 81602408 TableSize: 16.
Image: target.exe
VadRoot 816315e8 Clone 0 Private 37. Modified 102. Locked 0.


// walk VAD (see 'vad' export of kdexts.dll)

kd> !vad 0x816315e8
VAD level start end commit
815c92e8 ( 1) 10 10 1 Private READWRITE
ff9d97c8 ( 2) 20 20 1 Private READWRITE
...
ff88e6c8 ( 5) 140 141 0 Private Phys READWRITE // MDL mapping
...
816315e8 ( 0) 400 404 3 Mapped Exe EXECUTE_WRITECOPY
...


The same output from Softice QUERY:

Code:

:query target
Address Range Flags MMCI PTE Name
00010000-00010000 C4000001
00020000-00020000 C4000001
...
00140000-00141000 84080000 00000000 00000000 // MDL mapping
...
00400000-00404000 07100003 81680228 E1DF1CE0 target.exe


******************************************************************************************

The raw VAD Flags listed by Softice can be understood by examining the _MMVAD_FLAGS structure.
For example, for the MDL mapped region:

Code:

MDL Mapping:

Address Range Flags
00140000-00141000 84080000

0x84080000 = 1000,0100,0000,1000,0000,0000,0000,0000 b

1 PrivateMemory
0 MemCommit
0 LargePages
00100 Protection: 0x4
0 WriteWatch
0 NoChange
0 UserPhysicalPages
0 ImageMap
1 PhysicalMapping
0000000000000000000 CommitCharge: 0


Kayaker