Rootkits modify the internal functionality of the OS to conceal their existence. These modifications can hide files, processes, network connections, and other resources from running programs, making it difficult for antivirus products, administrators, and security analysts to discover malicious activity.
The majority of rootkits in use operate by somehow modifying the kernel. Although rootkits can employ a diverse array of techniques, in practice, one technique is used more than any other: System Service Descriptor Table hooking. This technique is several years old and easy to detect relative to other rootkit techniques. However, it’s still used by malware because it’s easy to understand, flexible, and straightforward to implement.
The System Service Descriptor Table (SSDT), sometimes called the System Service Dispatch
Table, is used internally by Microsoft to look up function calls into the kernel. It isn’t
normally accessed by any third-party applications or drivers. Recall from Chapter 7 that kernel code is only accessible from user space
via the SYSCALL, SYSENTER, or
INT 0x2E instructions. Modern versions of Windows use the
SYSENTER instruction, which gets instructions from a function
code stored in register EAX. Example 10-11 shows the code from
ntdll.dll, which implements the NtCreateFile
function and must handle the transitions from user space to kernel space that happen every time
NtCreateFile is called.
Example 10-11. Code for NtCreateFile function
7C90D682 ❶mov eax, 25h ; NtCreateFile
7C90D687 mov edx, 7FFE0300h
7C90D68C call dword ptr [edx]
7C90D68E retn 2ChThe call to dword ptr[edx] will go to the following
instructions:
7c90eb8b 8bd4 mov edx,esp 7c90eb8d 0f34 sysenter
EAX is set to 0x25
❶ in Example 10-11,
the stack pointer is saved in EDX, and then the sysenter
instruction is called. The value in EAX is the function number for NtCreateFile, which will be used as an index into the SSDT when the code enters the
kernel. Specifically, the address at offset 0x25
❶ in the SSDT will be called in kernel mode. Example 10-12 shows a few entries in the SSDT with the entry
for NtCreateFile shown at offset 25.
Example 10-12. Several entries of the SSDT table showing NtCreateFile
SSDT[0x22] = 805b28bc (NtCreateaDirectoryObject)
SSDT[0x23] = 80603be0 (NtCreateEvent)
SSDT[0x24] = 8060be48 (NtCreateEventPair)
❶SSDT[0x25] = 8056d3ca (NtCreateFile)
SSDT[0x26] = 8056bc5c (NtCreateIoCompletion)
SSDT[0x27] = 805ca3ca (NtCreateJobObject)When a rootkit hooks one these functions, it will change the value in the SSDT so that
the rootkit code is called instead of the intended function in the kernel. In the preceding example,
the entry at 0x25 would be changed so that it points to a
function within the malicious driver. This change can modify the function so that it’s
impossible to open and examine the malicious file. It’s normally implemented in rootkits by
calling the original NtCreateFile and filtering the results based
on the settings of the rootkit. The rootkit will simply remove any files that it wants to hide in
order to prevent other applications from obtaining a handle to the files.
A rootkit that hooks only NtCreateFile will not prevent the
file from being visible in a directory listing. In the labs for this chapter, you’ll see a
more realistic rootkit that hides files from directory listings.
Now we’ll look at an example of a rootkit that hooks the SSDT. We’ll analyze a hypothetical infected system, which we think may have a malicious driver installed.
The first and most obvious way to check for SSDT hooking is to examine the SSDT. The SSDT can
be viewed in WinDbg at the offset stored at nt!KeServiceDescriptorTable. All of the function offsets in the SSDT should point to
functions within the boundaries of the NT module, so the first thing we did was obtain those
boundaries. In our case, ntoskrnl.exe starts at address 804d7000 and ends at
806cd580. If a rootkit is hooking one of these functions, the function will probably not point into
the NT module. When we examine the SSDT, we see that there is a function that looks like it does not
fit. Example 10-13 is a shortened version of the
SSDT.
Example 10-13. A sample SSDT table with one entry overwritten by a rootkit
kd> lm m nt
...
8050122c 805c9928 805c98d8 8060aea6 805aa334
8050123c 8060a4be 8059cbbc 805a4786 805cb406
8050124c 804feed0 8060b5c4 8056ae64 805343f2
8050125c 80603b90 805b09c0 805e9694 80618a56
8050126c 805edb86 80598e34 80618caa 805986e6
8050127c 805401f0 80636c9c 805b28bc 80603be0
8050128c 8060be48 ❶f7ad94a4 8056bc5c 805ca3ca
8050129c 805ca102 80618e86 8056d4d8 8060c240
805012ac 8056d404 8059fba6 80599202 805c5f8eThe value at offset 0x25 in this table at ❶
points to a function that is outside the ntoskrnl module, so a
rootkit is likely hooking that function. The function being hooked in this case is NtCreateFile. We can figure out which function is being hooked by
examining the SSDT on the system without the rootkit installed and seeing which function is located
at the offset. We can find out which module contains the hook address by listing the open modules
with the lm command as shown in Example 10-14. In the kernel, the modules listed are all
drivers. We find the driver that contains the address 0xf7ad94a4, and we see that it is within the
driver called Rootkit.
Example 10-14. Using the lm command to find which driver contains a
particular address
kd>lm ... f7ac7000 f7ac8580 intelide (deferred) f7ac9000 f7aca700 dmload (deferred) f7ad9000 f7ada680 Rootkit (deferred) f7aed000 f7aee280 vmmouse (deferred) ...
Once we identify the driver, we will look for the hook code and start to analyze the driver. We’ll look for two things: the section of code that installs the hook and the function that executes the hook. The simplest way to find the function that installs the hook is to search in IDA Pro for data references to the hook function. Example 10-15 is an assembly listing for code that hooks the SSDT.
Example 10-15. Rootkit code that installs a hook in the SSDT
00010D0D push offset aNtcreatefile ; "NtCreateFile" 00010D12 lea eax, [ebp+NtCreateFileName] 00010D15 push eax ; DestinationString 00010D16 mov edi, ds:RtlInitUnicodeString 00010D1C call ❶edi ; RtlInitUnicodeString 00010D1E push offset aKeservicedescr ; "KeServiceDescriptorTable" 00010D23 lea eax, [ebp+KeServiceDescriptorTableString] 00010D26 push eax ; DestinationString 00010D27 call ❷edi ; RtlInitUnicodeString 00010D29 lea eax, [ebp+NtCreateFileName] 00010D2C push eax ; SystemRoutineName 00010D2D mov edi, ds:MmGetSystemRoutineAddress 00010D33 call ❸edi ; MmGetSystemRoutineAddress 00010D35 mov ebx, eax 00010D37 lea eax, [ebp+KeServiceDescriptorTableString] 00010D3A push eax ; SystemRoutineName 00010D3B call edi ; MmGetSystemRoutineAddress 00010D3D mov ecx, [eax] 00010D3F xor edx, edx 00010D41 ; CODE XREF: sub_10CE7+68 j 00010D41 add ❹ecx, 4 00010D44 cmp [ecx], ebx 00010D46 jz short loc_10D51 00010D48 inc edx 00010D49 cmp edx, 11Ch 00010D4F jl ❺short loc_10D41 00010D51 ; CODE XREF: sub_10CE7+5F j 00010D51 mov dword_10A0C, ecx 00010D57 mov dword_10A08, ebx 00010D5D mov ❻dword ptr [ecx], offset sub_104A4
This code hooks the NtCreateFile function. The first
two function calls at ❶ and ❷ create strings for NtCreateFile and KeServiceDescriptorTable that will be
used to find the address of the exports, which are exported by ntoskrnl.exe and
can be imported by kernel drivers just like any other value. These exports can also be retrieved at
runtime. You can’t load GetProcAddress from kernel mode,
but the MmGetSystemRoutineAddress is the kernel equivalent,
although it is slightly different from GetProcAddress in that it
can get the address for exports only from the hal and ntoskrnl kernel modules.
The first call to MmGetSystemRoutineAddress
❸ reveals the address of the NtCreateFile function, which will be used by the malware to determine which address in
the SSDT to overwrite. The second call to MmGetSystemRoutineAddress gives us the address of the SSDT itself.
Next there is a loop from ❹ to ❺, which iterates through the SSDT until it finds a value that
matches the address of NtCreateFile, which it will overwrite with
the function hook.
The hook is installed by the last instruction in this listing at ❻, wherein the procedure address is copied to a memory location.
The hook function performs a few simple tasks. It filters out certain requests while allowing
others to pass to the original NtCreateFile. Example 10-16 shows the hook function.
Example 10-16. Listing of the rootkit hook function
000104A4 mov edi, edi
000104A6 push ebp
000104A7 mov ebp, esp
000104A9 push [ebp+arg_8]
000104AC call ❶sub_10486
000104B1 test eax, eax
000104B3 jz short loc_104BB
000104B5 pop ebp
000104B6 jmp NtCreateFile
000104BB -----------------------------
000104BB ; CODE XREF: sub_104A4+F j
000104BB mov eax, 0C0000034h
000104C0 pop ebp
000104C1 retn 2ChThe hook function jumps to the original NtCreateFile
function for some requests and returns to 0xC0000034 for others. The value 0xC0000034 corresponds to STATUS_OBJECT_NAME_NOT_FOUND. The call at ❶
contains code (not shown) that evaluates the ObjectAttributes
(which contains information about the object, such as filename) of the file that the user-space
program is attempting to open. The hook function returns a nonzero value if the NtCreateFile function is allowed to proceed, or a zero if the rootkit
blocks the file from being opened. If the hook function returns a zero, the user-space applications
will receive an error indicating that the file does not exist. This will prevent user applications
from obtaining a handle to particular files while not interfering with other calls to NtCreateFile.
Interrupts are sometimes used by rootkits to interfere with system events. Modern processors implement interrupts as a way for hardware to trigger software events. Commands are issued to hardware, and the hardware will interrupt the processor when the action is complete.
Interrupts are sometimes used by drivers or rootkits to execute code. A driver calls IoConnectInterrupt to register a handler for a particular interrupt code,
and then specifies an interrupt service routine (ISR), which the OS will call every time that
interrupt code is generated.
The Interrupt Descriptor Table (IDT) stores the ISR information, which you can view with the
!idt command. Example 10-17 shows a normal IDT,
wherein all of the interrupts go to well-known drivers that are signed by Microsoft.
Example 10-17. A sample IDT
kd> !idt
37: 806cf728 hal!PicSpuriousService37
3d: 806d0b70 hal!HalpApcInterrupt
41: 806d09cc hal!HalpDispatchInterrupt
50: 806cf800 hal!HalpApicRebootService
62: 8298b7e4 atapi!IdePortInterrupt (KINTERRUPT 8298b7a8)
63: 826ef044 NDIS!ndisMIsr (KINTERRUPT 826ef008)
73: 826b9044 portcls!CKsShellRequestor::`vector deleting destructor'+0x26
(KINTERRUPT 826b9008)
USBPORT!USBPORT_InterruptService (KINTERRUPT 826df008)
82: 82970dd4 atapi!IdePortInterrupt (KINTERRUPT 82970d98)
83: 829e8044 SCSIPORT!ScsiPortInterrupt (KINTERRUPT 829e8008)
93: 826c315c i8042prt!I8042KeyboardInterruptService (KINTERRUPT 826c3120)
a3: 826c2044 i8042prt!I8042MouseInterruptService (KINTERRUPT 826c2008)
b1: 829e5434 ACPI!ACPIInterruptServiceRoutine (KINTERRUPT 829e53f8)
b2: 826f115c serial!SerialCIsrSw (KINTERRUPT 826f1120)
c1: 806cf984 hal!HalpBroadcastCallService
d1: 806ced34 hal!HalpClockInterrupt
e1: 806cff0c hal!HalpIpiHandler
e3: 806cfc70 hal!HalpLocalApicErrorService
fd: 806d0464 hal!HalpProfileInterrupt
fe: 806d0604 hal!HalpPerfInterruptInterrupts going to unnamed, unsigned, or suspicious drivers could indicate a rootkit or other malicious software.