Rootkits: Subverting the Windows Kernel
< Day Day Up > |
The interrupt descriptor table register (IDTR) stores the base (the start address) of the interrupt descriptor table (IDT) in memory. The IDT, used to find the software function employed to handle an interrupt, is very important.[8] Interrupts are used for a variety of low-level functions in a computer. For example, an interrupt is signaled whenever a keystroke is typed on the keyboard. [8] Also, for interrupt handling to occur on a CPU, the IF bit in that CPU's EFlags register must be set. The IDT is an array that contains 256 entries one for each interrupt. That means there can be up to 256 interrupts for each processor. Also, each processor has its own IDTR, and therefore has its own interrupt table. If a computer has multiple CPUs, a rootkit deployed on that computer must take into account that each CPU has its own interrupt table. When an interrupt occurs, the interrupt number is obtained from the interrupt instruction, or from the programmable interrupt controller (PIC). In either case, the interrupt table is used to find the appropriate software function to call. This function is sometimes called a vector or interrupt service routine (ISR). When the processor is in protected mode, the interrupt table is an array of 256 eight-byte entries. Each entry has the address of the ISR and some other security-related information. To obtain the address of the interrupt table in memory, you must read the IDTR. This is done using the SIDT (Store Interrupt Descriptor Table) instruction. You can also change the contents of the IDTR by using the LIDT (Load Interrupt Descriptor Table) instruction. More details on this technique can be found in Chapter 8. One trick employed by rootkits is to create a new interrupt table. This can be used to hide modifications made to the original interrupt table. A virus scanner may check the integrity of the original IDT, but a rootkit can make a copy of the IDT, change the IDTR, and then happily make modifications to the copied IDT without detection. The SIDT instruction stores the contents of the IDTR in the following format: /* sidt returns idt in this format */ typedef struct { unsigned short IDTLimit; unsigned short LowIDTbase; unsigned short HiIDTbase; } IDTINFO;
Using the data provided by the SIDT instruction, an attacker can then find the base of the IDT and dump its contents. Remember that the IDT can have up to 256 entries. Each entry in the IDT contains a pointer to an interrupt service routine. The entries have the following structure. // entry in the IDT: this is sometimes called // an "interrupt gate" #pragma pack(1) typedef struct { unsigned short LowOffset; unsigned short selector; unsigned char unused_lo; unsigned char segment_type:4; //0x0E is interrupt gate unsigned char system_segment_flag:1; unsigned char DPL:2; // descriptor privilege level unsigned char P:1; // present unsigned short HiOffset; } IDTENTRY; #pragma pack()
This data structure is used to locate the function in memory that will deal with an interrupt event. This structure is sometimes called an interrupt gate. Using an interrupt gate, a user-mode program can call kernel-mode routines. For example, the interrupt for a system call is targeted at offset 0x2E in the IDT table. A system call is handled in kernel mode, even though it can be initiated from user mode. Additional interrupt gates can be placed as a back door by a rootkit. A rootkit can also hook existing interrupt gates. To access the IDT, use the following code example as a guide: #define MAKELONG(a, b) ((unsigned long) (((unsigned short) (a)) | ((unsigned long) ((unsigned short) (b))) << 16)) The maximum number of entries in the IDT is 256. #define MAX_IDT_ENTRIES 0xFF For example purposes, we implement the parser within the DriverEntry routine of a sample rootkit. NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath ) { IDTINFO idt_info; // this structure is obtained by // calling STORE IDT (sidt) IDTENTRY* idt_entries; // and then this pointer is // obtained from idt_info unsigned long count; // load idt_info __asm sidt, idt_info We use the data returned by the SIDT instruction to get the base of the IDT. We then loop though each entry and print some data to the debug output. idt_entries = (IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase); for(count = 0;count <= MAX_IDT_ENTRIES;count++) { char _t[255]; IDTENTRY *i = &idt_entries[count]; unsigned long addr = 0; addr = MAKELONG(i->LowOffset, i->HiOffset); _snprintf(_t, 253, "Interrupt %d: ISR 0x%08X", count, addr); DbgPrint(_t); } return STATUS_SUCCESS; } This code example illustrates parsing the IDT. No actual modifications to the IDT are made. However, this code can easily become the base of something more complex. More detailed work with interrupts is covered in Chapters 5 and 8. Other Types of Gates
Beyond interrupt gates, the IDT can contain task gates and trap gates. A trap gate differs from an interrupt gate only in that it can be interrupted by maskable interrupts, while an interrupt gate cannot. A task gate, on the other hand, is a rather outdated feature of the processor. A task gate can be used to force an x86 task switch. Since the feature is not used by Windows, we don't illustrate it with an example. A task should not be confused with a process under Windows. A task for the x86 CPU is managed via a Task Switch Segment (TSS) a facility originally used to manage tasks using hardware. Linux, Windows, and many other OS's implement task switching in software, and for the most part do not utilize the underlying hardware mechanism. |
< Day Day Up > |