加载内核符号 - 使用 VMware 的 GDB 存根和 IDA Pro 进行 VMM 调试
参考:
1 https://www.triplefault.io/2017/07/setup-vmm-debugging-using-vmwares-gdb_9.html
2 https://www.triplefault.io/2017/07/loading-kernel-symbols-vmm-debugging.html
Sometimes you'll run into a situation that you can't analyze with a traditional kernel debugger like WinDbg. An example of such is trying to troubleshoot the runtime logic of PatchGuard (Microsoft's Kernel Patch Protection). In situations like this, you need to bust out the heavy tools. VMware has built in support for remote debugging of virtual machines running inside it through a GDB stub. IDA Pro, the defacto disassembler that most reverse engineers have, includes a GDB debugger. Together these make for a very powerful combo.
This article goes over how to setup VMware's GDB stub and how to connect to it using IDA Pro's GDB debugger.
Requirements
-
A copy of VMware Workstation (free 30-day trial). I'll be using VMware Workstation 12.5.7 (build-5813279).
-
Unfortunately, VMware Player (entirely free for non-commercial use) does not expose the GDB stub interface.
-
You can use either the Linux or Windows build of VMware. I'll be using the 64-bit Windows build.
-
The IDA Pro application. I'm using IDA Pro x64 Version 6.95.160808.
Optional, but preferred
-
A Windows operating system installed on your host and guest (VM). These do not have to be the same versions of Windows. My host and guest OS are both running Windows x64 10.0.15063 (Version 1703).
-
This can be any OS supported by VMware such as Ubuntu.
-
The second part of this tutorial (loading kernel symbols) assumes you're running a Windows 64-bit VM (AMD64).
Enabling the GDB stub within VMware
-
Select the VM you wish to enable GDB stub debugging on within VMware.
- VMs should be listed in the "Library" pane on the left of the GUI. If the "Library" pane is missing, you can restore it by selecting "View" then "Customize" and choosing "Library" (or hit F9).
-
Ensure that the VM is currently not running. If it's currently active, power it off via the menu bar: "VM" then "Power" then "Shut Down Guest" (or Ctrl+E).
-
Select "Edit virtual machine settings". Ensure that you are on the "Options" tab.
-
Find the "Working directory" text field and copy the string to your clipboard. 'Cancel' out of the prompt.
-
Go to the working directory.
-
Right-click on the *.vmx file and "Open with" your favorite text editor. I'll be using Notepad++.
-
Add one of the following lines to the end of the file, based on preference.
- If your VM is 32-bit and you want to debug locally:
debugStub.listen.guest32 = "TRUE" - If your VM is 64-bit and you want to debug locally:
debugStub.listen.guest64 = "TRUE" - If your VM is 32-bit and you want to debug remotely:
debugStub.listen.guest32.remote = "TRUE" - If your VM is 64-bit and you want to debug remotely:
debugStub.listen.guest64.remote = "TRUE"
- If your VM is 32-bit and you want to debug locally:
-
The default port for the GDB stub is 8864 for 64-bit guests and 8832 for 32-bit guests. If you'd like to change what port the VMware GDB stub listens on (e.g. 55555), add one of the following lines to the file:
- If your VM is 32-bit:
debugStub.port.guest32 = "55555" - If your VM is 64-bit:
debugStub.port.guest64 = "55555"
- If your VM is 32-bit:
-
If you want to start debugging immediately on BIOS load add one of the following lines to your file:
- If your VM is 32-bit:
monitor.debugOnStartGuest32 = "TRUE" - If your VM is 64-bit:
monitor.debugOnStartGuest64 = "TRUE"
- If your VM is 32-bit:
-
To make it difficult to detect breakpoints that you've set using GDB, it's strongly recommended to add the following option too:
* debugStub.hideBreakpoints = "TRUE"An important thing to note is that this option is restricted by the number of hardware breakpoints available to the processor (usually 4). -
Save the *.vmx file via "File" and then "Save" from the menu bar (or hit Control+S). Here's a copy of the contents of my *.vmx file:
[![](https://lh3.googleusercontent.com/9xOZloj_kjVrTdeVWpQZMK0GEyjkF_SBMZMfH5XPv2Ova4RY3w4-if8FmcHb2wE9sjkJUPrVM1UnWB99zI5Za9jJb1HSML5aeJgpQu8j4IpzP8yfe5cweowX4vs-J_60t8LdrspU)](https://lh3.googleusercontent.com/9xOZloj_kjVrTdeVWpQZMK0GEyjkF_SBMZMfH5XPv2Ova4RY3w4-if8FmcHb2wE9sjkJUPrVM1UnWB99zI5Za9jJb1HSML5aeJgpQu8j4IpzP8yfe5cweowX4vs-J_60t8LdrspU)
Close the file.
- Run the VM corresponding to the *.vmx file you just edited. Validate that the GDB stub is currently running by opening the vmware.log file in the same directory as the *.vmx file:
[![](https://lh5.googleusercontent.com/jDGePzDvbJuCwIp-87Z8IEDbznQ0mKdXcfSiI6noX0m3ECE2snn_4BI6ffGEv3xGqbdieVMyDJHQk4THtvoKdc2O_iSueH3mxJZmya97HBnxMvYcZtupJ6u4cKBWtlHDMLjNFjoX)](https://lh5.googleusercontent.com/jDGePzDvbJuCwIp-87Z8IEDbznQ0mKdXcfSiI6noX0m3ECE2snn_4BI6ffGEv3xGqbdieVMyDJHQk4THtvoKdc2O_iSueH3mxJZmya97HBnxMvYcZtupJ6u4cKBWtlHDMLjNFjoX)
If you see a message from "Debug stub" that tells you VMware is "listening" for a debug connection on a certain port number, you're in a good state.
If you are missing that log line or have an error, ensure that your \*.vmx file has the appropriate settings. **Remember**: you must edit the \*.vmx file when the Virtual Machine is off or your changes may be lost.
Configuring the GDB debugger within IDA Pro
-
Launch the 64-bit version of IDA Pro if you're debugging a 64-bit VM and the 32-bit version of IDA Pro if you're debugging a 32-bit VM.
-
Skip the "Welcome" dialog (by hitting "Go") and go to the main disassembler window. Choose "Debugger" and then "Attach" and finally "Remote GDB debugger" from the menu bar.
-
Enter the appropriate "Hostname" and "Port". These were set up by you during steps 7 and 8 of the "Enabling the GDB stub within VMware" section. Furthermore, these can be validated in the vmware.log file (this was done in step 12 of the same section). Then hit "Debug options".
-
In the "Debugger setup" window, select "Set specific options".
-
Ensure that the right "Processor" is set in the drop down box. If you're debugging a 64-bit edition of Windows (AMD64), select "Intel x64". If you're debugging a 32-bit edition of Windows (X86), select "Intel x86".
Select 'OK' in the "GDB configuration" window. And then select 'OK' in the "Debugger setup" window. Finally, select 'OK' in the "Debug application setup" window.
-
The VM will become suspended and a green "play" button will appear. At this point, IDA should bring up a window with the title "Choose process to attach to".
Select "
" and hit 'OK'. -
If you see this window, that means you're almost done.
-
Select "Debugger" and then "Manual memory regions" from the menu bar.
-
Inside of the "Manual memory regions" tab, right click and select "Insert" (or just press "Insert" on your keyboard).
-
A new window will pop up. Enter in the "Start address" as 0 and the "End address" as -2. Make sure the right "segment" is selected (e.g. 64-bit segment for 64-bit VM debugging) and hit 'OK'.
[![](https://lh5.googleusercontent.com/lQ_5DJbxkLRxvmGO7-wAWyzpAFj_75WodJgozhpCqgrQPqAAAmY9rLqAjJjzmxGfCJXiQ7GigvMyQz_Dher85htA3znF9LpRuJUbKxfSfzx730NrIxP6Jmm6u3L_bm0fdSASbj13)](https://lh5.googleusercontent.com/lQ_5DJbxkLRxvmGO7-wAWyzpAFj_75WodJgozhpCqgrQPqAAAmY9rLqAjJjzmxGfCJXiQ7GigvMyQz_Dher85htA3znF9LpRuJUbKxfSfzx730NrIxP6Jmm6u3L_bm0fdSASbj13)
* This essentially maps all virtual memory from 0 to 0xFFFFFFFFFFFFFFFE (on x64).
* "-1" is not an acceptable boundary for IDA Pro as the "End address".
- Find the "General Registers" window and find the IP register (RIP on x64, EIP on x86).
* If the "General Registers" window is gone, select "Debugger" and then "Debugger windows" and finally "General Registers" from the menu bar. - Right click on the IP register and select "Jump" from the context menu that appears.
- Your memory view will become synched to the IP register. If there are raw bytes listed and not code, don't panic. Place your cursor on the address of the IP and hit "C".
[![](https://lh4.googleusercontent.com/e06KA-HaAKERyNjRtDstLh4gx9oy8jRODkDDpd1ytINl3xWh1-Vzpao3C-5So5RoHdMaEX2cvRGvOPGHQvJJVkbiNQqfrLiufka_ZRxJqmEKamf6Jc7TLM6YGW-6Hc2UXAZeMpfH)](https://lh4.googleusercontent.com/e06KA-HaAKERyNjRtDstLh4gx9oy8jRODkDDpd1ytINl3xWh1-Vzpao3C-5So5RoHdMaEX2cvRGvOPGHQvJJVkbiNQqfrLiufka_ZRxJqmEKamf6Jc7TLM6YGW-6Hc2UXAZeMpfH)
- Congratulations. At this point you've successfully set up VMware's GDB stub and IDA Pro's GDB debugger. You are now able to debug the VM and apply breakpoints through the IDA Pro GUI just as you would normally through a kernel debugger. Most of the functionality of the GDB debugger can be accessed through the "Debugger" menu bar.
- This type of debugging is transparent to the kernel and therefore "debugger" checks like "KdDebuggerEnabled" and "KdDebuggerNotPresent" will not trigger. Furthermore, if the debugStub.hideBreakpoints option was enabled, breakpoints (up until the hardware maximum) will not make any inline code edits!
Final thoughts
Ultimately, the GDB debugger is not very useful without kernel symbols being loaded. One option, albeit a naive one, is to attach WinDbg as a kernel debugger while running IDA's GDB stub in the background. A tutorial on how to setup kernel debugging using WinDbg and VMware can be found here. You are then able to use the symbolic data that is provided from WinDbg to power debugging in IDA's GDB debugger. This is very cumbersome and has many disadvantages such as not being able to avoid kernel debugger checks.
Luckily, there is a better way. In the second part of this series, we'll discover how to load kernel symbols in IDA Pro's GDB debugger.
第二部分
This article assumes you've read the first part of the series. In particular, at this point you should have successfully setup VMware's GDB stub and IDA Pro's GDB debugger. You should now be in a connected state and broken into IDA Pro's debugger GUI.
Furthermore, the focus of this post is going to be exclusively on loading kernel symbols for 64-bit editions of Windows (AMD64). Different operating systems (and different architectures of Windows) require slight modifications to the article's logic.
Where's Waldo ntoskrnl?
The end goal
The first and most important thing is to discover where the NT Kernel (ntoskrnl.exe) is loaded in memory since it's not at any fixed (static) address thanks to address space layout randomization (ASLR).
We are then able to force IDA Pro to load symbol data (PDBs) at ntoskrnl's base address to have useful debugging information. From there, we can enumerate the linked list, nt!PsLoadedModuleList, to figure out where other kernel mode components are located. However, this isn't trivial. When you break in to IDA Pro's GDB debugger, it's difficult to know what state you'll be in on any given processor. You might be executing code in a usermode process, or you might be busy servicing a system call. Additionally, you're further restricted to the functionality the GDB stub exposes.
Enter the _KPCR
On all architectures and versions of Windows, each processor maintains a control structure dubbed as the _KPCR (Kernel Processor Control Region). This structure is massive and it can be used to infer exactly what the processor is doing. On Windows 10 (15063.0.amd64fre.rs2_release.170317-1834), the _KPCR is 0x6bc0 bytes large. It contains many kernel pointers that we can leverage to figure out exactly where the base of ntoskrnl is in memory. A link detailing the members of the _KPCR can be found here.
This structure can be accessed through its virtual address or through the fs segment on x86 and the gs segment on x64. In fact, if you've done any reverse engineering of the Windows kernel, you should have seen many examples of Windows itself accessing members of the _KPCR through the segment selector.
For example, when an int 3 (a software breakpoint; 0xCC) is executed by the processor, control is redirected by the CPU to a handler registered in the appropriate position of the IDT (Interrupt Descriptor Table). We'll touch more on this process later. In Windows, the handler for software breakpoints is nt!KiBreakpointTrap. Here is a snippet of the assembly code of the handler under AMD64:
.text:00000001401749C0 KiBreakpointTrap proc near ; DATA XREF: .pdata:000000014039F534o | |
.text:00000001401749C0 ; INITDATA:000000014082A1A8o | |
.text:00000001401749C0 | |
.text:00000001401749C0 sub rsp, 8 | |
.text:00000001401749C4 push rbp | |
.text:00000001401749C5 sub rsp, 158h | |
.text:00000001401749CC lea rbp, [rsp+80h] | |
.text:00000001401749D4 mov [rbp+0E8h+var_13D], 1 | |
.text:00000001401749D8 mov [rbp+0E8h+var_138], rax | |
.text:00000001401749DC mov [rbp+0E8h+var_130], rcx | |
.text:00000001401749E0 mov [rbp+0E8h+var_128], rdx | |
.text:00000001401749E4 mov [rbp+0E8h+var_120], r8 | |
.text:00000001401749E8 mov [rbp+0E8h+var_118], r9 | |
.text:00000001401749EC mov [rbp+0E8h+var_110], r10 | |
.text:00000001401749F0 mov [rbp+0E8h+var_108], r11 | |
.text:00000001401749F4 test [rbp+0E8h+arg_0], 1 | |
.text:00000001401749FB jz short loc_140174A58 | |
.text:00000001401749FD swapgs | |
.text:0000000140174A00 mov r10, gs:188h | |
.text:0000000140174A09 test [r10+_ETHREAD.Tcb.Header.___u0.__s3.TimerMiscFlags], 80h | |
.text:0000000140174A0E jz short loc_140174A43 | |
.text:0000000140174A10 mov ecx, 0C0000102h | |
.text:0000000140174A15 rdmsr | |
.text:0000000140174A17 shl rdx, 20h | |
.text:0000000140174A1B or rax, rdx | |
.text:0000000140174A1E cmp [r10+_ETHREAD.Tcb.Teb], rax | |
.text:0000000140174A25 jz short loc_140174A43 | |
.text:0000000140174A27 mov rdx, [r10+_ETHREAD.Tcb.___u35.__s8.Ucb] | |
.text:0000000140174A2E bts [r10+_ETHREAD.Tcb.___u16.MiscFlags], 8 | |
.text:0000000140174A34 dec [r10+_ETHREAD.Tcb.___u35.__s4.SpecialApcDisable] | |
.text:0000000140174A3C mov [rdx+80h], rax | |
.text:0000000140174A43 | |
.text:0000000140174A43 loc_140174A43: ; CODE XREF: KiBreakpointTrap+4Ej | |
.text:0000000140174A43 ; KiBreakpointTrap+65j | |
.text:0000000140174A43 test byte ptr [r10+3], 3 | |
.text:0000000140174A48 mov [rbp+0E8h+var_68], 0 | |
.text:0000000140174A51 jz short loc_140174A58 | |
.text:0000000140174A53 call KiSaveDebugRegisterState |
view raw nt!KiBreakpointTrap snippet - Windows 10 (15063.0.amd64fre.rs2_release.170317-1834).asm hosted with ❤ by GitHub
In particular, at address 0x00000001401749FD we see a swapgs instruction. Since the gs selector means different things in user-mode (_TEB) and the kernel (_KPCR), this instruction is utilized to ensure that we're operating on the kernel-mode construct (_KPCR). Immediately following that instruction at address 0x0000000140174A00, we have an access of the gs segment with a mov r10, gs:188h. The astute reader will realize that upon execution of this instruction, r10will contain the pointer from the _KPCR.Prcb.CurrentThread. This is discerned from the definition of the structure's members posted above. A breakdown of this process can be illustrated below:
kd> dt nt!_KPCR -b | |
... | |
+0x180 Prcb : _KPRCB | |
... | |
+0x008 CurrentThread : Ptr64 | |
At offset +0x180 in the _KPCR there is a _KPRCB structure. | |
At offset +0x008 in the _KPRCB there is a CurrentThread pointer. | |
0x180+0x008 = 0x188 | |
Therefore, accessing gs:188 in kernel-mode extracts this CurrentThread pointer (_KPCR.Prcb.CurrentThread). |
view raw _KPCR.Prcb.CurrentThread - Windows 10 (15063.0.amd64fre.rs2_release.170317-1834).txt hosted with ❤ by GitHub
We don't know the _KPCR's exact linear address (it too isn't allocated at a fixed location), but we should be able to access it through the segment selector, though, just like the Windows kernel does. This approach might seem like the ideal one, but, unfortunately, we're further restricted by the functionality of the GDB stub. Let's see what the GDB stub exposes by issuing help:
There are only three major commands available: help, r, and linuxoffsets. We've just executed help, and linuxoffsets isn't relevant to us since we're debugging a Windows kernel. The only other command is r. At first, r looks very useful to us. However, on closer examination, we can see that the GDB stub is unable to read arbitrary offsets off of the gs selector, e.g. the _KPCR.Prcb.CurrentThread from gs:188h by executing r gs:188h.
At least executing r gs without an offset produces data:
GDB>r gs | |
gs 0x2b base 0x00000000 limit 0xffffffff type 0x3 s 1 dpl 3 p 1 db 1 |
view raw Executing 'r gs' - GDB stub output.txt hosted with ❤ by GitHub
This command should get us the base of the gs selector. We then should be able to define a _KPCR structure at that location using IDA Pro. According to the GDB stub, though, our base is 0. If we go to that memory location in the "IDA View - RIP" tab by pressing 'G' and entering 0 in the "Jump to address" window, we don't see anything there:
What changed from x86 to x64?
If you ran this test on a VM running on an x86 (32-bit) version of Windows and substituted fs for gs, the base of the fs selector would not be 0. It would be a valid memory location. You would then have the address of the _KPCR and could continue on your merry way.
Unfortunately, you're a sucker for pain and are following this tutorial down to the T. In 64-bit (long) mode on x64, the cs, ss, ds, and es segment selectors have a zero-forced base address. gs and fs are the exceptions and have a non-zero base address. So, how is it possible that the base of the gs selector is 0 when Windows itself uses the segment selector to retrieve processor state?
The answer is in the model-specific registers, MSRs. MSRs are per-processor registers that can be read via rdmsr and written via wrmsr instructions. On x64, the IA32_GS_BASE (0xC0000101) and IA32_KERNEL_GS_BASE (0xC0000102) MSRs are used for storage of the base address of the gs selector. swapgs was introduced to exchange the address of the current gs base register with the value contained in the IA32_KERNEL_GS_BASE MSR.
This means that we could, theoretically, read the IA32_GS_BASE MSR if we're executing code in CPL0 (ring0/kernel-mode). This would get us the base address of the gs segment. However, that's not directly possible through the VMware GDB stub. There is no support for reading or writing to MSRs directly.
A shimmer in the shadows
Nevertheless, through persistence, we come up with an approach that plays nicely given our constraints. There are multiple ways to skin a cat and this approach may not be the most elegant solution, but it should work nicely for all x64 Windows kernels.
The basic idea is to leverage the IDT, the interrupt descriptor table, to find a symbol that's in the address space of ntoskrnl. We can access the idtr, a register that houses the IDT, through the GDB stub:
GDB>r idtr | |
idtr base=0xfffff802c4850000 limit=0xfff |
view raw Executing 'r idtr' - GDB stub output.txt hosted with ❤ by GitHub
Once we have the base of the IDT, in our case 0xfffff802c4850000, we can access the first entry of the IDT. This should resolve to a symbol within ntoskrnl (nt!KiDivideErrorFault):
kd> !idt -a | |
Dumping IDT: fffff802c4850000 | |
00: fffff802c27f4300 nt!KiDivideErrorFault | |
01: fffff802c27f4400 nt!KiDebugTrapOrFault | |
02: fffff802c27f45c0 nt!KiNmiInterrupt Stack = 0xFFFFF802C486D000 | |
03: fffff802c27f49c0 nt!KiBreakpointTrap | |
04: fffff802c27f4ac0 nt!KiOverflowTrap | |
05: fffff802c27f4bc0 nt!KiBoundFault | |
06: fffff802c27f4e40 nt!KiInvalidOpcodeFault | |
07: fffff802c27f5080 nt!KiNpxNotAvailableFault | |
08: fffff802c27f5140 nt!KiDoubleFaultAbort Stack = 0xFFFFF802C486B000 | |
09: fffff802c27f5200 nt!KiNpxSegmentOverrunAbort | |
0a: fffff802c27f52c0 nt!KiInvalidTssFault | |
0b: fffff802c27f5380 nt!KiSegmentNotPresentFault | |
0c: fffff802c27f5500 nt!KiStackFault | |
0d: fffff802c27f5640 nt!KiGeneralProtectionFault | |
0e: fffff802c27f5740 nt!KiPageFault | |
0f: fffff802c27ee128 nt!KiIsrThunk+0x78 | |
... |
view raw IDT - Windows 10 (15063.0.amd64fre.rs2_release.170317-1834).txt hosted with ❤ by GitHub
From there, we can walk kernel memory backwards until we get to a valid PE header. Since the symbol is contained within ntoskrnl's address space, the first valid PE header should belong to ntoskrnl:
_Figure 1: Layout of kernel memory. _
Writing an IDA script using IDAPython
It'd be nice to programmatically implement the algorithm described above so we don't need to manually go through it each time we're trying to discover the base address of ntoskrnl. We'll do this by writing a script for IDA Pro to run. I chose to do this with IDAPython instead of IDC (IDA's C-like bindings) because of the niceties that Python provides (like string manipulation).
The basics
We'll start by switching the input from "GDB" to "Python" in the "Output window". If your "Output window" is missing, you can restore it by selecting "Windows" and then "Output window" from the menu bar:
We can see all the functionality exposed by IDAPython by executing the Python command dir() in the text box. If you try to do this, you'll see lots of output. It's easy to feel overwhelmed. Luckily, there exists amble documentation on the Hex-Rays website that can help us navigate these murky waters.
I try to find useful things by searching for it first in the dir() listing. You can position your cursor in the "Output window" and press Alt+T to search for a keyword. To find the next occurrence, you can hit Ctrl+T. If this fails, I move on to the documentation.
Sending a command to the GDB stub
Our first task is to figure out how to send a command to the GDB stub. If you search for the "command" keyword in the "Output window" you'll find something labeled "SendDbgCommand". Let's see what this function does by executing help(SendDbgCommand):
Python>help(SendDbgCommand) | |
Help on function SendDbgCommand in module idc: | |
SendDbgCommand(cmd) | |
Sends a command to the debugger module and returns the output string. | |
An exception will be raised if the debugger is not running or the current debugger does not export | |
the 'SendDbgCommand' IDC command |
view raw help(SendDbgCommand) - IDAPython output.txt hosted with ❤ by GitHub
It seems very relevant to us. Let's give it a try:
Python>SendDbgCommand("help") | |
Supported monitor commands: | |
help | |
r | |
linuxoffsets | |
Please use "monitor help <command>" to get details. |
view raw SendDbgCommand("help") - IDAPython output.txt hosted with ❤ by GitHub
Looks like it's working. This is the same output we received from the GDB stub when we issued the help command.
Parsing the response from the GDB stub
Now that we know how to send a command to the GDB stub, we need to issue a command to retrieve the contents of the idtr. We then parse and extract the base address from the resulting string.
Python># Save off the result into the monitor_result variable. | |
Python>monitor_result = SendDbgCommand("r idtr") | |
Python># Display the monitor_result variable. | |
Python>print monitor_result | |
idtr base=0xfffff802c4850000 limit=0xfff | |
Python># Extract the base from the monitor_result variable using string splicing. | |
Python>base_pos = monitor_result.find("base=") | |
Python>limit_pos = monitor_result.rfind(" limit") | |
Python>idt_base = monitor_result[base_pos+5:limit_pos] | |
Python>print idt_base | |
0xfffff802c4850000 |
view raw Parsing the response from the GDB stub - IDAPython output.txt hosted with ❤ by GitHub
It's important to tell Python that we're working with an integer object by "casting" the string to an integer-type:
Python># We use 16 here because the string represents a hexadecimal number. | |
Python>idt_base = int(idt_base, 16) | |
Python>print idt_base | |
18446735289503514624 | |
Python>print hex(idt_base) | |
0xfffff802c4850000L |
view raw int-ifying the result - IDAPython output.txt hosted with ❤ by GitHub
Easy!
Getting the first IDT entry's handler
We have the base of the IDT in idt_base. Our next task is to retrieve the first entry in the IDT. The IDT is effectively an array that contains 256 IDT entries (0-0xFF) on x64. The format of the IDT is dictated by the architecture of the processor (e.g. Intel x64). Each IDT entry on x64 takes the following form:
kd> dt nt!_KIDTENTRY64 -b | |
+0x000 OffsetLow : Uint2B | |
+0x002 Selector : Uint2B | |
+0x004 IstIndex : Pos 0, 3 Bits | |
+0x004 Reserved0 : Pos 3, 5 Bits | |
+0x004 Type : Pos 8, 5 Bits | |
+0x004 Dpl : Pos 13, 2 Bits | |
+0x004 Present : Pos 15, 1 Bit | |
+0x006 OffsetMiddle : Uint2B | |
+0x008 OffsetHigh : Uint4B | |
+0x00c Reserved1 : Uint4B | |
+0x000 Alignment : Uint8B |
view raw nt!_KIDTENTRY64 (15063.0.amd64fre.rs2_release.170317-1834).txt hosted with ❤ by GitHub
To get to the handler (e.g. where the processor moves control to when an interrupt occurs), the target address is built from the OffsetHigh, OffsetMiddle, and OffsetLow fields of this structure using the following algorithm: HandlerAddress = ((OffsetHigh << 32) + (OffsetMiddle << 16) + OffsetLow).
We'll leverage the Dbg* commands to read virtual memory from IDAPython. Since we're extracting the first IDT entry, we can just read directly from the start of our idt_base:
Python>OffsetLow = DbgWord(idt_base + 0) | |
Python>OffsetMiddle = DbgWord(idt_base + 6) | |
Python>OffsetHigh = DbgDword(idt_base + 8) | |
Python>HandlerAddress = ((OffsetHigh << 32) + (OffsetMiddle << 16) + OffsetLow) | |
Python>print hex(HandlerAddress) | |
0xfffff802c27f4300L |
view raw Extracting the handler address from an _KIDTENTRY64 - IDAPython output.txt hosted with ❤ by GitHub
This shows us that the handler for the first IDT entry (nt!KiDivideErrorFault) is loaded at 0xfffff802c27f4300. If we wanted to read the N'th IDT entry, we'd have to index into the array by adding 0x10, the size of a _KIDTENTRY64, times the location in the array (in this case N). So, to index into the 3rd IDT entry, we'd apply the following math: idt_entry = idt_base + (0x10 * 2).
Finding the base address from a symbol within ntoskrnl
First, we'll define a simple helper function that will align addresses to their page boundaries. This will help speed up our lookup because we know that the base address of ntoskrnl will be on a page boundary:
Python>def page_align(address): | |
Python> return (address & ~(0x1000-1)) | |
Python>print hex(page_align(HandlerAddress)) | |
0xfffff802c27f4000L | |
Python>print hex(page_align(0x1000)) | |
0x1000 | |
Python>print hex(page_align(0x1fff)) | |
0x1000 | |
Python>print hex(page_align(0x1fff + 1)) | |
0x2000 |
view raw Page alignment helper function - IDAPython.txt hosted with ❤ by GitHub
We'll then create a very simple loop to walk memory backwards (on a page-aligned boundary) searching for the magical value 0x5A4D, commonly known as 'MZ' (IMAGE_DOS_SIGNATURE). This value signifies the start of the IMAGE_DOS_HEADER which is also the base address of an image:
Python>DosHeader = page_align(HandlerAddress) | |
Python>while True: | |
Python> e_magic = DbgWord(DosHeader + 0) | |
Python> if e_magic == 0x5A4D: | |
Python> print "Base address located at {}".format(hex(DosHeader)) | |
Python> break | |
Python> DosHeader -= 0x1000 | |
Python> | |
Base address located at 0xfffff802c2680000L |
view raw Walking kernel memory backwards - IDAPython.txt hosted with ❤ by GitHub
Voila! The base address of ntoskrnl is discovered at 0xfffff802c2680000.
Creating the final version of the script
After some refactoring and code tidying (including error checking), we produce a much better version of the script. This does the same thing as the commands we inserted in the IDAPython "Output window":
''' | |
Module Name: | |
find_nt_imagebase_x64.py | |
Abstract: | |
Discovers the base address of ntoskrnl when IDA's GDB stub is | |
loaded by leveraging the IDT. | |
NOTE: This is only compatible for 64-bit editions of Windows. | |
Author: | |
Nemanja (Nemi) Mulasmajic <nm@triplefault.io> | |
http://triplefault.io | |
''' | |
from idaapi import * | |
# The size of a page on x86/AMD64. | |
PAGE_SIZE = 4096 | |
def splice(string, start_token, end_token): | |
''' | |
Given an input 'string', extracts the contents between the | |
starting and ending tokens. | |
''' | |
start_pos = string.find(start_token) | |
end_pos = string.rfind(end_token) | |
# This means our tokens are invalid and don't exist in the string. | |
if start_pos == -1 or end_pos == -1: | |
return None | |
start_pos += len(start_token) | |
# Can't splice the string if this is true. | |
if start_pos > end_pos: | |
return None | |
# Splices the string. | |
return string[start_pos:end_pos] | |
def read_idt_entry(address): | |
''' | |
Extracts the virtual address of the _KIDTENTRY64 at 'address'. | |
''' | |
# nt!_KIDTENTRY64 | |
''' | |
+0x000 OffsetLow : Uint2B | |
+0x002 Selector : Uint2B | |
+0x004 IstIndex : Pos 0, 3 Bits | |
+0x004 Reserved0 : Pos 3, 5 Bits | |
+0x004 Type : Pos 8, 5 Bits | |
+0x004 Dpl : Pos 13, 2 Bits | |
+0x004 Present : Pos 15, 1 Bit | |
+0x006 OffsetMiddle : Uint2B | |
+0x008 OffsetHigh : Uint4B | |
+0x00c Reserved1 : Uint4B | |
+0x000 Alignment : Uint8B | |
''' | |
# Relevant structure offsets. | |
OFFSET_KIDTENTRY64_OFFSETLOW = 0x0 | |
OFFSET_KIDTENTRY64_OFFSETMIDDLE = 0x6 | |
OFFSET_KIDTENTRY64_OFFSETHIGH = 0x8 | |
# Read the data. | |
OffsetLow = DbgWord(address + OFFSET_KIDTENTRY64_OFFSETLOW) | |
OffsetMiddle = DbgWord(address + OFFSET_KIDTENTRY64_OFFSETMIDDLE) | |
OffsetHigh = DbgDword(address + OFFSET_KIDTENTRY64_OFFSETHIGH) | |
# Failed to read some part of the offset. | |
if OffsetLow is None or OffsetMiddle is None or OffsetHigh is None: | |
return None | |
# Build the 64-bit address representing this structure. | |
return ((OffsetHigh << 32) + (OffsetMiddle << 16) + OffsetLow) | |
def page_align(address): | |
''' | |
Aligns the 'address' on an architecture page boundary (0x1000). | |
''' | |
return (address & ~(PAGE_SIZE - 1)) | |
def find_base_address(address, verbose = True): | |
''' | |
Walks memory backwards from the starting 'address' until a | |
valid PE header is located. | |
''' | |
# nt!_IMAGE_DOS_HEADER | |
''' | |
+0x000 e_magic : Uint2B | |
+0x002 e_cblp : Uint2B | |
+0x004 e_cp : Uint2B | |
+0x006 e_crlc : Uint2B | |
+0x008 e_cparhdr : Uint2B | |
+0x00a e_minalloc : Uint2B | |
+0x00c e_maxalloc : Uint2B | |
+0x00e e_ss : Uint2B | |
+0x010 e_sp : Uint2B | |
+0x012 e_csum : Uint2B | |
+0x014 e_ip : Uint2B | |
+0x016 e_cs : Uint2B | |
+0x018 e_lfarlc : Uint2B | |
+0x01a e_ovno : Uint2B | |
+0x01c e_res : [4] Uint2B | |
+0x024 e_oemid : Uint2B | |
+0x026 e_oeminfo : Uint2B | |
+0x028 e_res2 : [10] Uint2B | |
+0x03c e_lfanew : Int4B | |
''' | |
IMAGE_DOS_SIGNATURE = 0x5A4D # 'MZ' | |
# Relevant structure offsets. | |
OFFSET_IMAGE_DOS_HEADER_E_MAGIC = 0x0 | |
OFFSET_IMAGE_DOS_HEADER_E_LFANEW = 0x3c | |
# nt!_IMAGE_NT_HEADERS | |
''' | |
+0x000 Signature : Uint4B | |
+0x004 FileHeader : _IMAGE_FILE_HEADER | |
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER64 | |
''' | |
IMAGE_NT_SIGNATURE = 0x00004550 # 'PE00' | |
# Relevant structure offsets. | |
OFFSET_IMAGE_NT_HEADERS_SIGNATURE = 0x0 | |
# Find the page aligned offset of the specified symbol's address by | |
# stripping off the page RVA. | |
DosHeader = page_align(address) | |
if verbose: | |
print "\nSearching for base address of symbol @ {} ({}).".format(hex(address), hex(DosHeader)) | |
print "=" * 100 | |
while DosHeader != 0: | |
e_magic = DbgWord(DosHeader + OFFSET_IMAGE_DOS_HEADER_E_MAGIC) | |
# If we can't read the page, it's most likely invalid (not | |
# mapped in). In the kernel most PE images (like ntoskrnl) | |
# are more or less guaranteed to have their PE header in | |
# the NonPagedPool. We skip invalid pages here. | |
if e_magic is not None: | |
if verbose: | |
print "{} --> {}".format(hex(DosHeader), hex(e_magic)) | |
# Do we have an 'MZ'? | |
if e_magic == IMAGE_DOS_SIGNATURE: | |
# Extract the e_lfanew. | |
e_lfanew = DbgDword(DosHeader + OFFSET_IMAGE_DOS_HEADER_E_LFANEW) | |
# Go to the (potential) IMAGE_NT_HEADERS at this location. | |
NtHeaders = DosHeader + e_lfanew | |
# The IMAGE_NT_HEADERS should be on the same | |
# page as the IMAGE_DOS_HEADER. If this is not true, | |
# something's weird and we shouldn't read from this address. | |
if page_align(NtHeaders) == DosHeader: | |
Signature = DbgDword(NtHeaders + OFFSET_IMAGE_NT_HEADERS_SIGNATURE) | |
if verbose: | |
print "\t{} --> {}".format(hex(NtHeaders), hex(Signature)) | |
# Do we have a 'PE00'? | |
if Signature == IMAGE_NT_SIGNATURE: | |
if verbose: | |
print "\t{} Base address located @ {}.".format("^" * 50, hex(DosHeader)) | |
# At this point, it looks like we have both a valid | |
# DOS and NT header. This should be the right base | |
# address. | |
return DosHeader | |
# Try another page. | |
DosHeader -= PAGE_SIZE | |
# If we get to here... someone left this script running way too long. | |
return None | |
########################################################### | |
# Begin scripting logic. | |
########################################################### | |
print "=" * 100 | |
print "Discovers the base address of ntoskrnl when IDA's GDB stub is loaded by leveraging the IDT.\n" | |
print "NOTE: This is only compatible for 64-bit editions of Windows." | |
print "\t\t\t~ http://triplefault.io ~" | |
print "=" * 100 | |
# Ask for the idtr register from the VMware GDB stub. | |
monitor_result = SendDbgCommand("r idtr") | |
# The string is returned in the following format: | |
# idtr base=0xfffff800707c9070 limit=0xfff | |
try: | |
# Try to extract just the numerical base. | |
idt_base = int(splice(monitor_result, "base=", " limit"), 16) | |
except: | |
print "ERROR: Failed to retrieve IDT base from VMware's GDB stub." | |
exit(-1) | |
print "IDT base @ {}.".format(hex(idt_base)) | |
idt_entry = read_idt_entry(idt_base) | |
if idt_entry is None: | |
print "ERROR: Failed to extract and parse KIDTENTRY64." | |
exit(-2) | |
print "_KIDTENTRY64[0] (nt!KiDivideErrorFault) @ {}.".format(hex(idt_entry)) | |
# We have a symbol in the address space of nt!* (unless someone | |
# detoured the IDT entry...). At this point, we walk kernel | |
# memory backwards from the start of this symbol until we | |
# get to a valid PE header. This should be the base address of | |
# ntoskrnl. | |
ntoskrnl_base = find_base_address(idt_entry) | |
if ntoskrnl_base is not None: | |
print "\nThe base address of nt (ntoskrnl) is @ {}.".format(hex(ntoskrnl_base)) | |
else: | |
print "\nERROR: Could not find the base address of ntoskrnl after searching all resident memory. Something clearly went wrong. Additionally, you waited a very long time. Sorry!" | |
exit(-3) |
view raw find_nt_imagebase_x64.py hosted with ❤ by GitHub
Save a copy of the script to your local drive. We are then able to run it at any time by going to "File" and then "Script file..." in the IDA Pro GUI. A sample of the output is listed below:
==================================================================================================== | |
Discovers the base address of ntoskrnl when IDA's GDB stub is loaded by leveraging the IDT. | |
NOTE: This is only compatible for 64-bit editions of Windows. | |
~ http://triplefault.io ~ | |
==================================================================================================== | |
IDT base @ 0xfffff802c4850000L. | |
_KIDTENTRY64[0] (nt!KiDivideErrorFault) @ 0xfffff802c27f4300L. | |
Searching for base address of symbol @ 0xfffff802c27f4300L (0xfffff802c27f4000L). | |
==================================================================================================== | |
0xfffff802c27f4000L --> 0xffff | |
0xfffff802c27f3000L --> 0x4817 | |
0xfffff802c27f2000L --> 0x48ff | |
... | |
0xfffff802c2684000L --> 0xe850 | |
0xfffff802c2683000L --> 0x3 | |
0xfffff802c2682000L --> 0x7f4d | |
0xfffff802c2681000L --> 0x0 | |
0xfffff802c2680000L --> 0x5a4d | |
0xfffff802c2680108L --> 0x4550 | |
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Base address located @ 0xfffff802c2680000L. | |
The base address of nt (ntoskrnl) is @ 0xfffff802c2680000L. |
view raw Script output - IDAPython output.txt hosted with ❤ by GitHub
The important line appears on the bottom; the base address of ntoskrnl is displayed. It checks out with the work we did by hand too.
Loading ntoskrnl at its base address
We mustn't forget the final objective: loading kernel symbols. We're almost at the finish line. Let's tell IDA to load ntoskrnl at the base address our script found.
First, we'll need to grab a copy of ntoskrnl on the VM. Don't use the version on your host as this may not match with what's on the VM. This'll be found in your guest's system directory:
You might need to resume your VM if you're currently active in IDA's GDB debugger by selecting "Debugger" and then "Continue process" (or by hitting F9) from the menu bar.
After you've pulled ntoskrnl from your VM, break into IDA's GDB debugger by selecting "Suspend". Now, we must load it by selecting "File" then "Load file" and finally "PDB file..."
Find where you copied ntoskrnl to on your host and use the address that the script found:
It'll take IDA at least a couple of minutes to fully finish the loading process. You can see IDA's progress in the bottom left corner:
You'll know IDA's finished when the status changes to "AU: idle".
Quick validation
We should make sure that the symbols are loaded correctly. Navigate to "Jump" and then "Jump to address" (or press "G"). Enter PsLoadedModuleList (case sensitive) and hit "OK".
From there, double click the address immediately to the right of the PsLoadedModuleList symbol. This takes you to the first entry in the list.
Each entry in this list is of type _LDR_DATA_TABLE_ENTRY. You might be familiar with this structure from usermode programming. It's also used in the kernel.
We'll need to add the definition of the _LDR_DATA_TABLE_ENTRY to IDA's structures. Luckily, we have symbols loaded and this is a pretty straightforward process.
After the structure was added, you'll see a window similar to this.
Go back to the "Debug View". Impose the _LDR_DATA_TABLE_ENTRY structure on that memory region:
Let's follow the FullName.Buffer field:
And now let's convert this to a readable string:
You should see the characters \SystemRoot\system32\ntoskrnl.exe. We did it!
Final thoughts
Now that symbols are loaded for ntoskrnl, it would be wise to iterate through the PsLoadedModuleList and load symbols for all the other kernel mode components. This can be scripted using IDAPython too, however, it's beyond the scope of this article.
Cheers!
jpg改zip