Malware Analyst’s Cookbook and DVD: Tools and Techniques for Fighting Malicious Code (2011)
Chapter 13. Kernel Debugging
Using a kernel debugger can provide powerful insight into the capabilities of low-level rootkits. Malware could introduce code into the kernel by loading a driver, patching existing drivers on disk, exploiting vulnerabilities, and writing to kernel memory from user mode withZwSystemDebugControl or by mapping the \Device\PhysicalMemory object. Regardless of how malware enters the kernel, if you are incapable of following it, you will quickly become lost, and your analysis will come to an abrupt halt.
This chapter provides an introduction to kernel debugging techniques and shows some practical examples of unpacking and reverse-engineering malicious kernel drivers. However, you can use a kernel debugger for more than just debugging drivers. You’ll commonly need to debug drivers and processes simultanously. For example, malware may have multiple components—a driver that runs in kernel mode and a process that runs in user mode. To fully understand how the components interact, you can use a kernel debugger to “watch” both sides of the conversation.
Remote Kernel Debugging
A typical kernel debugging session involves two separate systems—the target (the system being debugged) and the debugger (the system used to control the target). Figure 14-1 shows the basic idea for this type of setup. You need a separate machine to control the target because code cannot execute in the kernel while it is stopped in a debugger.
Figure 14-1: Remote kernel debugging requires two computers.
To connect the two systems in a remote debugging scenario, you can use a serial cable, USB cable, network connection, or virtual hardware (if you’re using virtual machines). The examples in this chapter are based on using virtual machines to perform your debugging tasks.
Local Kernel Debugging
In a local kernel-debugging scenario, shown in Figure 14-2, the debugger application runs on the same system as the one that is being debugged. This type of setup limits your ability to control the target, and essentially, you can only perform read operations. In other words, you can list processes and drivers, dump kernel memory, and locate kernel symbols and things of that nature, but you cannot set breakpoints, step through code, or change the contents of registers or memory.
Figure 14-2: Local kernel debugging is limited in power.
Software Requirements
The boxes representing the debugging system in Figures 14-1 and 14-2 contain the abbreviation WDK, which stands for Windows Driver Kit. The WDK contains Microsoft’s kernel debuggers, such as KD (a command-line version) and WinDbg (a GUI version). If you never plan to write your own drivers, then you can just install the Debugging Tools for Windows kit, which includes KD and WinDbg, but not the entire development environment. Depending on which package you install, the debugger applications will exist in different locations on your system. If you get them from Debugging Tools for Windows, then the path is probably C:\Program Files\Microsoft\Debugging Tools For Windows. If you get them from the WDK, the default path is C:\WINDDK\<Version>\Debuggers.
Additionally, you should install the symbols for your target operating system. Although you can download symbols from Microsoft at the time of your debugging session, it is always nice to have a local copy just in case network access isn’t available. Symbol files contain the names and addresses of functions, local and global variables, and type information for data structures, so they are critical to your ability to orient yourself in the kernel. The debuggers and symbols are freely available on Microsoft’s website at http://www.microsoft.com/whdc/devtools/default.mspx.
Recipe 14-1: Local Debugging with LiveKd
The LiveKd1 utility by Mark Russinovich lets you run Microsoft’s KD or WinDbg locally on a machine. As previously mentioned, this setup is limited in the amount of control you can exercise with your debugger (read operations only). However, sometimes if you’re just investigating small issues or “poking” around in the kernel, read access is all you need. To get started, follow these steps:
1. Make sure that you have installed the Microsoft debuggers and then download LiveKd from the link in the beginning of this recipe.
2. Extract livekd.exe from the archive and place it in the same directory as the Microsoft debuggers.
3. By default, when you launch livekd.exe, it starts the KD command-line debugger. If you would rather use WinDbg instead, then pass the –w flag to livekd.exe when executing it. You will need to answer a few questions related to setting up symbols, but in most cases, you can accept the defaults.
C:\>cd C:\WINDDK\7600.16385.0\Debuggers
C:\WINDDK\7600.16385.0\Debuggers>livekd.exe
LiveKd v3.14 - Execute kd/windbg on a live system
Sysinternals - www.sysinternals.com
Copyright (C) 2000-2010 Mark Russinovich
Symbols are not configured. Would you like LiveKd to set the
_NT_SYMBOL_PATH directory to reference the Microsoft symbol
server so that symbols can be obtained automatically? (y/n) y
Enter the folder to which symbols download (default is c:\symbols):
Launching C:\WINDDK\7600.16385.0\Debuggers\kd.exe:
Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\WINDOWS\livekd.dmp]
Kernel Complete Dump File: Full address space is available
Comment: 'LiveKD live system view'
Symbol search path is:
srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows XP Kernel Version 2600 (Service Pack 3) Free x86 compatible
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 2600.xpsp_sp3_gdr.090804-1435
Machine Name:
Kernel base = 0x804d7000 PsLoadedModuleList = 0x80554040
Debug session time: Sat Feb 12 22:34:57.897 17420 (GMT-4)
System Uptime: 0 days 1:39:35.562
Loading Kernel Symbols
...............................................................
.............................................................
Loading User Symbols
...........
Loading unloaded module list
..............
kd> type your commands here...
4. You can now skip to Recipe 14-5 to begin using the debugger, but keep in mind that you can only execute read/view operations because you’re debugging the kernel locally.
Note You can actually use KD and WinDbg on a system without LiveKd. To do this, pass the –kl parameters (for kernel, local) to kd.exe or windbg.exe when starting them. In this case, however, you will need to set up symbols and the debugging environment on your own.
1 http://technet.microsoft.com/en-us/sysinternals/bb897415.aspx
Recipe 14-2: Enabling the Kernel’s Debug Boot Switch
You can remotely debug the kernel of any Windows system without installing special software onto the target. However, you do need to let the target kernel know that it should accept and respond to debugger connections. To do this, you must enable the /debug boot switch as described in this recipe.
Windows XP and Server 2003 Targets
Microsoft’s recommended way to make the required changes is to use bootcfg.exe.2 This tool validates your syntax for boot options and rejects invalid entries. You can also modify C:\boot.ini directly, but if you make a careless mistake when manually editing boot.ini, then you may not be able to boot your system again. To use bootcfg.exe, follow these steps:
1. List the existing configuration like this:
C:\>bootcfg
Boot Loader Settings
--------------------
timeout: 30
default: multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
Boot Entries
------------
Boot entry ID: 1
Friendly Name: "Microsoft Windows XP Professional"
Path: multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
OS Load Options: /noexecute=optin /fastdetect
2. Create a copy of the boot entry (ID 1 in this case) and give it a meaningful name. Verify your changes by typing bootcfg again, without any arguments.
C:\>bootcfg /Copy /D "XP Professional with Debug" /ID 1
SUCCESS: Made a copy of the boot entry "1".
C:\>bootcfg
[...]
Boot entry ID: 2
Friendly Name: "Microsoft Windows XP Professional - Debug"
Path: multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
OS Load Options: /noexecute=optin /fastdetect
3. Enable the debug switch on the new boot entry (ID 2) and configure the port and baud. This particular setup uses the COM1 serial port, which you need to remember when adding a virtual serial device to your virtual machines.
C:\>bootcfg /Debug ON /ID 2 /PORT COM1 /BAUD 115200
SUCCESS: Changed the switches in OS entry "2" in the BOOT.INI.
4. Verify your changes by typing bootcfg again, without any arguments.
C:\>bootcfg
[...]
Boot entry ID: 2
Friendly Name: "Microsoft Windows XP Professional - Debug"
Path: multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
OS Load Options: /noexecute=optin /fastdetect /debug /debugport=com1
/baudrate=115200
Windows Vista and Windows 7 Targets
Starting with Vista, Windows no longer uses boot.ini for boot settings. To enable the debug switch on these systems, you can use bcdedit.exe3 instead as shown in the following steps:
1. Launch a command shell with administrator privileges and type bcdedit to print the current boot loader configuration.
C:\>bcdedit
Windows Boot Manager
--------------------
identifier {bootmgr}
device partition=\Device\HarddiskVolume1
description Windows Boot Manager
locale en-US
inherit {globalsettings}
default {current}
resumeobject {d121a616-887e-11de-be3f-9b9b7d346734}
displayorder {current}
toolsdisplayorder {memdiag}
timeout 30
Windows Boot Loader
-------------------
identifier {current}
device partition=C:
path \Windows\system32\winload.exe
description Windows 7
locale en-US
inherit {bootloadersettings}
recoverysequence {d121a618-887e-11de-be3f-9b9b7d346734}
recoveryenabled Yes
osdevice partition=C:
systemroot \Windows
resumeobject {d121a616-887e-11de-be3f-9b9b7d346734}
nx OptIn
2. Create a copy of the configuration with identifier {current}, like this:
C:\>bcdedit /copy {current} /d "Windows 7 with Debug"
The entry was successfully copied to
{d121a61a-887e-11de-be3f-9b9b7d346734}.
3. Enable the debug boot switch for the newly created identifier.
C:\>bcdedit /debug {d121a61a-887e-11de-be3f-9b9b7d346734} ON
The operation completed successfully.
4. Type bcdedit again, without any parameters, to check if the system accepted your changes.
C:\>bcdedit
Windows Boot Loader
-------------------
identifier {d121a61a-887e-11de-be3f-9b9b7d346734}
device partition=C:
path \Windows\system32\winload.exe
description Windows 7 with Debug
locale en-US
inherit {bootloadersettings}
recoverysequence {d121a618-887e-11de-be3f-9b9b7d346734}
recoveryenabled Yes
osdevice partition=C:
systemroot \Windows
resumeobject {d121a616-887e-11de-be3f-9b9b7d346734}
nx OptIn
debug Yes
Booting into Debug Mode
At the next power-on, select the debugger-enabled operating system. Everything will proceed as normal until you connect to the system with a debugger. Figure 14-3 shows what you should see, depending on what you named your entries.
Figure 14-3: Booting into debugger-enabled mode
2 http://support.microsoft.com/kb/317521
3 http://www.microsoft.com/whdc/driver/tips/Debug_Vista.mspx
Recipe 14-3: Debug a VMware Workstation Guest (on Windows)
This recipe assumes that you run VMware Workstation on a Windows host operating system (the debugger), and you want to explore the kernel of one of your VMware guests (the target). Here are the steps to getting your machines configured properly:
1. On your Windows host, install the Microsoft debuggers and the symbol package for your target’s operating system.
2. Enable the debug boot switch on your target, as described in Recipe 14-2. After the changes, shut down the target.
3. With the target powered down, you can add a new virtual serial device. Follow these steps:
a. Click Edit virtual machine configuration.
b. On the Hardware tab, click Add.
c. Select Serial Port and click Next.
d. Select Output to named pipe and click Next.
e. Enter a name for the pipe, or accept the default of \\.\pipe\com_1.
f. Select This end is the server.
g. Select The other end is an application.
h. Place a check in the Connect at power on box.
i. Place a check in the Yield on CPU poll box.
j. Verify your settings with Figure 14-4.
Figure 14-4: Adding a virtual serial port in VMware
4. Power on the target, and choose the debugger-enabled operating system, as described in Recipe 14-2.
5. Launch WinDbg from your Windows host operating system using the following syntax:
C:\WinDDK\7600~\Debuggers> windbg –k com:pipe,port=\\.\pipe\com_1
6. Once you see the WinDbg application, press Ctrl+Break, or click Debug ⇒ Break on the menu. You should see the welcome screen, as shown in Figure 14-5.
Figure 14-5: The debugger’s welcome screen
You can now skip to Recipe 14-5 to begin using the debugger.
Recipe 14-4: Debug a Parallels Guest (on Mac OS X)
Debugging between two virtual machines requires a few extra steps compared with Recipe 14-3. In this recipe, you’ll learn how to set up a remote debugging connection between guests using Parallels on Mac OS X. To start, you need two virtual machines running Windows.
1. Dedicate one of your virtual machines as the debugger and one as the target. You might want to rename the target “Windows—Debug Target” or something similar so you don’t get them mixed up.
2. On the debugging system, install the Microsoft debuggers and symbols for the target’s operating system.
3. Enable the debug boot switch on your target, as described in Recipe 14-2.
4. Power down both virtual machines.
5. Add a serial device to the target by following these steps:
a. Click Configure to bring up the virtual machine’s configuration.
b. Click the + icon to add hardware.
c. Choose Serial Port and click Continue.
d. Choose Socket and click Continue.
e. Enter a name for the Socket (/tmp/com_1 by default, which is fine).
f. Make sure the Mode is Server and click Add Device.
6. Add a serial device to the debugging system. To do this, follow the same steps as you did for the target, but for step f, make sure the Mode is Client and click Add Device. Verify that your target’s configuration appears like Figure 14-6 and that your debugging system’s configuration appears similar, but with Client selected instead of Server.
Figure 14-6: Adding a virtual serial port in Parallels
7. Power on the target, and choose the debugger-enabled operating system, as described in Recipe 14-1.
8. Launch WinDbg from your debugging system using the following syntax:
C:\WinDDK\7600.16385.0\Debuggers> windbg –k
9. Once you see the WinDbg application, press Ctrl+Break, or click Debug ⇒ Break on the menu. You should see the welcome screen, as shown in Figure 14-5.
You can now continue to Recipe 14-5 to begin using the debugger.
Recipe 14-5: Introduction to WinDbg Commands And Controls
This recipe introduces you to some of the common WinDbg commands and things you need to know before beginning a debugging session.
Configuring Symbols
You should always configure symbols at the start of your debugging session. If you installed the symbol packages for your target’s operating system onto your debugging system, then you’ll need to know the path to where you put them (default is C:\symbols or C:\windows\symbols). Then issue the following command:
kd> .sympath c:\windows\symbols
Otherwise, you can download symbols as needed by pointing WinDbg to Microsoft’s online symbol server.
kd> .sympath "SRV* http://msdl.microsoft.com/download/symbols"
When you’re done, reload the symbols so WinDbg can access them.
kd> .reload
Creating Log Files
You can create log files of your commands and the corresponding output. Log files are useful because a single command can generate hundreds of lines of output. Additionally, months from now, you might not always remember exactly what you typed. The following commands show you how to enable logging for your debugging session:
kd> .logopen c:\test.log
Opened log file 'c:\test.log'
[... type your commands here ...]
kd> .logclose
Closing open log file c:\test.log
Locating Functions and Variables
You can use the x (examine symbols) command to locate symbols, such as functions exported by kernel drivers, functions exported by user-mode DLLs, and global variables. The syntax is x [module]![symbol] and you can use asterisks as wildcards. The following example searches the nt module (the name of the kernel executive) for functions related to mutexes:
kd> x nt!*mutex*
804d7690 nt!_imp_ExReleaseFastMutex = <no type information>
8055f900 nt!MmSectionBasedMutex = <no type information>
8055a160 nt!KiGenericCallDpcMutex = <no type information>
8055f920 nt!MmSectionCommitMutex = <no type information>
[...]
The following command looks in any loaded kernel module for functions related to notification events:
kd> x *!*notify*
8058a950 nt!NtNotifyChangeDirectoryFile = <no type information>
80612b0a nt!FsRtlNotifyCompletion = <no type information>
80561500 nt!PspCreateProcessNotifyRoutineCount = <no type information>
80554a04 nt!SepRmNotifyMutex = <no type information>
8068eb38 nt!PsImageNotifyEnabled = <no type information>
[...]
b2f04dc7 tcpip!AddrChangeNotifyRequest = <no type information>
b2f2eef6 tcpip!TcpSynAttackNotifyCcb = <no type information>
b2f08eb3 tcpip!IPNotifyClientsIPEvent = <no type information>
[...]
bf8c1ad2 win32k!NtUserNotifyProcessCreate = <no type information>
bf8bfc08 win32k!xxxUserNotifyProcessCreate = <no type information>
bf8acfbf win32k!DeviceCDROMNotify = <no type information>
You can also perform reverse lookups on an address to see if any symbols exist at the address or if any symbols exist at nearby addresses. For example, in the output that follows, 8062d880 is an address between PsSetCreateProcessNotifyRoutine and PsSetCreateThreadNotifyRoutine in the nt module:
kd> ln 8062d880
(8062d7b6) nt!PsSetCreateProcessNotifyRoutine+0xca
(8062d88d) nt!PsSetCreateThreadNotifyRoutine
Printing Objects/Structures
You can use the dt (display type) command to display type information for data structures and kernel objects. If you know the address in memory where a given structure or object exists, then you can have WinDbg parse the structure’s members accordingly. If you pass the -r switch, then dt will recursively parse any nested structures. The following commands show the format of a PEB structure and then apply it to a particular process’s PEB.
kd> dt _PEB
ntdll!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 SpareBool : UChar
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void
[...]
kd> !process 0 0
PROCESS 820ddda0 SessionId: 0 Cid: 0e30 Peb: 7ffde000
ParentCid: 02a8 DirBase: 1710d000 ObjectTable: e1b809a8
HandleCount: 16.
Image: logon.scr
[...]
kd> .process /r /p 820ddda0
kd> dt _PEB 7ffde000
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 SpareBool : 0 ''
+0x004 Mutant : 0xffffffff
+0x008 ImageBaseAddress : 0x01000000
[...]
Here are a few structures and data types that you should become familiar with before an in-depth kernel debugging session. You will frequently run into functions that read or write these data types, so it’s important to get familiar with them ahead of time. To view them in WinDbg, use the dt command followed by their name, as shown in Table 14-1.
Table 14-1: Common dt Commands
Command |
Description |
_EPROCESS |
The executive process block |
_ETHREAD |
The executive thread block |
_PEB |
The process environment block |
_TEB |
The thread environment block |
_UNICODE_STRING |
Structure for wide character strings |
_DRIVER_OBJECT |
Structure for drivers |
_LIST_ENTRY |
The linking component in doubly linked lists |
_LARGE_INTEGER |
Structure for 64-bit numbers |
_CLIENT_ID |
Structure for process ID and thread ID pairs |
_POOL_HEADER |
Structure that describes kernel pool allocations |
_OBJECT_HEADER |
Structure that describes kernel objects |
_FILE_OBJECT |
Structure for file objects |
_CONTEXT |
Structure that describes a thread’s state and registers |
Formatting Data
You can print the data you find in memory using various formats. For example, the db command displays data as hex bytes and ASCII characters, the dd command displays data as double-word values, and the da/du commands display ASCII and Unicode strings, respectively. Here is an example dump using the address of the PEB from the preceding output:
kd> dd 7ffde000
7ffde000 00000000 ffffffff 01000000 00181e90
7ffde010 00020000 00000000 00080000 7c980600
7ffde020 7c901000 7c9010e0 00000001 7e412970
7ffde030 00000000 00000000 00000000 00000000
7ffde040 7c9805c0 000003ff 00000000 7f6f0000
7ffde050 7f6f0000 7f6f0688 7ffb0000 7ffc1000
7ffde060 7ffd2000 00000001 00000000 00000000
7ffde070 079b8000 ffffe86d 00100000 00002000
Assuming you only want to print the ImageBase value of the PEB, you can add the appropriate offset to the PEB base address and use the L parameter to control how many elements to display:
kd> dd 7ffde000+8 L1
7ffde008 01000000
The following example shows you how to display a hex + ASCII dump for a string. You can see that the string contains a \x00 byte between each character, which indicates it is a Unicode string.
kd> x nt!*sz*
805cc7cc nt!szDaylightBias = <no type information>
805cc7b0 nt!szDaylightName = <no type information>
kd> db nt!szDaylightBias
805cc7cc 4400610079006c00-6900670068007400 D.a.y.l.i.g.h.t.
805cc7dc 4200690061007300-000000002a535953 B.i.a.s.....*SYS
805cc7ec 54454d2a00000000-00000000e7030000 TEM*............
kd> du nt!szDaylightBias
805cc7cc "DaylightBias"
Printing Registers
You can print all registers at once with the r (registers) command, or specify an individual register such as r eax.
kd> r
eax=00000001 ebx=001f3475 ecx=80551fac edx=000003f8 esi=0000004a
edi=65f73b22
eip=804e3592 esp=f861f84c ebp=f861f85c iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000202
kd> r eax
eax=00000001
The following command shows the contents of the zero flag:
kd> r zf
zf=0
You can modify the contents of registers by simply assigning them a new value, like this:
kd> r eax=2
kd> r eax
eax=00000002
Searching Memory
You can search for a pattern of bytes in kernel or user-mode memory by using the s (search memory) command. The following example shows you how to locate potentially embedded executables by searching for the MZ header within a suspicious kernel driver.
kd> lm n
start end module name
804d7000 806ed700 nt ntoskrnl.exe
806ee000 8070e300 hal halaacpi.dll
b1ff1000 b2016880 windev_11a2_5d2d windev-11a2-5d2d.sys
b2180000 b21c0a80 HTTP HTTP.sys
b25a9000 b25fa880 srv srv.sys
kd> s -d b1ff1000 Lb2016880-b1ff1000 0x00905a4d
b1ff1000 00905a4d 00000003 00000004 0000ffff MZ..............
b1ff2340 00905a4d 00000003 00000004 0000ffff MZ..............
The first command determined the start and end address of a kernel driver named windev-11a2-5d2d.sys. The second command used the search function to find a double-word (-d) sized value of 0x00905a4d (MZ\x90\x00) anywhere in the driver’s memory. It found one occurrence at b1ff1000, which is the base of the driver—an expected result. It found a second occurrence at b1ff2340, which is not expected—it indicates the driver has another PE file embedded in its body. For more information about finding executable images and extracting them with WinDbg, see Cody Pierce’s MindshaRE4 blog entry.
You can search for ASCII strings with the -a flag or Unicode strings with the -u flag. In these cases, the strings in memory do not have to be NULL-terminated to match. Here’s an example of searching for the term “Windows” anywhere in the suspicious driver:
kd> s -a b1ff1000 Lb2016880-b1ff1000 "Windows"
b200ad9f 57696e646f77735c-495453746f726167 Windows\ITStorag
b200e278 57696e646f77734e-5420332e35310000 WindowsNT 3.51..
b200e288 57696e646f777320-3935000057696e64 Windows 95..Wind
b200e294 57696e646f777320-4e5420342e300000 Windows NT 4.0..
b200e2a4 57696e646f777320-3938000057696e64 Windows 98..Wind
b200e2b0 57696e646f777320-4d65000057696e25 Windows Me..Win%
b200e2d0 57696e646f777320-3230303000000000 Windows 2000....
b200e2e0 57696e646f777320-5850000057696e64 Windows XP..Wind
b200e2ec 57696e646f777320-3230303300000000 Windows 2003....
b200e2fc 57696e646f777320-5669737461000000 Windows Vista...
You can also extract ASCII or Unicode strings by using the s-sa or s-su commands, respectively. The following command lists all ASCII strings in the driver that are at least six characters long. The value in brackets specifies the length—it is a lowercase L followed by the number 6.
kd> s -[l6]sa b1ff1000 Lb2016880-b1ff1000
b1ff104d "!This program cannot be run in D"
b1ff106d "OS mode."
b1ff135f "'.rdata"
b1ff1387 "@.data"
b1ff13d8 ".reloc"
b1fF1414 "EventListener is EXITED, %d"
b1ff238d "!This program cannot be run in D"
b200adc8 "config"
b200add0 "\windev-peers.ini"
b200ade4 "[blacklist]"
b200e0e4 "contract@"
b200e0f8 "anyone@"
b200e100 "update"
b200e110 "f-secur"
b200e118 "rating@"
b200e120 "@microsoft"
b200e620 "Content-Type: application/x-www-"
b200e640 "form-urlencoded"
b200e814 "FORMAT"
b200e81c "COLLECTION"
[...]
Note If you plan to repeatedly search memory for the same terms, or if your WinDbg search is too slow or malware prevents your debugger from attaching, then you might be better off dumping memory and scanning it with a Volatility plug-in (see Recipe 16-6).
Controlling the Debugger
Table 14-2 shows commands that can assist you in controlling the execution of a program or kernel driver.
Table 14-2: Commands that Control Program Execution
Command |
Description |
g [breakaddress] |
Go. Starts executing a current process or thread until the program ends, the optional [breakaddress] instruction is reached, or another event causes execution to stop. |
p [count] |
Step. Executes [count] instructions (or one instruction if [count] is not specified). If subroutines are encountered, this command treats the call as a single instruction and essentially steps over them. |
pa <stopaddress> |
Step to address |
pt |
Step to next return |
t [count] |
Trace. Executes [count] instructions (or one instruction if [count] is not specified). If subroutines are encountered, this command traces each instruction in the subroutine. |
ta <stopaddress> |
Trace to address |
tt |
Trace to next return |
u [address] |
Unassemble instructions at address (or starting at EIP if no address is specified) |
uf [address] |
Unassemble all instructions in a given function (uf shows a disassembly of the current function where EIP points) |
bp <location>, bu <location>, bm <location> |
Set a software breakpoint. The location parameter can be an absolute address (0x400020), an address relative to a register (eip+800), or a symbol (nt!ZwClose). |
bl |
List breakpoints |
bc [number] |
Clear a breakpoint |
For a more comprehensive list of commands and their arguments, see one of the following resources:
· WinDbg From A to Z5
· WinDbg Thematically Grouped Command Sheet6
· The debugger.chm file distributed with Microsoft’s debuggers or Windows Driver Kit
4 http://dvlabs.tippingpoint.com/blog/2008/11/06/mindshare-finding-executable-images-in-windbg
5 http://windbg.info/doc/2-windbg-a-z.html
6 http://windbg.info/doc/1-common-cmds.html
Recipe 14-6: Exploring Processes and Process Contexts
As previously mentioned, you’ll rarely use a kernel debugger to only debug kernel drivers. In most cases, you’ll be switching back and forth between drivers and processes to understand how components in user mode interact with components in kernel mode. This recipe shows some techniques for investigating processes.
Listing Active Processes
You can use the !process command to print information about active processes. As the first parameter, you can specify the address of an EPROCESS structure to print a single process, or zero to print all processes. The second parameter indicates the level of detail you want about the process. The following command prints the smallest amount of detail about all processes:
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 823c8830 SessionId: none Cid: 0004 Peb: 00000000
ParentCid: 0000
DirBase: 00039000 ObjectTable: e1000cf8 HandleCount: 442.
Image: System
PROCESS 823823e0 SessionId: none Cid: 0260 Peb: 7ffde000
ParentCid: 0004
DirBase: 0a85d000 ObjectTable: e100d098 HandleCount: 19.
Image: smss.exe
PROCESS 8222b1b0 SessionId: 0 Cid: 0290 Peb: 7ffde000
ParentCid: 0260
DirBase: 0c973000 ObjectTable: e15c5af0 HandleCount: 375.
Image: csrss.exe
[...]
In the output, you can see the following fields:
· Cid: The process ID
· Peb: The address of the Process Environment Block
· ParentCid: The process ID of the process’s parent
· DirBase: The directory table (used for translation between virtual and physical addresses)
· ObjectTable: The handle table (see upcoming section on listing handles)
If you wanted to get the extended details about the csrss.exe process, you could specify the address of its EPROCESS block and increase the level of information like this:
kd> !process 8222b1b0 1
PROCESS 8222b1b0 SessionId: 0 Cid: 0290 Peb: 7ffde000
ParentCid: 0260
DirBase: 0c973000 ObjectTable: e15c5af0 HandleCount: 375.
Image: csrss.exe
VadRoot 820d5940 Vads 109 Clone 0 Private 293. Modified 959.
Locked 0.
DeviceMap e1004470
Token e14c9478
ElapsedTime 09:10:13.437
UserTime 00:00:00.265
KernelTime 00:00:00.718
[...]
Because the kernel organizes process objects in a linked list, you can create your own version of !process using the generic !list command. For example, let’s say you want to print the name and process ID for each process on the system. First, you’ll need to determine the offsets for the linked list, process ID, and file name fields in the EPROCESS block:
kd> dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER
+0x078 ExitTime : _LARGE_INTEGER
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : Ptr32 Void
+0x088 ActiveProcessLinks : _LIST_ENTRY
[...]
+0x174 ImageFileName : [16] UChar
Once you know the offsets, you can use them in a command like this:
kd> !list "-t ntdll!_LIST_ENTRY.Flink -x \"db /c 8 @$extret-88+174 L16;
dd @$extret-88+84 L1\" nt!PsActiveProcessHead"
823c89a4 53 79 73 74 65 6d 00 00 System.. ; ImageFileName
823c89ac 00 00 00 00 00 00 00 00 ........
823c89b4 00 00 00 00 00 00 ......
823c88b4 00000004 ; UniqueProcessId
82382554 73 6d 73 73 2e 65 78 65 smss.exe ; ImageFileName
8238255c 00 00 00 00 00 00 00 00 ........
82382564 00 00 00 00 00 00 ...... ; UniqueProcessId
82382464 00000260
8222b324 63 73 72 73 73 2e 65 78 csrss.ex ; ImageFileName
8222b32c 65 00 00 00 00 00 00 00 e.......
8222b334 00 00 00 00 00 00 ......
8222b234 00000290 ; UniqueProcessId
[...]
The parameters for !list tell the command to start walking a linked list starting at nt!PsActiveProcessHead (a symbol in the nt module that points to the start of the process list). The command will iterate until it wraps back around to the beginning of the list or when it reaches a NULL entry. We have also indicated that it should use db to print the process name and dd to print the process ID. The @$extret variable contains the address of the list entry for each member of the list. Because the list entry starts at offset 88 within the EPROCESS block, you have to subtract 88 from @$extret to find the EPROCESS base. Then, to find the process ID and name fields, you add 84 and 174, respectively.
Switching Process Contexts
As you may know, each process has a unique “view” of user mode memory. Therefore, commands like dd 401000 are ambiguous, and you must first switch into the context of the process whose memory you want to view. Otherwise, you’ll see the data at 401000 (or just the question mark (?) characters if the address isn’t valid) in a different process than you expect. For example, consider the following commands, which print the same address in different process contexts:
kd> .process /r /p 82216c08
Implicit process is now 82216c08
.cache forcedecodeuser done
kd> dd 401000 L4
00401000 77dd7cc9 77dd7cb8 77dd7305 77dd819e
kd> .process /r /p 820ddda0
Implicit process is now 820ddda0
.cache forcedecodeuser done
kd> dd 401000 L4
00401000 ???????? ???????? ???????? ????????
As you can see, 401000 is valid in the context of one process, but not the other.
Listing Loaded DLLs
Once you switch to the correct process context, you can list the loaded DLLs using the !peb or !dlls commands. Because the list of loaded DLLs exists in the PEB, either command will work, but they show slightly different information. If you want to enumerate DLLs and then find a particular exported function, you could do something like this:
kd> !process 0 0
[...]
PROCESS 820eada0 SessionId: 0 Cid: 02e0 Peb: 7ffde000
ParentCid: 02a8
DirBase: 0d270000 ObjectTable: e15e20d0 HandleCount: 421.
Image: lsass.exe
kd> .process /r /p 820eada0
Implicit process is now 820eada0
.cache forcedecodeuser done
kd> !peb
PEB at 7ffde000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: No
ImageBaseAddress: 01000000
Ldr 00191e90
Ldr.Initialized: Yes
Ldr.InInitializationOrderModuleList: 00191f28 . 00194350
Ldr.InLoadOrderModuleList: 00191ec0 . 00194340
Ldr.InMemoryOrderModuleList: 00191ec8 . 00194348
Base TimeStamp Module
1000000 48025186 Apr 13 2008 C:\WINDOWS\system32\lsass.exe
7c900000 49901d48 Feb 09 2009 C:\WINDOWS\system32\ntdll.dll
7c800000 49c4f482 Mar 21 2009 C:\WINDOWS\system32\kernel32.dll
77dd0000 49901d48 Feb 09 2009 C:\WINDOWS\system32\ADVAPI32.dll
77e70000 49e5f46d Apr 15 2009 C:\WINDOWS\system32\RPCRT4.dll
77fe0000 4988a20b Feb 03 2009 C:\WINDOWS\system32\Secur32.dll
75730000 49901d48 Feb 09 2009 C:\WINDOWS\system32\LSASRV.dll
[...]
kd> x lsasrv!*crypt*
757bcb33 LSASRV!LsaICryptProtectData (<no parameter info>)
757bcc91 LSASRV!LsaICryptUnprotectData (<no parameter info>)
The commands locate the address, in the memory of lsass.exe, for any functions in LSASRV.dll that contain the term “crypt.”
Viewing Process Memory Map
Virtual Address Descriptors (VAD) contain information about allocated memory segments in a process. As Chapter 16 discusses in greater detail, the VAD can help you locate hidden or injected code. To find a process’s VadRoot, use the !process command. Then pass the VadRoot value to !vad, like this:
kd> !process 823823e0 1
PROCESS 823823e0 SessionId: none Cid: 0260 Peb: 7ffde000
ParentCid: 0004
DirBase: 0a85d000 ObjectTable: e100d098 HandleCount: 19.
Image: smss.exe
VadRoot 8220e590 Vads 16 Clone 0 Private 29. Modified 9. Locked 0.
[...]
kd> !vad 8220e590
VAD level start end commit
822eb210 ( 1) 0 ff 0 Private READWRITE
822ec270 ( 2) 100 100 1 Private READWRITE
822fbd18 ( 3) 110 110 1 Private READWRITE
822feae0 ( 4) 120 15f 4 Private READWRITE
822ec0a8 ( 5) 160 25f 6 Private READWRITE
823008e8 ( 6) 260 26f 6 Private READWRITE
82302b58 ( 7) 270 2af 4 Private READWRITE
8237b038 ( 8) 2b0 2ef 4 Private READWRITE
822fb590 ( 9) 2f0 2f0 1 Private READWRITE
8220e590 ( 0) 48580 4858e 2 Mapped Exe EXECUTE_WRITECOPY
8220da58 ( 1) 7c900 7c9b1 5 Mapped Exe EXECUTE_WRITECOPY
822c0a18 ( 2) 7ffb0 7ffd3 0 Mapped READONLY
8229c008 ( 6) 7ffdb 7ffdb 1 Private READWRITE
8229d990 ( 5) 7ffdc 7ffdc 1 Private READWRITE
822b9838 ( 4) 7ffdd 7ffdd 1 Private READWRITE
822b7aa8 ( 3) 7ffde 7ffde 1 Private READWRITE
Total VADs: 16 average level: 5 maximum depth: 9
To calculate the virtual address for each VAD node, you need to multiply the start and end values by 0x1000. Thus, the VAD node at 8220da58 describes the memory at 7c900000–7c9b1000 inside the smss.exe process. According to the output, this memory contains a mapped executable, but it doesn’t show exactly which executable. In that case, you can leverage the lm command (vt is for verbose mode with timestamps) and determine that ntdll.dll exists in that space.
kd> lm vt a 7c900000
start end module name
7c900000 7c9b2000 ntdll
Loaded symbol image file: ntdll.dll
Mapped memory image file:
c:\windows\symbols\ntdll.dll\49901D48b2000\ntdll.dll
Image path: C:\WINDOWS\system32\ntdll.dll
Image name: ntdll.dll
Timestamp: Mon Feb 09 07:10:48 2009 (49901D48)
CheckSum: 000BC674
ImageSize: 000B2000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Viewing Process Handles
You can list information about a process’s open handles using the !handle command. The first argument to !handle is the handle value (or zero to list all handles) and the second argument is the level of information requested (zero displays the least information and 0xf displays the most information). The following command lists the least information for all handles in the current process context:
kd> !handle 0 0
processor number 0, process 823823e0
PROCESS 823823e0 SessionId: none Cid: 0260 Peb: 7ffde000
ParentCid: 0004
DirBase: 0a85d000 ObjectTable: e100d098 HandleCount: 19.
Image: smss.exe
Handle table at e13e9000 with 19 Entries in use
0004: Object: e1005448 GrantedAccess: 000f0003
0008: Object: 822e0d68 GrantedAccess: 00100020 (Inherit)
000c: Object: e17b73c0 GrantedAccess: 001f0001
0010: Object: e161ee80 GrantedAccess: 001f0001
0014: Object: e10044d0 GrantedAccess: 000f000f
0018: Object: e1645030 GrantedAccess: 000f000f
001c: Object: 822396b8 GrantedAccess: 00100001
0020: Object: e163d148 GrantedAccess: 000f0001
0024: Object: e17ac030 GrantedAccess: 000f000f
0028: Object: 8222dbe8 GrantedAccess: 001f0003
002c: Object: 82285480 GrantedAccess: 001f0003
0030: Object: 8222b1b0 GrantedAccess: 001f0fff
0034: Object: 8222b1b0 GrantedAccess: 00000400
0038: Object: e16095f0 GrantedAccess: 001f0001
003c: Object: e1805298 GrantedAccess: 001f0001
0040: Object: e1609820 GrantedAccess: 001f0001
0044: Object: e1fb6eb0 GrantedAccess: 001f0001
0048: Object: 82136800 GrantedAccess: 001f0fff
004c: Object: 821d2a70 GrantedAccess: 00000400
Each line in the output shows the handle value, the object’s address, and an access mask that describes the level of access granted for the object. As with any handle, the most important facts you’ll want to know are the object type (file object, mutex object, and so on) and the object name, if there is one. To find this out, specify a handle value this time when calling !handle and increase the level of information to the maximum:
kd> !handle 48 f
0048: Object: 82136800 GrantedAccess: 001f0fff Entry: e13e9090
Object: 82136800 Type: (823c8e70) Process
ObjectHeader: 821367e8 (old version)
HandleCount: 15 PointerCount: 336
Now you can tell that handle 48 is for a process object. This means you can find an EPROCESS object at 82136800. Therefore, you should be able to identify the process with the following command:
kd> !process 82136800 0
PROCESS 82136800 SessionId: 0 Cid: 02a8 Peb: 7ffdb000
ParentCid: 0260
DirBase: 0cf38000 ObjectTable: e15a1570 HandleCount: 577.
Image: winlogon.exe
At this point, you’ve identified that handle 48 in smss.exe is a handle to the winlogon.exe process. As shown in Figure 14-7, the handle value and interpretation is the same value you would see using a tool such as Process Hacker to examine smss.exe.
Figure 14-7: Process Hacker confirms that handle 48 is for a process named winlogon.exe.
Recipe 14-7: Exploring Kernel Memory
This recipe introduces you to some of the WinDbg commands that you’ll likely execute when exploring kernel drivers and kernel memory.
Listing Loaded Modules
You can use the lm (list modules) command to list loaded modules, along with their start and end addresses in kernel memory and the file name on disk. To receive more information about the PE header values for the loaded module, you can pass the module’s base address to !dh or !lmi.
kd> lm f
start end module name
804d7000 806ed700 nt ntoskrnl.exe
806ee000 8070e300 hal halaacpi.dll
b22c8000 b2308a80 HTTP \SystemRoot\System32\Drivers\HTTP.sys
b2651000 b26a2880 srv \SystemRoot\system32\DRIVERS\srv.sys
[...]
kd> !dh b22c8000
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (i386)
7 number of sections
480256BC time date stamp Sun Apr 13 14:53:48 2008
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
10E characteristics
Executable
Line numbers stripped
Symbols stripped
32 bit word machine
OPTIONAL HEADER VALUES
10B magic #
7.10 linker version
34500 size of code
C280 size of initialized data
0 size of uninitialized data
3B757 address of entry point
[...]
Viewing Pool Usage
When drivers allocate memory in the kernel, many of them use the ExAllocatePoolWithTag API function. The drivers can specify the size of the memory block, the type of memory (paged, non-paged, and so on), and a 4-byte ASCII tag to be associated with the memory. Here is a description of the function’s parameters:
PVOID ExAllocatePoolWithTag(
IN POOL_TYPE PoolType,
IN SIZE_T NumberOfBytes,
IN ULONG Tag
);
Parameters:
PoolType
The type of pool memory to allocate (PagedPool, NonPagedPool, etc)
NumberOfBytes
The number of bytes to allocate.
Tag
The 4-byte ASCII tag to be associated with the allocated memory.
Microsoft allows driver-defined tags to be associated with memory blocks to simplify debugging tasks, such as finding the source of a memory leak (for more information, see Who’s Using the Pool?7). It’s easy to find a memory-hogging application in user mode because monitoring programs show per-process memory usage. On the other hand, kernel drivers share the same memory pools, so it’s difficult to isolate the one driver that repeatedly fails to free memory.
Before you can benefit from pool tagging, you have to enable the tagging feature in the kernel (which takes effect after the next reboot). Then you can print statistics on how much memory is being tied up with each tag, and then hunt down which driver allocates memory with the suspect tags.
You can enable pool tagging on a target system in several ways:
· Use the global flags editor (glags.exe), which is distributed with the WDK.
· Use the !gflag WinDbg extension, like this:
kd> !gflag + ptg
Current NtGlobalFlag contents: 0x00000400
ptg - Enable pool tagging
· Use the Pooltag.exe program, which is distributed with the Windows Driver Kit (see Figure 14-8).
Figure 14-8: PoolTag enables pool tagging in the kernel.
Regardless of how you choose to enable pool tagging, once it’s done, you can print statistics about the system’s pool usage. Figure 14-9 shows the Pooltag.exe application sorted by bytes used (highest to lowest). You can see that memory associated with the tag Gh05 is taking up the most memory.
Figure 14-9: Pools tagged with Gh05 are taking up the most memory.
You can print similar statistics using the !poolused extension for WinDbg. Here is an example of how to print the pools in alphabetical order by tag, including a description of the tag’s purpose and source driver. The debugger reads descriptions from a plain text file named pooltag.txt with the format <pooltag> - <driver> - <description> so you can add to the known list of pool tags on your own.
kd> !poolused
Sorting by Tag
Pool Used:
NonPaged Paged
Tag Allocs Used Allocs Used
8042 4 3944 0 0 PS/2 kb and mouse,
Binary: i8042prt.sys
AcdN 2 1072 0 0 TDI AcdObjectInfoG
AcpA 3 192 1 504 ACPI arbiter data,
Binary: acpi.sys
AcpB 0 0 4 832 ACPI buffer data,
Binary: acpi.sys
[...]
Gh04 0 0 22 8368 GDITAG_HMGR_SPRITE_TYPE,
Binary: win32k.sys
Gh05 0 0 332 3488008 GDITAG_HMGR_SPRITE_TYPE,
Binary: win32k.sys
Gh08 0 0 8 8016 GDITAG_HMGR_SPRITE_TYPE,
Binary: win32k.sys
Gh09 0 0 1 616 GDITAG_HMGR_SPRITE_TYPE,
Binary: win32k.sys
Gh0< 0 0 105 3360 GDITAG_HMGR_SPRITE_TYPE,
Binary: win32k.sys
[...]
Proc 27 17280 0 0 Process objects,
Binary: nt!ps
PsQb 9 648 0 0 Process quota block,
Binary: nt!ps
The preceding output identified that the Gh05 tags are associated with memory owned by win32k.sys—which means they probably contain GDI objects. Based on pool tagging, you can also see that process objects (with tag Proc) are abundant in non-paged memory.
Finding Pool Allocations
Once you know the tag for an interesting (or suspicious) pool, you can use the !poolfind WinDbg extension to locate the addresses of all the memory blocks associated with the tag. For example, the following command shows pools with a Proc tag. If a rootkit calls ExAllocatePoolWithTag with a tag such as l33t, then you can use a similar command to hunt down all the kernel memory allocated by the rootkit.
kd> !poolfind Proc 0
Scanning large pool allocation table for Tag: Proc (823ec000 : 823f8000)
Searching NonPaged pool (81337000 : 82400000) for Tag: Proc
81f99d80 size: 8 previous size: 38 (Free) Pro.
81fbebc0 size: 280 previous size: 278 (Allocated) Proc (Protected)
81fc3680 size: 280 previous size: 30 (Allocated) Proc (Protected)
81fc9d80 size: 280 previous size: 98 (Free) Pro.
81fd5588 size: 280 previous size: 108 (Allocated) Proc (Protected)
81ff0930 size: 8 previous size: 40 (Free) Pro.
81ffd688 size: 280 previous size: 8 (Allocated) Proc (Protected)
82000770 size: 280 previous size: 40 (Allocated) Proc (Protected)
[...]
The output shows that !poolfind located several allocations with the Proc tag. Some are free (perhaps previously used for process objects that terminated) and some are allocated and protected (probably containing process objects for active processes). Because you know the structure for a process object (i.e., _EPROCESS), you can use that to get detailed information about each allocation. The following command shows how to determine the process name for the allocation at 81fbebc0:
kd> dt _EPROCESS 81fbebc0 + 8 + 18
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER 0x1cada55'd9ffb16e
+0x078 ExitTime : _LARGE_INTEGER 0x0
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : 0x00000120
[...]
+0x168 Filler : 0
+0x170 Session : 0xf8a94000
+0x174 ImageFileName : [16] "sqlservr.exe"
+0x184 JobLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x18c LockedPagesList : (null)
Why did we add 8 and 18 bytes (hex) to the pool allocation? It’s because each pool begins with a _POOL_HEADER structure, which is 8 bytes on the XP system that we used for testing. In the case of process objects, the pool header is then followed by an _OBJECT_HEADER, which is 18 bytes. After that, the _EPROCESS structure begins.
Finding the Pool Tag for an Address
You can use the !pool command to perform a reverse lookup on an address. If you have an address and don’t know its purpose, you can query for the associated tag, like this:
kd> !pool 81f4b270
Pool page 81f4b270 region is Nonpaged pool
81f4b000 size: 1d0 previous size: 0 (Free) Irp
81f4b1d0 size: 30 previous size: 1d0 (Allocated) Even (Protected)
81f4b200 size: 10 previous size: 30 (Free) Irp
81f4b210 size: 30 previous size: 10 (Allocated) Vad
81f4b240 size: 30 previous size: 30 (Allocated) Vad
*81f4b270 size: 10 previous size: 30 (Free) *File
Pooltag File : File objects
81f4b280 size: 98 previous size: 10 (Allocated) File (Protected)
81f4b318 size: 40 previous size: 98 (Allocated) Vadl
Now that you’ve determined the address 81f4b270 to be within a memory pool marked with the File tag, you can bet it’s a pool that contains a _FILE_OBJECT structure.
Additional Information
You should note the following points about pool tagging:
· The default pooltag.txt contains descriptions for tags used by most of the Microsoft drivers, but not for all third-party drivers, much less rootkits. One way you can hunt down the associated driver on disk, assuming it isn’t packed, is by searching your system32\drivers directory for .sys files that contain the 4-byte ASCII pool tag (see How to find pool tags used by third-party drivers8).
· The kernel does not prevent a rootkit from calling ExAllocatePoolWithTag with a tag used for a legitimate purpose. For example, a rootkit could allocate memory from the non-paged pool with the tag Proc and use it to store a list of command and control servers. You could catch these attempts by performing sanity checks on the content—something memory forensics frameworks do to reduce false positives when scanning for objects. For example, you could check if the process ID is valid, based on the maximum number of processes your system supports (see Pushing the Limits of Windows: Processes and Threads9). If the claimed process ID is something like 0xF7175511, then the memory you found in a pool marked with a Proc tag either contains an old, partially overwritten process object, or it never contained a process object in the first place. Also, be aware that rootkits can allocate memory using ExAllocatePool, which does not assign tags at all.
· For more information on pool headers and object headers, see Andreas Schuster’s Searching for processes and threads in Microsoft Windows memory dumps.10 If you don’t know the object’s structure, or if the memory doesn’t contain an object at all, then you can just explore it with commands such as db and dd.
7 http://www.microsoft.com/whdc/driver/tips/PoolMem.mspx
8 http://support.microsoft.com/kb/298102
9 http://blogs.technet.com/markrussinovich/archive/2009/07/08/3261309.aspx
10 http://www.dfrws.org/2006/proceedings/2-Schuster.pdf
Recipe 14-8: Catching Breakpoints on Driver Load
You can find supporting material for this recipe on the companion DVD.
The best place to start debugging a rootkit driver is at its entry point address. Why? Well, for the same reason that you typically debug processes starting with their entry points. If you allow any instructions to execute before your debugger gets control, then the malware could disable your debugger or complete installation before you even get the chance to analyze it.
One of the issues with catching a breakpoint on a driver’s entry point address is that you won’t know where to set the breakpoint until the driver loads. You can’t add the ImageBase and AddressOfEntryPoint values in the driver’s PE header and determine the address of the first instruction as you can for executable (.exe) Win32 programs. This is because executables are first to load in their own private address space, so there shouldn’t be any address conflicts. Drivers, on the other hand, share the same address space with all other drivers and will need to be re-based.
Before you get started, let’s review some of the methods that malware can use to load a driver. The techniques you use to catch breakpoints will depend on how the driver was loaded.
· ZwLoadDriver: Malware can load drivers by calling this API function, which exists on XP and later systems.
· Services: Malware can load drivers by installing them as a service and then starting the service.
· ZwSetSystemInformation: Malware can load drivers by calling this API function with the SystemLoadAndCallImage class.
Table 14-3 contains a summary of the different techniques discussed in this recipe, along with their primary advantages and disadvantages.
Table 14-3: Methods of Catching Breakpoints on Driver Load
Method |
Advantage |
Disadvantage |
Deferred BP |
Works for all loading methods |
Requires prior knowledge of driver’s name and entry point address |
Hard-coded BP |
Not WinDbg-specific, works for all loading methods |
Requires CRC update, will not work on signed drivers, and must have access to the driver’s file on disk before it loads |
Loading a test driver |
Not WinDbg-specific |
Requires a separate breakpoint for different loading methods, may require recompiling the test driver for your target platform |
Event exceptions |
Does not require prior knowledge of driver name or prior access to driver’s file on disk, works for all loading methods |
Requires a few additional commands after catching the exception |
In the following discussions, you will need to know how to load a driver for the purposes of analyzing it. Here are a few techniques you can use:
· Use the sc.exe command11 to create a service for the driver.
· Use Process Hacker (click Tools⇒ Create Service).
· Use the DLoad12 utility from Code Project—this is a GUI tool that lets you load a driver using ZwLoadDriver, ZwSetSystemInformation, or by using Services.
· Double-click malware that installs the driver you want to analyze.
Deferred Breakpoints
You can set deferred breakpoints with the bu command (the u stands for unresolved, which is interchangeable with deferred in this case). The significance of these breakpoints is that WinDbg allows you to set them even if the target driver has not loaded yet. In the future, whenever a new driver loads, WinDbg checks if the driver contains the routine for which you set a deferred breakpoint. If so, WinDbg converts the routine to an address and sets the breakpoint.
The following command shows you how to use deferred breakpoints, assuming your driver is named mydriver.sys and it contains a function named DriverEntry. When you use the bl (breakpoint list) command to list the breakpoints, you’ll see parentheses around the routine name, which indicates that WinDbg was not able to resolve the routine in any currently loaded driver (as expected).
kd> bu mydriver!DriverEntry
kd> bl
0 eu 0001 (0001) (mydriver!DriverEntry)
At this point, you can use the g (go) command to let the target system execute. On the target system, load mydriver.sys. Your breakpoint should trigger like this:
kd> g
Breakpoint 0 hit
mydriver!DriverEntry:
f8c534b0 8bff mov edi,edi
One weakness with deferred breakpoints is that drivers aren’t required to export a function named DriverEntry—they can have any name the programmer desires. Thus, in many cases, your deferred breakpoint, based on locating DriverEntry, will fail and the driver will execute beyond your control.
To avoid this unwanted execution, you could look up the AddressOfEntryPoint value in the driver’s PE header and use that as a relative offset from the driver name when setting a breakpoint. This would take care of issues regarding function names. Assuming the driver’s AddressOfEntryPoint is 0x605, you could use the following command:
kd> bu mydriver+605
kd> bl
0 eu 0001 (0001) (mydriver+605)
In this case, you must at least know the driver’s name ahead of time. In addition, you need the AddressOfEntryPoint value, which requires that you parse the driver’s PE header before it loads. If you’re dealing with malware that drops a randomly named driver each time, or tries to prevent other programs from accessing its driver on disk, then you might need to use an anti-rootkit tool such as GMER to locate and extract the driver first.
Hard-coding Breakpoints
By hard-coding a breakpoint into the driver’s file on disk, you can be sure to catch it when the driver loads. This eliminates the need to set special breakpoints in your debugger, but it requires that you make a modification to the driver on disk. Specifically, you would look up the driver’s AddressOfEntryPoint value and replace the first byte of the function with 0xCC (an INT 3software breakpoint). The following commands show you how to make the required changes with pefile and then update the CRC checksum (otherwise some versions of Windows will reject the driver entirely). Make sure you save the original byte that you overwrite because you’ll need to replace it once the driver loads.
$ python
>>> import pefile
>>> pe = pefile.PE("mydriver.sys")
>>> orig_byte = pe.get_data(pe.OPTIONAL_HEADER.AddressOfEntryPoint, 1)
>>> print "Original: %x" % ord(orig_byte)
Original: 8b
>>> pe.set_bytes_at_rva(pe.OPTIONAL_HEADER.AddressOfEntryPoint,
chr(0xCC))
True
>>> pe.OPTIONAL_HEADER.CheckSum = pe.generate_checksum()
>>> pe.write("output.sys")
After applying the patch, regardless of how the driver is loaded, you should catch a breakpoint on its entry point function. Use the eb (edit byte) command in WinDbg to replace the original byte that you overwrote with 0xCC, and then you can continue debugging the driver.
kd> g
Break instruction exception - code 80000003 (first chance)
output+0x605:
bfaf1605 cc int 3
kd> u eip
output+0x605:
bfaf1605 cc int 3
bfaf1606 ff558b call dword ptr [ebp-75h]
bfaf1609 ec in al,dx
bfaf160a a18415afbf mov eax,dword ptr [output+0x584 (bfaF1484)]
bfaf160f 85c0 test eax,eax
bfaf1611 b940bb0000 mov ecx,0BB40h
bfaf1616 7404 je output+0x61c (bfaf161c)
bfaf1618 3bc1 cmp eax,ecx
kd> eb bfaf1605 8b
kd> u eip
output+0x605:
bfaf1605 8bff mov edi,edi
bfaf1607 55 push ebp
bfaf1608 8bec mov ebp,esp
bfaf160a a18415afbf mov eax,dword ptr [output+0x584 (bfaF1484)]
bfaf160f 85c0 test eax,eax
bfaf1611 b940bb0000 mov ecx,0BB40h
bfaf1616 7404 je output+0x61c (bfaf161c)
bfaf1618 3bc1 cmp eax,ecx
The disadvantage to hard-coding breakpoints is that you need access to the driver’s file on disk prior to loading it. If you’re analyzing malware that drops a driver on the fly and then loads it, you may need to recover the driver first. Furthermore, this technique won’t work for drivers that are cryptographically signed.
Loading a Test Driver
This method involves loading a test driver on your target system before executing malware. When the test driver loads, it looks on the stack to determine which instruction called the driver’s entry point—which you can then use as your breakpoint address. If the malware loads a malicious driver using the same technique as you used to load the test driver, your breakpoint will trigger at the right time—immediately before the malicious driver’s entry point is called.
The following is the source code for the test driver, named DriverEntryFinder, which you can find on the DVD.
#include "ntddk.h"
#include <stdio.h>
NTSTATUS DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
return 0;
}
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObj,
IN PUNICODE_STRING DriverReg)
{
int RETADDR;
// look on the stack to see who called us...
// the return address for the caller should
// be at +12 bytes relative to the ESP register
__asm {
push edx
mov edx, [esp+12]
mov [RETADDR], edx
pop edx
};
DbgPrint("The BP address depends on your load method:\n");
DbgPrint(" 1 - ZwLoadDriver\n");
DbgPrint(" 2 - Services\n");
DbgPrint(" 3 - ZwSystemSystemInformation\n");
DbgPrint("BP address if you used 1 or 2: 0x%x\n", RETADDR-3);
DbgPrint("BP address if you used 3: 0x%x\n", RETADDR-2);
DriverObj->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
To use DriverEntryFinder, simply load it on your target system using the desired method (ZwLoadDriver, ZwSetSystemInformation, or Services). As described in Table 14-3, the breakpoint address will differ depending on how the driver is loaded. If you use ZwLoadDriver or the Services method, the breakpoint address will be inside a function named nt!IopLoadDriver. If you usent!ZwSetSystemInformation, the breakpoint address will be inside nt!ZwSetSystemInformation. Therefore, you should use DriverEntryFinder to locate all possible breakpoint addresses—unless you already know which method your malware sample uses.
If you’re already attached to your target with WinDbg, then you’ll see the DriverEntryFinder’s output in your WinDbg window. Otherwise, you can see the output with DebugView.
kd> g
The BP address depends on your load method:
1 - ZwLoadDriver
2 - Services
3 - ZwSystemSystemInformation
BP address if you used 1 or 2: 0x805a39aa
BP address if you used 3: 0x805a39ab
kd> ln 0x805a39aa
(805a35a9) nt!IopLoadDriver+0x66a
kd> u 0x805a39aa
nt!IopLoadDriver+0x66a:
805a39aa ff572c call dword ptr [edi+2Ch]
kd> bp nt!IopLoadDriver+0x66a
The output from the program prints two BP addresses. It is up to you to pick the right one based on how you loaded the driver. For example, if you used ZwLoadDriver (method 1), then the correct BP address is 0x805a39aa. The call instruction that you see at this address leads to the driver’s entry point!
Event Exceptions
You can configure how WinDbg handles events, including how the debugger reacts when new drivers load, new processes start, new threads start, and so on. This is probably the most straightforward way to catch a breakpoint on loading drivers. To view how WinDbg currently handles particular events, use the sx (set exception) command, like this:
kd> sx
ct - Create thread - ignore
et - Exit thread - ignore
cpr - Create process - ignore
epr - Exit process - ignore
ld - Load module - ignore
ud - Unload module - ignore
ser - System error - ignore
ibp - Initial breakpoint - ignore
iml - Initial module load - ignore
out - Debuggee output – output
[...]
As you can see, WinDbg currently ignores the load module event (module is a synonym for driver in this case, but can also refer to user mode DLLs). If you want to gain control whenever a new module loads, you can reconfigure it like this:
kd> sxe ld
kd> sx
ct - Create thread - ignore
et - Exit thread - ignore
cpr - Create process - ignore
epr - Exit process - ignore
ld - Load module - break
ud - Unload module - ignore
ser - System error - ignore
ibp - Initial breakpoint - ignore
iml - Initial module load - ignore
out - Debuggee output – output
[...]
Most of the events can accept arguments so that WinDbg doesn’t break when any driver loads or when any process starts—you can tailor it by name. However, assuming you don’t know the name of the driver to be loaded, you can just use the sxe ld command and it will cause WinDbg to break for all drivers. Once that is set, you can execute the malware that loads a driver, and you should see something like this:
kd> g
nt!DebugService2+0x10:
80506d3e cc int 3
Now, find the newly loaded driver and set a normal breakpoint at its entry point address.
kd> lm n
start end module name
804d7000 806ed700 nt ntoskrnl.exe
806ee000 8070e300 hal halaacpi.dll
b21cd000 b220da80 HTTP HTTP.sys
bfaf3000 bfaf3780 mydriver mydriver.sys
[...]
kd> !dh -a bfaf3000
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (i386)
5 number of sections
4AA83235 time date stamp Wed Sep 09 18:54:45 2009
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
10E characteristics
Executable
Line numbers stripped
Symbols stripped
32 bit word machine
OPTIONAL HEADER VALUES
10B magic #
7.10 linker version
180 size of code
180 size of initialized data
0 size of uninitialized data
605 address of entry point
[...]
kd> bp mydriver+605
kd> bl
0 e bfaf3605 0001 (0001) mydriver+0x605
kd> g
Breakpoint 0 hit
mydriver+0x605:
bfaf3605 8bff mov edi,edi
The address bfaf3605 is the entry point address for mydriver.sys. On any given system, there may be hundreds of drivers loaded, and if you’re not familiar with their names, it will be difficult to spot the one new driver that triggered your breakpoint. In this case, you can use .logopen as discussed in Recipe 14-5 to save the output of lm n before you execute malware. When your breakpoint triggers, re-run lm n and use diff on the log file to identify which driver is new.
11 http://support.microsoft.com/kb/251192
12 http://www.codeproject.com/KB/system/DLoad.aspx
Recipe 14-9: Unpacking Drivers to OEP
Assuming you’ve followed the instructions in the previous recipe, you can execute malware on a target system and expect to catch the breakpoint when a new driver loads. This gives you the ability to inspect the driver’s load parameters, unpack the driver, and understand its run-time behavior via debugging. It’s worth mentioning that if you get really lucky and run into a packed driver that doesn’t make any API calls during its unpacking routine, you might be able to unpack it with a user mode debugger (see the inReverse blog13). The example we use for this recipe is a variant of the Tibs malware—which you can find more about on ThreatExpert’s website.14
Investigating the Driver Object
First, make sure the target system is running by typing g for go. Then execute the malware on your target system. Assuming the driver was loaded with ZwLoadDriver or via Services, you’ll see something like this:
kd> g
Breakpoint 0 hit
nt!IopLoadDriver+0x66a:
805a39aa ff572c call dword ptr [edi+2Ch]
Before moving further, you may want to pause and gather some information about the loading driver. The value in the edi register is a pointer to the loading driver’s _DRIVER_OBJECT structure. Why does the instruction in IopLoadDriver call the member at 2Ch of this structure? Well, let’s see:
kd> dt _DRIVER_OBJECT [edi]
nt!_DRIVER_OBJECT
+0x000 Type : 4
+0x002 Size : 168
+0x004 DeviceObject : (null)
+0x008 Flags : 2
+0x00c DriverStart : 0xb2034000
+0x010 DriverSize : 0x25880
+0x014 DriverSection : 0x820e2da0
+0x018 DriverExtension : 0x8205e2f0 _DRIVER_EXTENSION
+0x01c DriverName : _UNICODE_STRING
"\Driver\windev-6ec4-1ec9"
+0x024 HardwareDatabase : 0x8068fa90 _UNICODE_STRING
"\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x028 FastIoDispatch : (null)
+0x02c DriverInit : 0xb2058a00
+0x030 DriverStartIo : (null)
+0x034 DriverUnload : (null)
+0x038 MajorFunction : [28] 0x804fa87e
nt!IopInvalidDeviceRequest+0
The preceding output shows that the driver’s DriverInit (entry point function) value exists at offset 2Ch of the _DRIVER_OBJECT structure—that’s why IopLoadDriver calls it. You can also see the following information about the driver:
· DeviceObject: This member is currently NULL, which means the driver has not yet initialized any devices (for example, through the use of IoCreateDevice or IoCreateDeviceSecure). If a driver creates any devices at all, it typically does so in the DriverEntry function, which hasn’t executed yet, which is why it is currently NULL.
· DriverStart: This member specifies the driver’s load address in kernel memory.
· DriverSize: This member specifies the size in bytes of the driver’s binary in memory (as per the SizeOfImage field in the PE header).
· DriverName: This member specifies the driver’s name.
· DriverInit: This member specifies the address of the driver’s entry point function.
· DriverUnload: This member specifies the virtual address of a function to be called when the driver unloads. In this case, the value is NULL because the driver hasn’t been allowed to execute long enough to set its unload function yet.
· MajorFunction: This is an array of 28 IRP (Input/Output Request Packet) handlers that are currently all initialized to the default nt!IopInvalidDeviceRequest.
To get to the driver’s entry point function from your breakpoint in IopLoadDriver, you just need to execute a single instruction (call dword ptr [edi+2Ch]). When you type the t (trace) command, it executes a single instruction and then prints the location and disassembly of the next instruction, like this:
kd> t
windev_6ec4_1ec9+0x24a00:
b2058a00 e81c000000 call windev_6ec4_1ec9+0x24a21 (b2058a21)
The output shows that the new driver’s name is windev_6ec4_1ec9.sys. Also, notice how the next instruction is at b2058a00, which is the same value you saw in the DriverInit member of the _DRIVER_OBJECT structure. This verifies that you’ve reached the driver’s entry point function. However, this isn’t necessarily the original entry point function (i.e., before being packed).
Unpacking Stage One
Microsoft defined the driver entry point function as follows:
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
);
The important part to remember is that a pointer to the driver’s own _DRIVER_OBJECT is passed as its first parameter. You can print a disassembly of the entire entry point function, like this:
kd> uf .
windev_6ec4_1ec9+0x24a00:
b2058a00 e81c000000 call windev_6ec4_1ec9+0x24a21 (b2058a21)
b2058a05 60 pushad
b2058a06 b97c040000 mov ecx,47Ch ; this is the loop counter
windev_6ec4_1ec9+0x24a0b:
b2058a0b 812a7338483f sub dword ptr [edx],3F483873h ; unpack key
b2058a11 83c204 add edx,4 ; scan to next 4 bytes
b2058a14 83e904 sub ecx,4 ; subtract 4 from the loop counter
b2058a17 85c9 test ecx,ecx ; is the counter zero?
b2058a19 75f0 jne windev_6ec4_1ec9+0x24a0b (b2058a0b)
windev_6ec4_1ec9+0x24a1b:
b2058a1b 61 popad
b2058a1c 83c208 add edx,8
b2058a1f ffe2 jmp edx ; jump to unpacked code
The entry point calls a function at b2058a21 so you can explore that function as well:
kd> uf b2058a21
windev_6ec4_1ec9+0x24a21:
; moves the DriverObject into edx
b2058a21 8b542408 mov edx,dword ptr [esp+8]
; moves the DriverObject->DriverStart into edx
b2058a25 8b520c mov edx,dword ptr [edx+0Ch]
b2058a28 81c280530200 add edx,25380h
b2058a2e b835580200 mov eax,25835h
b2058a33 c3 ret
According to the disassemblies, the purpose of the function at b2058a21 is to copy the driver’s load address (DriverObject->DriverStart) into the edx register, add 25380 to the value, and then return. The entry point function then initializes a loop counter to 47c and subtracts 3F483873 from each 4 bytes starting at the value pointed to by edx (which presumably is the start of the packed code) until the loop counter reaches 0. Once the simple round of decoding is complete, the driver jumps to edx+8, which is either the program’s original entry point (OEP) or the next layer of packing.
The following command steps over the function at b2058a21 because you know what it does now:
kd> p
windev_6ec4_1ec9+0x24a05:
b2058a05 60 pushad
At this time, the edx register should contain a pointer to the packed code. You can verify by printing a hexdump and disassembly. Notice how the disassembly contains instructions such as aas and les that you don’t typically see—that’s a sign that the code is packed, which makes sense because you haven’t unpacked it yet.
kd> r edx
edx=b2059380
kd> db edx
b2059380 7338483f7338483f-c88b9e96c420493f s8H?s8H?..... I?
b2059390 7338a5c0602b607f-7320dc4173384907 s8..'+'.s .As8I.
b20593a0 fe38d1c40053883f-fcc5f559b3384bcc .8...S.?...Y.8K.
b20593b0 1453883ffcc5155a-b338d3fc4853883f .S.?...Z.8..HS.?
b20593c0 76f5f559b338d5f4-7e54883f2c6d483f v..Y.8..~T.?,mH?
b20593d0 732bedccf833647f-73c3e5ec8d78483e s+...3d.s....xH>
b20593e0 28ea627f7337fee4-8d7848a974889b27 (.b.s7...xH.t..'
b20593f0 003b483ffebde159-b338cdffe74f983e .;H?...Y.8...O.>
kd> u edx
windev_6ec4_1ec9+0x25380:
b2059380 7338 jae windev_6ec4_1ec9+0x253ba (b20593ba)
b2059382 48 dec eax
b2059383 3f aas
b2059384 7338 jae windev_6ec4_1ec9+0x253be (b20593be)
b2059386 48 dec eax
b2059387 3f aas
b2059388 c88b9e96 enter 9E8Bh,96h
b205938c c420 les esp,fword ptr [eax]
You can let the driver unpack itself by allow it to execute until it reaches the jmp edx instruction at b20581af, like this:
kd> g b2058a1f
windev_6ec4_1ec9+0x24a1f:
b2058a1f ffe2 jmp edx
Did it work? If so, you should see an entirely new set of bytes at the same addresses as before.
kd> db edx
b2059388 5553565751e80000-00005d81edf21740 USVWQ.....]....@
b2059398 00e89302000001c8-8b0089858d1a4000 ..............@.
b20593a8 898dad1a4000038d-a11a4000898dcd1a ....@.....@.....
b20593b8 40008bbdd51a4000-03bdad1a40008db5 @.....@.....@...
b20593c8 0b1c4000b9340000-00f3a48d85fb1b40 ..@..4.........@
b20593d8 008b9dad1a4000ff-b5b11a4000ffb5a5 .....@.....@....
b20593e8 1a40006a015053e8-8d0200008b85991a .@.j.PS.........
b20593f8 400085c0741750ff-b5c51a4000ffb5ad @...t.P....@....
kd> u edx
windev_6ec4_1ec9+0x25388:
b2059388 55 push ebp
b2059389 53 push ebx
b205938a 56 push esi
b205938b 57 push edi
b205938c 51 push ecx
b205938d e800000000 call windev_6ec4_1ec9+0x25392 (b2059392)
b2059392 5d pop ebp
b2059393 81edf2174000 sub ebp,4017F2h
Great! The data has been decoded in memory and now represents valid instructions. Now you can use the t command to execute the jmp instruction, which will take you to b2059388. Then disassemble the entire function revealed by the first layer of packing.
kd> t
windev_6ec4_1ec9+0x25388:
b2059388 55 push ebp
kd> uf .
windev_6ec4_1ec9+0x25388:
b2059388 55 push ebp
b2059389 53 push ebx
b205938a 56 push esi
b205938b 57 push edi
b205938c 51 push ecx
b205938d e800000000 call windev_6ec4_1ec9+0x25392 (b2059392)
b2059392 5d pop ebp
b2059393 81edf2174000 sub ebp,4017F2h
b2059399 e893020000 call windev_6ec4_1ec9+0x25631 (b2059631)
b205939e 01c8 add eax,ecx
b20593a0 8b00 mov eax,dword ptr [eax]
b20593a2 89858d1a4000 mov dword ptr [ebp+401A8Dh],eax
b20593a8 898dad1a4000 mov dword ptr [ebp+401AADh],ecx
b20593ae 038da11a4000 add ecx,dword ptr [ebp+401AA1h]
b20593b4 898dcd1a4000 mov dword ptr [ebp+401ACDh],ecx
b20593ba 8bbdd51a4000 mov edi,dword ptr [ebp+401AD5h]
b20593c0 03bdad1a4000 add edi,dword ptr [ebp+401AADh]
b20593c6 8db50b1c4000 lea esi,[ebp+401C0Bh]
b20593cc b934000000 mov ecx,34h
b20593d1 f3a4 rep movs byte ptr es:[edi],byte ptr [esi]
b20593d3 8d85fb1b4000 lea eax,[ebp+401BFBh]
b20593d9 8b9dad1a4000 mov ebx,dword ptr [ebp+401AADh]
b20593df ffb5b11a4000 push dword ptr [ebp+401AB1h]
b20593e5 ffb5a51a4000 push dword ptr [ebp+401AA5h]
b20593eb 6a01 push 1
b20593ed 50 push eax
b20593ee 53 push ebx
b20593ef e88d020000 call windev_6ec4_1ec9+0x25681 (b2059681)
b20593f4 8b85991a4000 mov eax,dword ptr [ebp+401A99h]
b20593fa 85c0 test eax,eax
b20593fc 7417 je windev_6ec4_1ec9+0x25415 (b2059415)
windev_6ec4_1ec9+0x253fe:
b20593fe 50 push eax
b20593ff ffb5c51a4000 push dword ptr [ebp+401AC5h]
b2059405 ffb5ad1a4000 push dword ptr [ebp+401AADh]
b205940b e835000000 call windev_6ec4_1ec9+0x25445 (b2059445)
b2059410 e812000000 call windev_6ec4_1ec9+0x25427 (b2059427)
windev_6ec4_1ec9+0x25415:
b2059415 e8b3000000 call windev_6ec4_1ec9+0x254cd (b20594cd)
b205941a 8b85cd1a4000 mov eax,dword ptr [ebp+401ACDh]
b2059420 59 pop ecx
b2059421 5f pop edi
b2059422 5e pop esi
b2059423 5b pop ebx
b2059424 5d pop ebp
b2059425 ffe0 jmp eax ; jump to unpacked code
The output shows calls to six subroutines (which, for the sake of brevity, we will not show here) and a similar-looking jump near the end. It is generally unsafe to simply play until you reach the final jump because the driver may execute anti-debugging code or complete installation in one of the six subroutines. Therefore, you should disassemble each subroutine to get an idea of what they do, and then determine the next steps. In this case, you’ll see that they only seem to contain more unpacking code. Therefore, you can, in fact, safely execute the driver until it reaches the jump near the end, and then follow the jump and see where you end up.
kd> g b2059425
windev_6ec4_1ec9+0x25425:
b2059425 ffe0 jmp eax
kd> t
windev_6ec4_1ec9+0x24b8c:
b2058b8c 8bff mov edi,edi
kd> uf .
windev_6ec4_1ec9+0x24aee:
b2058aee 8bff mov edi,edi
b2058af0 55 push ebp
b2058af1 8bec mov ebp,esp
b2058af3 56 push esi
b2058af4 ff750c push dword ptr [ebp+0Ch]
b2058af7 8b7508 mov esi,dword ptr [ebp+8] ; DriverObject
b2058afa 56 push esi
b2058afb e806ffffff call windev_6ec4_1ec9+0x24a06 (b2058a06)
b2058b00 85c0 test eax,eax
b2058b02 757e jne windev_6ec4_1ec9+0x24b82 (b2058b82)
windev_6ec4_1ec9+0x24b04:
b2058b04 b9464403b2 mov ecx,offset
windev_6ec4_1ec9+0x446 (b2034446)
; setting the 28 IRP handler functions
b2058b09 898ea4000000 mov dword ptr [esi+0A4h],ecx
b2058b0f 898ea0000000 mov dword ptr [esi+0A0h],ecx
b2058b15 898e9c000000 mov dword ptr [esi+9Ch],ecx
b2058b1b 898e98000000 mov dword ptr [esi+98h],ecx
b2058b21 898e94000000 mov dword ptr [esi+94h],ecx
b2058b27 898e90000000 mov dword ptr [esi+90h],ecx
b2058b2d 898e8c000000 mov dword ptr [esi+8Ch],ecx
b2058b33 898e88000000 mov dword ptr [esi+88h],ecx
b2058b39 898e84000000 mov dword ptr [esi+84h],ecx
b2058b3f 898e80000000 mov dword ptr [esi+80h],ecx
b2058b45 894e7c mov dword ptr [esi+7Ch],ecx
b2058b48 894e78 mov dword ptr [esi+78h],ecx
b2058b4b 894e74 mov dword ptr [esi+74h],ecx
b2058b4e 894e70 mov dword ptr [esi+70h],ecx
b2058b51 894e6c mov dword ptr [esi+6Ch],ecx
b2058b54 894e68 mov dword ptr [esi+68h],ecx
b2058b57 894e64 mov dword ptr [esi+64h],ecx
b2058b5a 894e60 mov dword ptr [esi+60h],ecx
b2058b5d 894e5c mov dword ptr [esi+5Ch],ecx
b2058b60 894e58 mov dword ptr [esi+58h],ecx
b2058b63 894e54 mov dword ptr [esi+54h],ecx
b2058b66 894e50 mov dword ptr [esi+50h],ecx
b2058b69 894e4c mov dword ptr [esi+4Ch],ecx
b2058b6c 894e48 mov dword ptr [esi+48h],ecx
b2058b6f 894e44 mov dword ptr [esi+44h],ecx
b2058b72 894e40 mov dword ptr [esi+40h],ecx
b2058b75 894e3c mov dword ptr [esi+3Ch],ecx
b2058b78 894e38 mov dword ptr [esi+38h],ecx
; setting DriverObject->DriverUnload
b2058b7b c74634744403b2 mov dword ptr [esi+34h],offset
windev_6ec4_1ec9+0x474 (b2034474)
[...]
This time, when you print the disassembly of the function you’ve reached, you’ll see some code that you typically see in an (unpacked) driver’s entry point. In particular, the function sets the driver’s unload action and initializes the table of 28 IRP handlers. You can see it move [ebp+8], which is the function’s first argument (a pointer to the driver’s _DRIVER_OBJECT) into the esiregister. Then it moves the address of a subroutine at b2034446 into the ecx register—this is presumably the default IRP handler or I/O dispatcher. It moves the subroutine’s address into all 28 slots of the MajorFunction table. How do you know all those offsets from esi are slots in the MajorFunction table? If you look at the beginning of this recipe where it shows the format of a_DRIVER_OBJECT, you’ll see that the DriverUnload function exists at offset 34h and the MajorFunction table begins at 38h. Therefore, [esi+38h] is MajorFunction[0], [esi+3Ch] is MajorFunction[1], and so on.
13 http://www.inreverse.net/?p=327
14 http://www.threatexpert.com/reports.aspx?page=1&find=windev
Recipe 14-10: Dumping and Rebuilding Drivers
You can find supporting materials for this recipe on the companion DVD.
The tools we introduced in the unpacking section of Chapter 12 (such as LordPE, ProcDump, and Import REConstructor) don’t operate in kernel mode. If you need to extract a driver, or code from an arbitrary pool in kernel memory, one option is to use Volatility and the associated plug-ins (see Recipe 16-9). This recipe shows an alternate method, which involves using WinDbg to dump the driver. Then you can open the dumped file in IDA Pro for more in-depth static analysis.
Dumping the Driver
First, you’ll need to determine the memory range you want to dump. There are a few ways that you can go about finding that information:
· If you’ve unpacked the driver to OEP, as shown in the previous recipe, or if you were able to spot the malicious driver by using anti-rootkit tools (see Recipe 10-6), then you know the name and/or base address of the driver.
· If you know the starting address of a thread created by a malicious driver, you can dump memory at the thread’s start address and search backwards in memory to find the corresponding MZ header (if there is one).
· If you search kernel memory for any MZ headers that aren’t in the list of loaded modules per the lm command, then you might have found a rootkit hiding.
The technique you use to find a suspicious memory range will vary between cases. In this example, we’ll continue using the driver from the previous recipe that you unpacked to OEP. The following command identifies its start and end address:
kd> lm n
start end module name
804d7000 806ed700 nt ntoskrnl.exe
806ee000 8070e300 hal halaacpi.dll
b2034000 b2059880 windev_6ec4_1ec9 windev-6ec4-1ec9.sys
[...]
The following command dumps a copy of the driver’s memory to disk. When you do this, the dumped copy is saved to your debugging machine (the one on which you run WinDbg) and not the target. You specify the output file name, starting address, and number of bytes to read from the starting address like this:
kd> .writemem c:\unpacked.sys b2034000 Lb2059880-b2034000
Writing 25880 bytes.........................
Repairing the Driver
If you plan to analyze the dumped driver in IDA, you need to take a few additional steps.
1. Repair the PE header. The dumped driver contains the original PE header, so it reflects the default ImageBase rather than the driver’s real load address. Furthermore, in this case it reflects the packed driver’s AddressOfEntryPoint value rather than the unpacked driver’s entry point (OEP). The real load address is b2034000—the same as what you typed to dump the driver. The OEP address is shown in Recipe 14-9, but here it is again as a refresher:
kd> uf .
windev_6ec4_1ec9+0x24aee:
b2058aee 8bff mov edi,edi
b2058af0 55 push ebp
b2058af1 8bec mov ebp,esp
[...]
You can apply the changes using any PE editor, or you can do it on the command line with pefile. Remember that the AddressOfEntryPoint is relative to the ImageBase, not the absolute address.
$ python
>>> import pefile
>>> pe = pefile.PE("unpacked.sys")
>>> orig_ImageBase = pe.OPTIONAL_HEADER.ImageBase
>>> orig_AddressOfEntryPoint = pe.OPTIONAL_HEADER.AddressOfEntryPoint
>>> pe.OPTIONAL_HEADER.ImageBase = 0xb2034000
>>> pe.OPTIONAL_HEADER.AddressOfEntryPoint = (0xb2058aee - 0xb2034000)
>>> pe.write("unpacked.sys")
>>> print "Old Base: %x\nNew Base: %x\nOld EP: %x\nNew EP: %x\n" % (
orig_ImageBase,
newpe.OPTIONAL_HEADER.ImageBase,
orig_AddressOfEntryPoint,
newpe.OPTIONAL_HEADER.AddressOfEntryPoint)
Old Base: 10000
New Base: b2034000
Old EP: 24a00
New EP: 24aee
2. Load the driver in IDA. Because the file type is a kernel driver, IDA automatically labels the entry point function as DriverEntry and labels its parameters accordingly. Figure 14-10 shows how this should appear.
Figure 14-10: The unpacked driver loaded into IDA Pro
3. Examine the code. You’ll notice if you browse other functions in the driver that the Import Address Table (IAT) is not properly rebuilt. This is the same problem you will run into when unpacking user mode programs (see Recipe 12-10) and when extracting processes and drivers from memory dumps (see Recipe 16-8). Figure 14-11 shows you how the unrepaired disassembly appears in IDA Pro. Instead of API function names, you can only see calls to addresses.
Figure 14-11: TWithout repairing the IAT, you can’t see API function names.
4. Find the IAT. To do this, find an IAT entry in WinDbg or in the IDA Pro disassembly. Figure 14-11 shows two—dword_B2035230 and dword_B203522C. For this purpose, you’ll want to use the lowest address because you’re looking for the start of the IAT. Depending on the size of the IAT, configure your command to show the entire IAT, like this:
kd> dps B203522C-34 L30
b20351f8 00000000
b20351fc 00000000
b2035200 804e3bf6 nt!IofCompleteRequest
b2035204 804dc1a0 nt!KeWaitForSingleObject
b2035208 804e3996 nt!KeSetEvent
b203520c 80505480 nt!IoDeleteDevice
b2035210 805c5ba9 nt!IoDeleteSymbolicLink
b2035214 804dc8b0 nt!ZwClose
b2035218 8057b03b nt!PsTerminateSystemThread
b203521c 804ff079 nt!DbgPrint
b2035220 804e68eb nt!KeResetEvent
b2035224 805b86b4 nt!IoCreateNotificationEvent
b2035228 804d92a7 nt!RtlInitUnicodeString
b203522c 80564be8 nt!ObReferenceObjectByHandle
b2035230 8057ae8f nt!PsCreateSystemThread
b2035234 8054cbe8 nt!NtBuildNumber
b2035238 805a9c9b nt!IoCreateSymbolicLink
b203523c 8059fa61 nt!IoCreateDevice
b2035240 804fcaf3 nt!wcsstr
b2035244 8054b587 nt!ExFreePoolWithTag
b2035248 8054b6c4 nt!ExAllocatePoolWithTag
b203524c 80591865 nt!IoGetDeviceObjectPointer
b2035250 804d9050 nt!ObfDereferenceObject
b2035254 805473ba nt!_wcslwr
b2035258 80501e33 nt!wcsncpy
b203525c 8057715c nt!PsLookupThreadByThreadId
b2035260 804e7748 nt!wcscmp
b2035264 804dd440 nt!ZwQuerySystemInformation
b2035268 804dc810 nt!ZwAllocateVirtualMemory
b203526c 804ea23a nt!KeDetachProcess
b2035270 804dd044 nt!ZwOpenProcess
b2035274 804ea2c4 nt!KeAttachProcess
b2035278 8057194e nt!PsLookupProcessByProcessId
b203527c 804e8784 nt!KeInitializeEvent
b2035280 8055a220 nt!KeServiceDescriptorTable
b2035284 804e5411 nt!KeInsertQueueApc
b2035288 804e5287 nt!KeInitializeApc
b203528c 80552000 nt!KeTickCount
b2035290 805337eb nt!KeBugCheckEx
b2035294 00000000
b2035298 0044005c
5. You can copy and paste all lines shown in bold and save it to a text file. This is the information you need to label the imported functions in the IDA database.
6. Use the windbg_to_ida.py script to convert the lines you pasted into a text file (info.txt in the example) into IDC code for IDA Pro.
$ python windbg_to_ida.py info.txt
MakeName(0xb2035200, "IofCompleteRequest");
MakeName(0xb2035204, "KeWaitForSingleObject");
MakeName(0xb2035208, "KeSetEvent");
MakeName(0xb203520c, "IoDeleteDevice");
MakeName(0xb2035210, "IoDeleteSymbolicLink");
MakeName(0xb2035214, "ZwClose");
MakeName(0xb2035218, "PsTerminateSystemThread");
MakeName(0xb203521c, "DbgPrint");
MakeName(0xb2035220, "KeResetEvent");
[...]
7. In IDA Pro, go to File ⇒ IDC Command (or Shift+F2) and paste in the output from windbg_to_ida.py. You should see a window similar to the one shown in Figure 14-12. When you click OK, the IDC statements will label the API calls throughout your dumped driver.
Figure 14-12: Entering IDC statements into IDA Pro
8. In IDA Pro, click Options ⇒ General ⇒ Analysis ⇒ Reanalyze Program. This will cause IDA Pro to fix up the disassembly with types and variable names, now that it can recognize which API functions are being called. Figure 14-13 shows an updated view of the same code blocks that Figure 14-11 contained, but with the new labels applied.
Figure 14-13: The repaired driver in IDA Pro
The addresses and exact commands you learned about in the past few recipes are specific to windev_6ec4_1ec9.sys. However, the tools, techniques, and reasons you entered particular commands are all generic—and you can use them to unpack and rebuild kernel drivers installed by other malware samples.
Recipe 14-11: Detecting Rootkits with WinDbg Scripts
You can find supporting material for this recipe on the companion DVD.
If you routinely type the same commands into WinDbg, you could save time by creating reusable scripts. Another advantage to writing scripts is that you can share them with the community. You can find several general-purpose scripts on Microsoft’s Debugging Toolbox blog15 and some security-related scripts on the Laboskopia website.16
Using the Laboskopia Scripts
The Laboskopia scripts are particularly relevant because you can use them to identify kernel-level rootkits. For example, the scripts are capable of listing the following information:
· Entries in the Interrupt Descriptor Table (IDT) to identify rootkits that hook interrupts
· Entries in the Global Descriptor Table (GDT) to identify rootkits that install call gates
· Model-specific registers (MSRs) to identify rootkits that hook SYSENTER on XP and later systems
· System service descriptor tables (SSDTs) to identify rootkits that hook kernel-mode API functions
Note If you’re looking for a concise, but informative explanation of the following rootkit techniques, see skape & Skywing’s “A Catalog of Windows Local Kernel-mode Backdoor Techniques” at http://uninformed.org/index.cgi?v=8&a=2.
WinDbg scripts are plain-text files that contain the same commands that you would normally type into the debugger. To install scripts, just copy them into a subdirectory relative to WinDbg.exe. The image in Figure 14-14 shows an example directory layout after unzipping the collection of scripts from Laboskopia.
The syntax for executing a script in WinDbg looks like this:
kd> $$><directory\filename.txt
kd> $$>a< "c:\directory\filename.txt" "argument1" "argument2"
Figure 14-14: Directory layout for installed WinDbg scripts
WinDbg is strict about where you place spaces and quotations when calling external scripts, so be careful what you type. Once you’ve got the Laboskopia scripts installed, run the initialization script, which sets up aliases for the other commands. It will look like this:
kd> $$><script\\@@init_cmd.wdbg;
Labo Windbg Script : Ok :)
('al' for display all commands)
kd> al
Alias Value
------- -------
!!display_all_gdt $$><script\display_all_gdt.wdbg;
!!display_all_idt $$><script\display_all_idt.wdbg;
!!display_all_msrs $$><script\display_all_msrs.wdbg;
!!display_current_gdt $$><script\display_current_gdt.wdbg;
!!display_current_idt $$><script\display_current_idt.wdbg;
!!display_current_msrs $$><script\display_current_msrs.wdbg;
!!display_system_call $$><script\display_system_call.wdbg;
!!hide_current_process $$><script\hide_current_process.wdbg;
!!save_all_reports $$><script\save_all_reports.wdbg;
!!search_hidden_process $$><script\search_hidden_process.wdbg;
!@display_gdt $$><script\display_gdt.wdbg;
!@display_idt $$><script\display_idt.wdbg;
!@display_msrs $$><script\display_msrs.wdbg;
!@get_debug_mode $$><script\get_debug_mode.wdbg;
!@get_original_ntcall $$><script\get_original_ntcall.wdbg;
!@get_original_win32kcall $$><script\get_original_win32kcall.wdbg;
!@get_system_version $$><script\get_system_version.wdbg;
!@hide_process $$><script\hide_process.wdbg;
!@is_hidden_process $$><script\is_hidden_process.wdbg;
With WinDbg commands alone (i.e., not using scripts), you can print IDT and MSR addresses like this:
kd> !idt 2e
Dumping IDT:
2e: 804de631 nt!KiSystemService
kd> rdmsr 0x176
msr[176] = 00000000'804de6f0
kd> ln 804de6f0
(804de6f0) nt!KiFastCallEntry
The authors chose to display the 0x2E entry of the IDT and the 0x176 MSR, because those are popular values that rootkits overwrite. However, they are not the only values that rootkits can overwrite to perform malicious actions. Using the Laboskopia scripts, you can print more comprehensive listings. Here is an example showing the extra information provided for the IDT:
kd> !!display_all_idt
####################################
# Interrupt Descriptor Table (IDT) #
####################################
Processor 00
Base : 8003F400 Limit : 07FF
Int Type Sel : Offset Attrib Symbol/Owner
---- ------ ------------- ------ ------------
002A IntG32 0008:804DEB92 DPL=3 nt!KiGetTickCount (804deb92)
002B IntG32 0008:804DEC95 DPL=3 nt!KiCallbackReturn (804dec95)
002C IntG32 0008:804DEE34 DPL=3 nt!KiSetLowWaitHighThread (804dee34)
002D IntG32 0008:F8964F96 DPL=3 SDbgMsg+0xf96 (f8964f96)
002E IntG32 0008:804DE631 DPL=3 nt!KiSystemService (804de631)
002F IntG32 0008:804E197C DPL=0 nt!KiTrap0F (804e197c)
[...]
The following example shows you how to print the MSRs:
kd> !!display_all_msrs
###################################
# Model-Specific Registers (MSRs) #
###################################
Processor 00
IA32_P5_MC_ADDR msr[00000000] = 0
IA32_P5_MC_TYPE msr[00000001] = 0
IA32_MONITOR_FILTER_LINE_SIZE msr[00000006] = 0
IA32_TIME_STAMP_COUNTER *msr[00000010] = 000066ce'0366c49c
IA32_PLATFORM_ID *msr[00000017] = 21520000'00000000
IA32_APIC_BASE *msr[0000001B] = 00000000'fee00900
MSR_EBC_HARD_POWERON msr[0000002A] = 0
MSR_EBC_SOFT_POWERON msr[0000002B] = 0
MSR_EBC_FREQUENCY_ID msr[0000002C] = 0
IA32_BIOS_UPDT_TRIG msr[00000079] = 0
IA32_BIOS_SIGN_ID *msr[0000008B] = 00000008'00000000
IA32_MTRRCAP *msr[000000FE] = 00000000'00000508
IA32_SYSENTER_CS *msr[00000174] = 00000000'00000008
IA32_SYSENTER_ESP *msr[00000175] = 00000000'f8974000
IA32_SYSENTER_EIP *msr[00000176] = 00000000'804de6f0
nt!KiFastCallEntry (804de6f0)
[...]
The next example shows you how to print the SSDTs. This script actually displays which entries are hooked rather than just printing their addresses. The target machine is infected with a rootkit that hooks NtEnumerateValueKey and NtOpenProces for the purpose of hiding files and processes.
kd> !!display_system_call
*****************
* Current Table *
*****************
ServiceDescriptor n0
---------------------
ServiceTable : nt!KiServiceTable (804e26a8)
ParamTableBase : nt!KiArgumentTable (80510088)
NumberOfServices : 0000011c
Index Args Check System call
----- ---- ----- -----------
0000 0006 OK nt!NtAcceptConnectPort (8058fe01)
0001 0008 OK nt!NtAccessCheck (805790f1)
[...]
0049 0006 HOOK-> lanmandrv+0x884 (f8b0e884) ##### Original ->
nt!NtEnumerateValueKey (80590677)
004A 0002 OK nt!NtExtendSection (80625758)
004B 0006 OK nt!NtFilterToken (805b0b4e)
[...]
0079 000C OK nt!NtOpenObjectAuditAlarm (805953b5)
007A 0004 HOOK-> lanmandrv+0x53e (f8b0e53e) ##### Original ->
nt!NtOpenProcess (805717c7)
007B 0003 OK nt!NtOpenProcessToken (8056def5)
007C 0004 OK nt!NtOpenProcessTokenEx (8056e0ee)
[...]
A final thing you can do with the Laboskopia scripts is compile all the output from previously shown commands (and more) into a single text file for later analysis. To do this, use the !!save_all_reports commands and then look for the log file in the same directory as WinDbg.exe.
Writing Your Own Scripts
If you want to add scripts to the Laboskopia collection (or start building your own from scratch), then you can. The following WinDbg script checks for registered notification routines (for more information, see Recipe 17-9). You can find the full source file named WinDbgNotify.txt on the companion DVD.
$$
$$ Example WinDbg script
$$
r $t0 = poi(nt!PspCreateThreadNotifyRoutineCount);
r $t1 = poi(nt!PspCreateProcessNotifyRoutineCount);
r $t2 = poi(nt!PspLoadImageNotifyRoutineCount);
.printf "No. thread start callbacks: %x\n", @$t0;
r $t3 = 0;
.while (@$t3 < 8)
{
r $t4 = poi(nt!PspCreateThreadNotifyRoutine + (@$t3 * 4));
.if (@$t4 != 0) {
.printf "%x => %x\n", @$t3, @$t4;
}
r $t3 = @$t3 + 1;
}
.printf "No. process start callbacks: %x\n", @$t1;
r $t3 = 0;
.while (@$t3 < 8)
{
r $t4 = poi(nt!PspCreateProcessNotifyRoutine + (@$t3 * 4));
.if (@$t4 != 0) {
.printf "%x => %x\n", @$t3, @$t4;
}
r $t3 = @$t3 + 1;
}
.printf "No. image load callbacks: %x\n", @$t2;
r $t3 = 0;
.while (@$t3 < 8)
{
r $t4 = poi(nt!PspLoadImageNotifyRoutine + (@$t3 * 4));
.if (@$t4 != 0) {
.printf "%x => %x\n", @$t3, @$t4;
}
r $t3 = @$t3 + 1;
}
Assuming you place the WinDbgNotify.txt script in a directory named MyScript, you can then invoke it like this:
kd> $$><MyScript/WinDbgNotify.txt
No. thread start callbacks: 0
No. process start callbacks: 0
No. image load callbacks: 1
0 => e13cdb37
The output shows that the target system has one registered image load callback routine. The routine at e13cbd37 will therefore execute when processes load DLLs. You could take this script further by doing a reverse lookup on the address and printing the owning driver, or even disassembling the function.
15 http://blogs.msdn.com/debuggingtoolbox/default.aspx
16 http://www.laboskopia.com/download/SysecLabs-Windbg-Script.zip
Recipe 14-12: Kernel Debugging with IDA Pro
Recent versions of IDA Pro come with a WinDbg plug-in that gives you the best of both worlds—access to a remote kernel using WinDbg’s engine paired with IDA’s GUI, IDA’s scripting languages, and IDA’s plug-ins. This recipe walks you through setting up the WinDbg plug-in for IDA and shows how it can make your life much easier.
To get started, you’ll need to follow the instructions in Recipe 14-3 or 14-4 so that your debugging machine and target system are connected. You should also review the tutorial created by the Hex-Rays staff and a supplementary blog post on debugging a VMware kernel with IDA’s GDB debugger, both accessible on the Hex-Rays website.17
Establishing a Connection
1. Open IDA Pro. Select the WinDbg plug-in, as shown in Figure 14-15.
Figure 14-15: Selecting IDA Pro’s WinDbg plug-in
2. Configure the debug options. In particular, modify the Connection string to the port or pipe that you set up on your virtual machine. Then enable Kernel mode debugging and enter the path to your Debugging tools folder (the directory that contains dbgeng.dll), as shown in Figure 14-16. If you plan on executing malware on the target system that loads a kernel driver, check the Stop on library load/unload option in the Debugger setup window.
Figure 14-16: Configuring the debug options
3. Accept the connection. Upon successful connection to the target system, IDA displays the image shown in Figure 14-17—an option to attach to the remote kernel. Click OK to continue.
Figure 14-17: Accepting the kernel connection
At this point, you can explore the kernel in a very intuitive manner. The image in Figure 14-18 shows critical information in every window.
Figure 14-18: Debugging a remote kernel with IDA Pro
· The IDA View: Shows the main disassembly window—where you view code, set/remove breakpoints, name variables, and so on.
· Debugger controls: Lets you play, pause, stop, step-in, step-over, and so on (there are also keyboard shortcuts for all of the controls).
· Modules tab: Lists the loaded kernel drivers with their base addresses and sizes.
· Symbols tab: If you click any of the loaded kernel drivers in the Modules tab, a new tab opens like the one shown in the top right—where you can browse the symbols in your selected module.
· WinDbg shell: Provides full access to the WinDbg command shell.
Configuring Type Libraries
When you open a file in IDA Pro, the application typically loads type libraries, which contain preconfigured structures and enumerations. However, when you use IDA Pro to debug a kernel, you have to manually load the type libraries. Go to View ⇒ Open subviews ⇒ Type Libraries. Then press the Insert key or right-click in the empty window and select Load type library. At a minimum, you should add the following libraries:
· ntddk: MS Windows <ntddk.h>
· ntapi: MS Windows NT 4.0 Native API <ntapi.h><ntdll.h>
· wnet: MS Windows DDK <wnet/windows.h>
· mssdk: MS SDK (Windows XP)
Once the type libraries are loaded, you can use the Symbol tab to find IopLoadDriver—the function responsible for calling a loaded driver’s entry point (see Recipe 14-8). Then you can do a text search for “call *dword ptr*” and locate the exact instruction in IopLoadDriver that leads to a driver’s entry point. Because you know the instruction references a _DRIVER_OBJECT, and now you have imported the correct type libraries, you can begin to apply labels, as shown in Figure 14-19.
Figure 14-19: The instruction in IopLoadDriver that Calls a driver entry point
Unpacking the Driver
The following example assumes that you’ve read Recipe 14-9 because it’s based on unpacking the same driver, except this time you’ll see it from the perspective of IDA’s GUI. On the target system, load the malicious driver and use IDA’s single-step key (F7) to get from the breakpoint in IopLoadDriver to the loaded driver’s entry point. You should recognize the entry point function where it performs the first round of unpacking.
To let the driver unpack and get to the next round of decoding, right-click the line with jmp edx and select Run to cursor. As you will remember from Recipe 14-9, you actually have to repeat this step once more for the next function because there are two packing layers. When you reach the driver’s unpacked entry point and apply names and labels, it should appear like the image in Figure 14-20.
Figure 14-20: The unpacked driver with labels
17 http://www.hexblog.com/2009/02/advancedwindowskerneldebugg.html