windbgida

How to do hybrid user-mode/kernel-mode debugging?


Basically, I have a user mode program that calls kernel32.CreateProcessA() which internally calls kernel32.CreateProcessInternalW(). Within this function, I'm interested in what is happening inside ntdll.NtCreateSection() which attempts to map the executable in virtual memory. Once in this function, the program quickly sets up the kernel call as EAX=0x32 and executes the SYSENTER instruction.

Obviously I can't see beyond the call gate in a user mode debugger. I have a little experience debugging kernel-mode drivers, so I loaded a copy of XP SP3 in a VMWare window and used VirtualKD to conect the pipe to the WinDbg (which I happen to be running inside IDA). After connecting the kernel debugger, I copied my user-mode EXE program and PDB onto the virtual machine, but I'm kind of at a loss on how to set the initial breakpoint in my user-mode program properly. I don't want to intercept all calls to the equivalent ntdll.ZwCreateSection() which I believe to be on the other side of the call gate. Ideally, I'd like to break into the user-mode code and step through that call gate now that I'm using a Kernel debugger, but I don't know what the first steps are.

I've done some googling and I've come close by setting a "ntsd -d" value in

HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\myprocess.exe

This causes a break in the kernel debugger when I start my process, but I can't seem to set any breakpoints following the .breakin command I need to issue to IDA to get to the WinDbg prompt. I've been following this guide where I locate my process with !process then switch to the context, and reload the symbols but I'm having problems setting the breakpoint in my process or advancing past the initial breakpoint set by "ntsd -d". After getting the message that the breakpoint could not be resolved and a deferred breakpoint is added, I cannot seem to advance "into" to the process without clearing the breakpoints if that makes any sense. Here's the stack of where I seem to be at when I hit that initial break:

ChildEBP RetAddr  
b2b55ccc 8060e302 nt!RtlpBreakWithStatusInstruction
b2b55d44 8053d638 nt!NtSystemDebugControl+0x128
b2b55d44 7c90e4f4 nt!KiFastCallEntry+0xf8
0007b270 7c90de3c ntdll!KiFastSystemCallRet
0007b274 6d5f5ca6 ntdll!ZwSystemDebugControl+0xc
0007bd48 6d5f6102 dbgeng!DotCommand+0xd0d
0007de8c 6d5f7077 dbgeng!ProcessCommands+0x318
0007dec4 6d5bec6c dbgeng!ProcessCommandsAndCatch+0x1a
0007eedc 6d5bed4d dbgeng!Execute+0x113
0007ef0c 010052ce dbgeng!DebugClient::Execute+0x63
0007ff3c 010069fb ntsd!MainLoop+0x1ec
0007ff44 01006b31 ntsd!main+0x10e
0007ffc0 7c817067 ntsd!mainCRTStartup+0x125
0007fff0 00000000 kernel32!BaseProcessStart+0x23

To be honest, I'm not sure my PDB is being loaded but I suspect its probably not my immediate problem; my modules pane is only showing kernel driver modules, not user mode modules. When I had been doing driver debugging in the past, I could see my driver image in this pane and whether or not the symbols had loaded, so I'm not sure what to expect for a user-mode image. Without the image, I can't really expect the debugger to resolve any breakpoints.

I realize I may be going about this completely wrong but I'm not having any luck searching for how to do user-mode/kernel-mode hybrid debugging. Is there anyone out there that could point me in the right direction so I can step into this kernel mode function from a specific user-mode process? Or, at least set a proper kernel-mode breakpoint so it is only triggered as a result of my particular user-mode process?

UPDATE: I loaded my module (happens to be named runlist.exe) in a user-mode debugger on the debugged OS (I happened to use OllyDbg). Once I was paused at a user-mode breakpoint only a couple instructions from SYSENTER, I suspended the OS using the kernel debugger. I then set the process context. The WinDbg command window contents were as follows:

WINDBG>!process 0 0 runlist.exe

PROCESS 820645a8  SessionId: 0  Cid: 01b4    Peb: 7ffd7000  ParentCid: 02b0
    DirBase: 089c02e0  ObjectTable: e1671bb0  HandleCount:   8.
    Image: runlist.exe

WINDBG>.process /i /r /p 820645a8
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
WINDBG>g
This command cannot be passed to the WinDbg plugin directly, please use IDA Debugger menu to achieve the same result.
Break instruction exception - code 80000003 (first chance)
WINDBG>.reload /user
Loading User Symbols
....
Caching 'Modules'... ok
WINDBG>lmu
start    end        module name
00400000 00405000   runlist  C (no symbols)           
7c340000 7c396000   MSVCR71    (private pdb symbols)  g:\symcache\msvcr71.pdb\630C79175C1942C099C9BC4ED019C6092\msvcr71.pdb
7c800000 7c8f6000   kernel32   (pdb symbols)          e:\windows\symbols\dll\kernel32.pdb
7c900000 7c9af000   ntdll      (pdb symbols)          e:\windows\symbols\dll\ntdll.pdb
WINDBG>bp 0x7c90d16a
WINDBG>bl
 0 e 7c90d16a     0001 (0001) ntdll!ZwCreateSection+0xa

Although I couldn't get my process' symbols to load with ".reload" (PDB is in the same directory - might need to copy it to my symbols dir), the breakpoint I care about is in ntdll anyway so I set it on the address 0x7C90D16A which the debugger recognized as being within ntdll.ZwCreateSection(). Oddly to me, in user-mode code this address resolves to ntdll.NtCreateSection(), but either way that breakpoint was only 2 instructions from where I had my user-mode break. When I resumed the machine, my intention was to "run" the user-mode debugged-process and this would trigger the kernel-mode breakpoint 2 instructions away. The kernel breakpoint was never hit and the app resumed past this point. I can however set a breakpoint on ntdll!ZwCreateSection() but then when resuming the OS, the breakpoint is repeatedly hit by other processes preventing me from getting back to the user-mode debugger so I can "run" it to that location only within my own process.

UPDATE Merging the tips provided by @conio, the following steps worked for me:

1> after attaching kernel debugger and booting target OS, suspend the OS and apply some configuration options:

!gflag +ksl         //allow sxe to report user-mode module load events under kernel debugger
sxe ld myproc.exe   //cause kernel debugger break upon process load
.sympath+ <path>    //path to HOST machine's user-mode app's symbols

2> run debugger to resume target OS

3> on the target, run the EXE we want to debug

4> kernel debugger should break; now enter the following commands to switch to the usermode context:

!process 0 0 myproc.exe                 //get address of EProcess structure (first number on 1st line after "PROCESS")
.process /i /r /p <eprocess*>           //set kernel debugger to process context
g                                       //continue execution to allow the context switch; debugger will break after switch complete
.reload /user                           //reload user symbols
lmu                                     //ensure you have symbols although not really necessary in my particular case

5> now since I already know what happens in the user-mode side of ntdll.NtCreateSection(), I just went ahead and set a breakpoint for the kernel mode side of that function, but specifying that I want the breakpoint to occur only within the context of my process. This way, the breakpoint is not triggered OS wide:

bu /p <eprocess*> nt!NtCreateSection        //set breakpoint in kernel side of function
g                                           //run to break

6> if all goes as planned, the breakpoint will wake up the debugger on the kernel mode side of NtCreateSection(). I appreciate all the responses and tips!


Solution

  • There are two ways to combine user-mode debugging with kernel-mode debugging and you're confusing and mixing them up.

    The way you tried is to use the kernel-mode debugger to debug kernel-mode code, use the user-mode debugger (ntsd) to debug user-mode code, and control the user-mode debugger running on the target machine from the kernel debugger. That's what the -d flag to ntsd does. This method is described in the Controlling the User-Mode Debugger from the Kernel Debugger page and its subpages on MSDN.

    What this does (more or less) is to redirect ntsd input and output to the kernel debugger. The modules pane - as the rest of the windows in WinDbg - belongs to the kernel debugger. Your only interaction with the user-mode debugger is through the tunnel the kernel debugger creates, and you can access it only through the command window. This is documented in the documentation for the -d flag:

    -d

            Passes control of this debugger to the kernel debugger. If you are debugging CSRSS, this control redirection always is active, even if -d is not specified. (This option cannot be used during remote debugging -- use -ddefer instead.) See Controlling the User-Mode Debugger from the Kernel Debugger for details. This option cannot be used in conjunction with either the -ddefer option or the -noio option.

            Note  If you use WinDbg as the kernel debugger, many of the familiar features of WinDbg are not available in this scenario. For example, you cannot use the Locals window, the Disassembly window, or the Call Stack window, and you cannot step through source code. This is because WinDbg is only acting as a viewer for the debugger (NTSD or CDB) running on the target computer.

    The second way, which is the one used in the link you put, is to use the kernel debugger to debug both kernel-mode code and user-mode code. No user-mode debugger. No ntsd. You said you've followed the guide, but in fact you didn't. If you had, there wouldn't be any ntsd.

    I suggest you use this method for start, and after that use the user-mode debugger only if you find out you need to (because you want to use a user-mode extension, for example).

    In order for the kernel debugger to work well with user-mode modules you have to enable the Enable loading of kernel debugger symbols GlobalFlag. Use !gflag +ksl to do that.

    Once you do that, break on the loading of your process using sxe ld:runlist, set the breakpoint (possibly with the /p option) and debug whatever it is that you want.

    Just do that instead of all the ntsd mess.