Principle of Windows kernel debugger
ArticleAuthor: sobeit
Some time ago, I was suddenly interested in implementing the kernel debugger. So I briefly analyzed the principle of the mainstream kernel debugger in windows and wrote an extremely simple debugger after imitating the principle :)
The difference between windbg and the user debugger is that the kernel debugger starts on one machine and uses serial port debugging to debug another associated system started in debug mode. This system can be a system on a virtual machine, it can also be a system on another machine (this is only a method recommended and implemented by Microsoft, in fact, kernel debugger such as SoftICE can implement standalone debugging ). Many people think that the main functions are implemented in windbg. In fact, this is not the case. Windows has integrated the kernel debugging mechanism into the kernel, the kernel debugger such as windbg and KD only needs to send packets in specific formats in serial mode, such as interrupting the system, downloading the breakpoint, and displaying memory data. Then, the received data packet is displayed after windbg processing.
Before further introducing windbg, we will introduce two functions: kdptrace and kdpstub. I have mentioned these two functions in Windows exception handling process. Now, when an exception occurs in the kernel state, kidebugroutine is called twice. If the exception occurs in the user State, kidebugroutine is called once, in addition, the first call is the first time an exception is handled.
When the windbg is not loaded, kidebugroutine is kdpstub, which is easy to process, mainly for the exceptions caused by INT 0x2d, such as dbuplint, dbgprompt, load and unload symbols (the exceptions caused by INT 0x2d will be described in detail later), etc. add 1 to eip and skip the int 0x3 command followed by INT 0x2d.
Kdptrap is the function that actually implements the windbg function. It is responsible for handling all status_breakpoint and status_single_step (single step) exceptions. Status_breakpoint exceptions include int 0x3, dbuplint, dbgprompt, and load/unload symbols. It is the easiest to process dbuplint. kdptrap directly sends a package containing strings to the debugger. Because dbgprompt is used to output and receive strings, it first sends packets containing strings, and then falls into a loop waiting to receive packets containing reply strings from the debugger. Symbols is loaded and detached by calling kdpreportsymbolsstatechange, int 0x3 breakpoint exception and INT 0x1 single-step exception (these two exceptions are basically the most handled exceptions by the kernel debugger) by calling kdpreportexceptionstatechange, these two functions are very similar, both by calling the kdpsendwaitcontinue function. Kdpsendwaitcontinue is the manager of the kernel debugger function and is responsible for distributing various functions. This function sends the information to be sent to the kernel debugger, for example, the status of all current registers. After each step, we can find that the register information is updated, the kernel debugger accepts the package containing the latest machine status and the symbols status, so that we can see the corresponding response in the kernel debugger after loading and uninstalling symbols. Then, kdpsendwaitcontinue waits for the package containing commands from the kernel debugger to decide what to do next. Let's take a look at what kdpsendwaitcontinue can do:
Case dbgkdreadvirtualmemoryapi:
Kdpreadvirtualmemory (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdreadvirtualmemory64api:
Kdpreadvirtualmemory64 (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwritevirtualmemoryapi:
Kdpwritevirtualmemory (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwritevirtualmemory64api:
Kdpwritevirtualmemory64 (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdreadphysicalmemoryapi:
Kdpreadphysicalmemory (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwritephysicalmemoryapi:
Kdpwritephysicalmemory (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdgetcontextapi:
Kdpgetcontext (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdsetcontextapi:
Kdpsetcontext (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwritebreakpointapi:
Kdpwritebreakpoint (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdrestorebreakpointapi:
Kdprestorebreakpoin (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdreadcontrolspaceapi:
Kdpreadcontrolspace (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwritecontrolspaceapi:
Kdpwritecontrolspace (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdreadiospaceapi:
Kdpreadiospace (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwriteiospaceapi:
Kdpwriteiospace (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdcontinueapi:
If (nt_success (manipulatestate. U. Continue. continuestatus )! = False ){
Return continuesuccess;
} Else {
Return continueerror;
}
Break;
Case dbgkdcontinueapi2:
If (nt_success (manipulatestate. U. continue2.continuestatus )! = False ){
Kdpgetstatechange (& manipulatestate, contextrecord );
Return continuesuccess;
} Else {
Return continueerror;
}
Break;
Case dbgkdrebootapi:
Kdpreboot ();
Break;
Case dbgkdreadmachinespecificregister:
Kdpreadmachinespecificregister (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdwritemachinespecificregister:
Kdpwritemachinespecificregister (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdsetspecialcallapi:
Kdsetspecialcall (& manipulatestate, contextrecord );
Break;
Case dbgkdclearspecialcallsapi:
Kdclearspecialcils ();
Break;
Case dbgkdsetinternalbreakpointapi:
Kdsetinternalbreakpoint (& manipulatestate );
Break;
Case dbgkdgetinternalbreakpointapi:
Kdgetinternalbreakpoint (& manipulatestate );
Break;
Case dbgkdgetversionapi:
Kdpgetversion (& manipulatestate );
Break;
Case dbgkdcausebugcheckapi:
Kdpcausebugcheck (& manipulatestate );
Break;
Case dbgkdpageinapi:
Kdpnotsupported (& manipulatestate );
Break;
Case dbgkdwritebreakpointexapi:
Status = kdpwritebreakpointex (& manipulatestate,
& Messagedata,
Contextrecord );
If (Status ){
Manipulatestate. apinumber = dbgkdcontinueapi;
Manipulatestate. U. Continue. continuestatus = status;
Return continueerror;
}
Break;
Case dbgkdrestorebreakpointexapi:
Kdprestorebreakpointex (& manipulatestate, & messagedata, contextrecord );
Break;
Case dbgkdswitchprocessor:
Kdportrestore ();
Continuestatus = keswitchfrozenprocessor (manipulatestate. processor );
Kdportsave ();
Return continuestatus;
Case dbgkdsearchmemoryapi:
Kdpsearchmemory (& manipulatestate, & messagedata, contextrecord );
Break;
Read/write memory, search memory, set/restore breakpoints, continue execution, restart, and so on. Can all functions in windbg be implemented? Haha.
Each time the kernel debugger takes over the system, it calls kidebugroutine (kdptrace) in the kidispatchexception by calling it. But we know that the system must have encountered an exception when executing the kidispatchexception command. The kernel debugger and the system to be debugged are only connected through the serial port. The serial port will only be interrupted and the system will not cause exceptions. So how does one cause an exception in the system? The answer is in keupdatesystemtime. When halpclockinterrupt performs some underlying processing after the clock is interrupted, it will jump to this function to update the system time (because it is a jump rather than a call, therefore, after windbg is disconnected, tracing the stack won't find the address of halpclockinterrupt. It is one of the most frequently called functions in the system. In keupdatesystemtime, the system checks whether kddebuggerenable is true. If it is true, kdpollbreakin is called to determine whether there is a packet containing interrupt information from the kernel debugger. If yes, dbgbreakpointwithstatus is called and an int 0, after the exception handling process enters kdptrace, packets are sent to the kernel debugger according to different processing conditions, and the response to kernel debugging is infinite loop. Now we can understand why keupdatesystemtime-> rtlpbreakwithstatusinstruction can be found in sequence after the system is interrupted in windbg. The system stops at the int 0x3 command (in fact, int 0x3 has already been executed, but the EIP has been reduced by 1), and has actually entered the kidispatchexception-> kdptrap, and handed the control to the kernel debugger.
In addition to int 0x3, the system interacts with the debugger by dbuplint, dbgprompt, loading, and uninstalling symbols. They call debugservice to obtain the service.
Ntstatus debugservice (
Ulong serviceclass,
Pvoid arg1,
Pvoid arg2
)
{
Ntstatus status;
_ ASM {
MoV eax, serviceclass
MoV ECx, arg1
MoV edX, arg2
Int 0x2d
Int 0x3
MoV status, eax
}
Return status;
}
Serviceclass can be beakpoint_print (0x1), breakpoint_prompt (0x2), breakpoint_load_symbols (0x3), and breakpoint_unload_symbols (0x4 ). Why do I need to share it with int 0x3 and M $?Code(I didn't understand what it meant-_-) because of the int 0x2d trap processing.ProgramAfter some processing, jump to the int 0x3 trap handler to continue processing. But in fact, there is no processing for the int 0x3 command, just to add 1 to the EIP and skip it. So this int 0x3 can be changed to any byte.
The exception records generated by INT 0x2d and INT 0x3 are completed (exception_record) exceptionrecord. the exceptioncode is status_breakpoint (0x80000003), which is different from the exceptionrecord generated by INT 0x2d. numberparameters> 0 and predictionrecord. predictioninformation corresponds to the corresponding serviceclass, such as breakpoint_print. As a matter of fact, after the kernel debugger is mounted, it will not directly send packets to the kernel debugger through the int 0x2d trap service to process the sent characters such as dbgprs Int. M $ is safer because kdenterdebugger and kdexitdebugger are not called.
Finally, let's talk about the communication between the debug System and the kernel debugger. The debugged system and the kernel debugger send data packets through the serial port for communication. The IO port address of COM1 is 0x3f8, And the IO port address of com2 is 0x2f8. Before the debugging system prepares to send packets to the kernel debugger, it will call kdenterdebugger to pause the running of Other Processors and obtain the COM port spin lock (of course, this is for multi-processor ), set the port flag to the saved status. Call kdexitdebugger to restore a package after the package is sent. Each packet contains a packet header and specific content, just like a packet on the network. The Header Format is as follows:
Typedef struct _ kd_packet {
Ulong packetleader;
Ushort packettype;
Ushort bytecount;
Ulong packetid;
Ulong checksum;
} Kd_packet, * pkd_packet;
Packetleader is a packet sent from four identifiers of the same bytes. The average package is 0x30303030, the control package is 0x69696969, and the package of the interrupted debugging system is 0x62626262. Read one byte each time and read it four times in a row to identify the package. The packet that interrupts the system is very special. The data in the packet is only 0x62626262. The package identifier is the package size, type, package ID, and detection code. The packet header is followed by the specific data. This is similar to the packets transmitted over the network. There are also some similarities, for example, each packet sent to the debugger will receive an ACK reply packet to determine whether the debugger receives the packet. If you receive a resend package or haven't received a response for a long time, it will be sent again. For a package that sends an output string to the debugger, reports the symbol status, and so on, it is immediately returned as soon as the ACK package is received, and the system resumes execution. The performance of the system is as short as possible. Only the reported package will wait for each control package of the kernel debugger to complete the corresponding functions until the package contains the commands to continue executing. A 0xaa value will be added at the end of the package regardless of whether the package is sent or received, indicating the end of the package.
Now let's take a look at the debugging process with several examples.
I have previously asked jiurl why windbg's single step is so slow (relative to SoftICE) that he did not think it was slow? * $ & $ ^ (& I ft... Now we can understand why windbg's single step and the interruption from the operating system are so slow. The single step is slow because each step has to send and receive packets from the serial mode in addition to the necessary processing. How can this problem be solved. The system interruption is slow because the system will accept the interruption package from windbg only when the clock interruption occurs to keupdatesystemtime. Now let's take a look at why the kidispatchexception cannot be broken, but we can use a single step to track the cause of the kidispatchexception. If a breakpoint is placed somewhere in the kidispatchexception, when the breakpoint is executed, the system returns to the kidispatchexception again, and then runs to int 0x3. This leads to an endless loop, the Code modified by the breakpoint int 0 3 cannot be restored. But for int 0x1, it is caused by the fact that the TF bit in the eflag storage is set and is automatically reset every time, so the system can continue to execute without endless loops. Now that we know the internal mechanism, we can call the kdxxx function to implement a kernel debugger like windbg, or even replace kidebugroutine (kdptrap) implement a more powerful Debugger for your own functions.
The principle of SoftICE is completely different from that of windbg. It acquires control of the system by replacing the interrupt handler in the normal system, and thus it can implement standalone debugging. Its function implementation method is very underlying and seldom relies on the interface functions provided by windows. Most of its functions are implemented through IO port read/write.
SoftICE replaces the following interrupt (TRAP) handlers in the IDT table:
0x1: One-Step trap Handler
0x2: NMI unshielded interrupt
0x3: debug the trap Handler
0x6: Invalid operation code trap Handler
0xb: Segment does not have a trap Handler
0xc: Stack error trap Handler
0xd: general protective error trap Handler
0xe: Page error trap Handler
0x2d: debug the service trap Handler
0x2e: System Service trap Handler
0x31: 8042 keyboard controller interrupt handling program
0x33: Serial Port 2 (com2) interrupt handler
0x34: Serial Port 1 (COM1) interrupt handler
0x37: Parallel interrupt handling program
0x3c: PS/2 mouse interrupt handler
0x41: unused
(This is an interruption of replacement on the PIC system. If it is an APIC system, the change interrupt number is different, but it is also a replacement of these interrupt handlers)
The key is to replace the 0x3 debugging trap handler and the 0x31 i8042 keyboard interrupt handler (the keyboard is controlled by the i8042 chip ), softICE obtains control of the system from these two locations.
After the SoftICE service is started, in addition to replacing the processing program in IDT, SoftICE also has several important points. First, it hooks the i8042prt. read_port_uchar function in SYS, because after reading port 0x60, the status of the control register corresponding to port 0x64 is changed. So after the SoftICE keyboard interrupt control program reads port 0x60 and returns control to the normal keyboard interrupt control program, it should not be read again. In addition, the first 1 MB address space in the physical memory is mapped to the virtual address space by calling mmmapiospace, which includes the physical address of the memory, after the screen is re-painted, you can modify the video content mapped to the virtual address space.
If the display mode is color, the starting address of the video memory is 0xb8000, the CRT index register port is 0x3d4, And the CRT data register port is 0x3d5. If the display mode is monochrome, the starting address of the video memory is 0xb0000, the CRT index register port is 0x3b4, And the CRT data register port is 0x3b5. First write the index register to select one of the display control internal registers (r0-r17) to be set, and then write the parameter to its data register port.
the i8042 keyboard controller interrupt control driver is triggered every time a key is pressed and a key is popped up. After SoftICE hook the normal keyboard interrupt control program to gain control of the system, it first reads the scan code by pressing the key from Port 0x60 and then sends the universal EOI (0x20) to port 0x20) the interrupt has ended. If the activation key (CTRL + d) is not pressed, the normal keyboard interrupt handler is returned. If you press the hotkey, the system determines whether the console (the black screen that is waiting for the code to be displayed) is activated. If it is not activated, it is activated first. Then set the highest priority for the irq1 keyboard interrupt, and set the Interrupt Mask to 0x21 and 0 x A1 in the two 8259a interrupt controllers, which interrupt should be shielded and set to 1). Only irq1 and irq2 are allowed, because the PS/2 mouse interruption belongs to the 8259a-2 interrupt controller, only open irq2 can respond to the interruptions managed by 8259a-2) and irq12 (PS/2 mouse interruption, if any ), in this case, the system only responds to the three interruptions. The new keyboard and mouse interrupt handler creates a buffer to save a certain amount of input scan information. After the current work is completed, a circular code is entered, which is responsible for processing the scan code buffer for keyboard and mouse input, at the same time, the ing Address Buffer of the video memory is constantly updated to redraw the screen (this loop code is the same as that of the package sent from the serial port in the windbg loop, all the commands are cyclically waiting for the user in the background ). This loop code is called in the activation console routine, that is, when the console has been activated, the normal process will not go into this loop code again (nonsense, if you enter the system again, there will be no endless loops ). When a new key is pressed, the keyboard interrupt handler is re-called. Because the console is activated, it simply updates the content of the keyboard input buffer and then iret returns. It does not return a normal keyboard interrupt handler, because it will hand over control (to prove this is also very easy, in SoftICE, the normal keyboard interrupt handler is broken, then G, one second later, we can break it down here. This is F10. If SoftICE gives control to the normal keyboard interrupt handler, an endless loop has long occurred here ). The same is true for mouse interrupt drivers. At this time, the actual iret returns the loop code, so the code to be debugged will not be executed unless you press a key such as F10, it instructs the exit loop to return the interrupt handler at the beginning, and then iret to return the place where the interrupt was at the beginning. Of course, because the TF bit in eflag is set, the code that runs a command will go into that loop through a single-step processing program.
The processing of int 0x3 is similar. If the console is not activated, activate and shield all the interruptions except the keyboard, mouse, and 8259a-2 interrupt controller, and then enter the loop code.
For comparison, let's take a look at the process of processing int 0x3 and one step in SoftICE. When int 0x3 is executed, the console is activated and the interrupt is blocked. Then, the commands before and after int 0x3 are decompiled and written into the memory ing address space, and the latest register values are also written in, finally, wait for the keyboard to enter the command cyclically in the background. When the command is F10, set the TF bit of eflag, clear the interrupt shield register in the 8259a interrupt controller, enable all interrupts, and clear the console, return the new keyboard (or Int 0x3) interrupt handler from the loop code, and then return to the normal keyboard (or Int 0x3) interrupt handler, from here iret to the interrupted code for execution. After executing a command, the system enters the background loop code due to a single-step exception.
The reason why SoftICE's single step is much faster than windbg is very simple, softICE only needs to refresh the compiled code and data after simple processing, and then write it to the memory ing address buffer to refresh the screen. The serial packet sending and receiving are omitted. The interruption system is faster. If you press the key to interrupt the system, you do not need to interrupt the system with a clock like windbg.
Postscript:
It seems very easy to say. In fact, a kernel debugger is extremely complicated to implement, but it is not detailed. First, it is called "analysis" because of the question, which is something similar to science; second, water and time are limited (mainly due to ^). Third, it is not something that can be clearly written in detail. In addition, the disassembly of ntice. sys is really a tough task, which is n times more complex than the vulnerability analysis. At the beginning, I was dizzy when I was not on the road. I would like to express my special thanks to the author of syser, who is a cool man. When my understanding of SoftICE's working principles is still in chaos, I woke up with a few words ^. Due to the limited level, there are inevitably many mistakes and mistakes, and you should forget to point out :)