Rootkits: Subverting the Windows Kernel

 < Day Day Up > 

All memory is separated into pages, as in a book. Each page can hold only a certain number of characters. Each process may have a separate lookup table to find these memory pages.

Imagine that memory is like a giant library of books, where every process has its own separate card catalog for looking things up. The different lookup tables can cause memory to be viewed entirely differently by each process. This is how one process can read memory at address 0x00401122 and see "GREG," while another process can read memory at the same address but see "JAMIE." Each process can have a unique "view" of memory.

Access controls are applied to memory pages. To continue our library metaphor, imagine that the CPU is an overbearing librarian who will allow a process to examine only a few books in the library. To read or write memory, a process must first find the correct "book," and then the exact "page" for the memory in question. If the CPU doesn't approve of the book or page that is requested, access is denied.

The lookup procedure for finding a page in this manner is long and involved; access control is enforced at several stages during this procedure. First, the CPU checks whether the process can open the book in question (the descriptor check); next, the CPU checks whether the process can read a certain chapter in the book (the page directory check); and finally, the cpu checks whether the process can read a particular page in the chapter (the page check). Wow that is a lot of work!

Only if the process can pass all the security checks will it be allowed to read a page.

Even if the CPU checks are passed, the page may be marked as read-only. This, of course, means the process can read the page, but cannot write to it. In this way, the integrity of the data can be maintained. Rootkit developers are like vandals in this library, attempting to scribble all over these books so we must learn all we can about manipulating access controls.

Memory Access Check Details

To access a memory page, the x86 processor performs the following checks, in the order shown:

  • Descriptor (or segment) check: Typically, the global descriptor table (GDT) is accessed and a segment descriptor is checked. The segment descriptor contains a value known as the descriptor privilege level (DPL). The DPL contains the ring number (zero to three) required of the calling process. If the DPL requirement is lower than the ring level (sometimes called the current privilege level [CPL]) for the calling process, access is denied, and the memory check stops here.

  • Page directory check: A user/supervisor bit is checked for an entire page table that is, an entire range of memory pages. If the user/supervisor bit is set to zero, then only "supervisor" programs (Rings Zero, One, and Two) can access the range of memory pages; if the calling process is not a "supervisor," the memory check stops here. If the user/supervisor bit is set to 1, then any program can access the range of memory pages.

  • Page check: This check is made for a single memory page. If the page-directory check has succeeded, a page check will be made for the individual page in question. Like the page directory, each individual page has a user/supervisor bit that is checked. If the user/supervisor bit is set to zero, then only "supervisor" programs (Rings Zero, One, and Two) can access the individual page. If the user/supervisor bit is set to 1, then any program can access the individual page. A process can access the page of memory only if it can get all the way to and through this check without any access denials.

The Windows family of operating systems does not really use the descriptor check. Instead, Windows relies only on Rings Zero and Three (sometimes called kernel mode and user mode). This allows the user/supervisor bit in the page table check alone to control access to memory. Kernel-mode programs, running as Ring Zero, will always be able to access memory. User-mode programs, running as Ring Three, can access only memory tagged as "user."

Figure 3-2 shows a dump of the GDT (using SoftIce) for Windows 2000. The DPL for each entry is noted. The first four entries (08, 10, 1B, and 23) encompass the entire range of memory for data and code, and both Ring Zero and Ring Three programs. The result is that the GDT does not provide any security for the system. Security must be enforced "downstream" within the page tables. To understand this in detail, you must first comprehend how a virtual-memory address is translated into an actual physical address. This is explained in the next section.

Figure 3-2. The GDT on Windows 2000.

Paging and Address Translation

The memory-protection mechanism is used for more than just security. Most modern operating systems support virtual memory. This allows each program on the system to have its own address space. It also allows a program to use much more memory than is actually available as "main memory." For example, a computer with 256 MB of RAM does not limit every program to only 256 MB of memory. A program can easily use one GB of memory if it so chooses: The extra memory is simply stored on disk in a file (sometimes called the paging file). Virtual memory allows multiple processes to execute simultaneouslly, each with its own memory, when the total used by all processes is greater than the installed physical RAM.

Memory pages can be marked as paged out (that is, stored on disk rather than in RAM). The processor will interrupt when any of these memory pages is sought. The interrupt handler reads the page back into memory, making it paged in. Most systems allow only a small percentage of all available memory to be paged in at any given time. A computer that is low on physical RAM will have a large paging file that is constantly being accessed. Conversely, more physical RAM means fewer hits on the paging file.

Whenever a program reads memory, it must specify an address. For each process, this address must be translated into an actual physical memory address. This is important: An address used by a process is not the same as the actual physical address where the data resides. A translation routine is needed to identify the proper physical storage location.

For example: If NOTEPAD.EXE seeks the memory contents of virtual address 0x0041FF10, the actual physical address may translate to, say, 0x01EE2F10. If NOTEPAD.EXE executes the instruction "mov eax, 0x0041FF10," the value being read into EAX is actually stored at the physical address 0x01EE2F10. The address is translated from a virtual address to a physical one (see Figure 3-3).

Figure 3-3. Translating the address for a mov instruction.

Page-Table Lookups

Translation of memory addresses is handled via a special table known as the page-table directory. The Intel x86 CPU stores the pointer to the page-table directory in a special register called CR3. This register, in turn, points to an array of 1024 32-bit values called the page directory. Each 32-bit value (called a page-directory entry) specifies the base address of a page table in physical memory, and includes a status bit indicating whether the page table is currently present in memory. From the page table, actual physical addresses can be obtained (see Figure 3-4).

Figure 3-4. Finding a page in memory.

Figure 3-4 shows the different tables that are referenced when looking up a physical memory address. Different parts of the requested address (or virtual address) are used during this lookup. Figure 3-5 shows how each part of the requested address is used during lookup.

Figure 3-5. Different parts of a requested address.[2]

31

22

21

12

11

0

Page Directory Index (1024 possible values)

Page Table Index (1024 possible values)

Location in page (4096 possible values)

[2] If the page is marked as a 4-MB page, bits 22 31 specify the base address of the physical page, and bits 0 21 specify the offset to the physical memory page.

The following steps are taken by the operating system and the CPU in order to translate a requested virtual address into a physical memory address:

  • The CPU consults CR3 to find the base of the page-table directory.

  • The requested memory address is split into three parts, as shown in Figure 3-5.

  • The top 10 bits are used to find the location in the page-table directory (see Figure 3-4).

  • Once the page-directory entry is located, the corresponding page table is found in memory.

  • The middle 10 bits of the address are used to find the index in the page table (see Figure 3-4).

  • The corresponding physical memory address (sometimes called the physical page frame) is found for the page.

  • The bottom 12 bits of the requested address are used to locate an offset in the physical page-frame memory (up to 4096 bytes). The resulting actual physical address contains the requested data.

The requested address is sometimes called a virtual address virtual in that it must first be translated into a real (physical) memory address before it can be used. As you can see, a few twists and turns are required to translate a virtual address into an actual physical memory address. Each step requires information that is obtained from a table. Any of this data could be modified or used by a rootkit.

The Page-Directory Entry

As we have stated, the CR3 register points to the base of the page directory. The page directory is an array of page-directory entries (see Figure 3-6). When a page-directory entry is accessed, the U bit (bit 2) is checked. If U is set to zero, then the page table in question is meant only for the kernel.

Figure 3-6. Page-directory entry.

31

12

11

9

8

7

6

5

4

3

2

1

0

Page Table Base Address

  

0

P S

0

A

P C D

P

W

T

U

W

P

The W bit (bit 1) is also checked. If W is set to zero, then the memory is read-only (as opposed to read/write). Remember that the page-directory entry points to an entire page table (Figure 3-7) an entire collection of pages. The settings in the page-directory entry apply to an entire range of memory pages.

Figure 3-7. Page-table entry.[3]

31

12

11

9

8

7

6

5

4

3

2

1

0

Page Base Address

  

0

0

D

A

P C

D

P W

T

U

W

P

[3] The format of the page-table entry can be somewhat different, depending on the OS.

Note that the program that consults the page directory must be running in Ring Zero.

The Page-Table Entry

The page-table entry concerns only a single page of memory. Again, the U bit is checked, and if it is set to zero, only kernel-mode programs can access this page of memory. The W bit is also checked for read/write access. Noteworthy also is the P bit: If it is set to zero, then the memory is currently paged out to disk (whereas if it is set to one, the memory is resident and available). If the memory is paged out, the memory manager must page in the memory before access can succeed.

Read-Only Access to Some Important Tables

On Windows XP and greater, the memory pages containing the SSDT and IDT are set to read-only in the page table. If an attacker wishes to alter the contents of these memory pages, she must first change the pages to read/write. The best way for a rootkit to do this is called the CR0 trick, described later in this chapter. However, you can also make these tables writable by altering two registry keys. If you wish to disable the read-only settings permanently, you can alter the following registry keys and then reboot.[4]

[4] Thanks to Rob Beck for finding this information.

HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\EnforceWriteProtection = 0 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\DisablePagingExecutive = 1

(The first of these two keys does not exist in a clean XP install; you must add it manually.)

Of course, even if left unchanged, these registry keys are no protection against rootkits, since a rootkit can modify the page tables directly or use the CR0 trick to enable or disable access restrictions on-the-fly.

Multiple Processes, Multiple Page Directories

In theory, using just a single page directory, an operating system can maintain multiple processes, memory protection between processes, and a paging file on disk. But with only one page directory, there would be only one translation map for virtual memory. That would mean all processes would need to share the same memory. Under Windows NT/2000/XP/2003, we know that each process has its own memory they do not share.

The start address of most executables is 0x00400000. How can multiple processes use the same virtual address, but not collide in physical memory? The answer is multiple page directories.

Every process on the system maintains a unique page directory. Each process has its own private value for the CR3 register. This means that every process has a separate and unique virtual memory map. Thus, two different processes can access the memory address 0x00400000, and have it translate into two separate physical memory addresses. This is also why one process cannot "see" into another process's memory.

Even though each process has a unique page table, the memory above 0x7FFFFFFF is typically mapped identically across all processes. This range of memory is reserved for the kernel, and kernel memory stays consistent, regardless of which process is running.

Even when running in Ring Zero, there will be an active process context. The process context includes the machine state for that process (such as the saved registers), the process's environment, the process's security token, and other parameters. For purposes of this discussion, the process context contains the CR3 register, and hence the page directory of the active process. A rootkit developer should consider that modifications made to the page tables for a process will affect not only that process while in user mode, but also the kernel whenever that process is in context. This can be leveraged for advanced stealth techniques.

Processes and Threads

Rootkit developers should understand that the primary mechanism for managing running code is the thread, not the process. The Windows kernel schedules processes based on the number of threads, not processes. That is, if there are two processes, one single-threaded and the other with nine threads, the system will give each thread 10% of the processing time. The single-threaded process would get 10% of the CPU time, while the process with nine threads would get 90%. This example is contrived, of course, since other factors (such as priority) also play a part in scheduling. But the fact remains that, all other factors being equal, scheduling is based entirely on the number of threads, not the number of processes.

Just what is a process? Under Windows, a process is simply a way for a group of threads to share the following data:

  • virtual address space (that is, the value used for CR3)

  • access token, including SID[5]

    [5] A thread may have its own access token which, if present, overrides that of the process.

  • handle table for win32 kernel objects

  • working set (physical memory "owned" by the process)

Rootkits must deal with threads and thread structures for a variety of purposes, including stealth and code injection. Rather than creating new processes, it can create new threads and assign them to an existing process. Rarely would a whole new process need to be created.

When a context switch to a new thread occurs, the old thread state is saved. Each thread has its own kernel stack, so the thread state is pushed onto the top of the thread kernel stack. If the new thread belongs to a different process, the new page directory address for the new process is loaded into CR3. The page directory address can be found in the KPROCESS structure for the process. Once the new thread kernel stack is found, the new thread context is popped from the top of the new thread kernel stack, and the new thread begins execution. If a rootkit modifies the page tables of a process, the modifications will be applied to all threads in that process, because the threads all share the same CR3 value.

We go into much more detail on thread and process structures in Chapter 7, Direct Kernel Object Manipulation.

     < Day Day Up > 

    Категории