Logging Keystrokes with MSDOS: Part 1
Introduction
In the previous article, we saw how we can compile the source code to a 16-bit binary executable, create an iso image with the executable stored in them and mount the iso image with VirtualBox to make its contents available in the MSDOS environment. The program we've presented dumped the whole Interrupt Vector Table IVT and nulled its values in it to crash the system. This wasn't a particularly smart way of doing something cool, which is why we'll now present another program that can be used to log the keystrokes typed in the MSDOS environment. Now that's something cool, isn't it?
We'll do that with the use of TSR (Terminate and Stay Resident), which is a computer system call in MSDOS that returns control to the system as if the program has quit, but keeps the program in memory, to be revived later by a hardware or software interrupt. This system call can be used to create the appearance of multitasking by transferring control back to the terminated program on automatic or externally-generated events, such as pressing a certain key on keyboard [1].
In the MSDOS operating system, only one program can be running at any single time. When the program completes, it gives control back to the DOS shell program, the COMMAND.COM using the system call "int 21". After the program terminates, all the system resources, including the memory, are freed so that they can be used again by some other process. Because of this, we can't reload the process at some near future at some specific input vector, but must instead restart it from scratch. But if the program terminates with a system call "int 21", some of the memory of the process is not freed, but can be used by the process. Usually, before the program terminates with an "int 21", it must install a few interrupt handlers, so it can be called again: this usually happens by reading the current address and storing it within the memory space of the TSR and installing a pointer to its own code; the stored address is called after the TSR has received the interrupt.
In this example, we'll change the entry in the IVT table, so that we'll be able to log the keystrokes when each key on the keyboard has been pressed: the pressed keys will be stored in a global memory. The logging of the keys will be done automatically, since we'll change the appropriate entry in the IVT table, but reading the values from memory must be done via a program, because no program exists that can be used in such a way.
Normal Execution when Pressing a Key
First, we must be aware of the fact that when a key on the keyboard is pressed, a maskable interrupt 9 is being invoked. But why is the key press a maskable interrupt and not a nonmaskable interrupt? It's because it can be masked and the processor can execute it sometime in the future, while the nonmaskable interrupts must be handled right away. You've probably noticed this without even realizing it. Has it ever happened that you were doing something really intensive hard-drive and CPU-wise and the computer become unresponsive. Then when pressing some key, like Alt-F2 to kill the process that's causing the non-responsiveness, the key just didn't work. And soon after that, that program closed by itself. Well, this is the cause of a maskable interrupts, because the previous key press is not being executed right away, but as soon as the processor had some extra time to actually execute it, it did, which consequently killed the program causing all the problems.
But let's not waste any more time, let's get back to talking about intercepting key presses. So far, we know that whenever we press a key on the keyboard, the maskable interrupt 9 will be called, which will execute the Interrupt Service Routine ISR from the IVT. The normal execution when pressing a key is presented on the picture below:
We have the MSDOS system running and when pressing a key, an interrupt 9 is invoked, which goes into the Interrupt Vector Table IVT and takes the address of the ISR from it. Then it invokes the Interrupt Service Routine ISR of the corresponding interrupt and returns the execution to the MSDOS system, where the pressed key has been shown on the screen. But this happens so fast we don't even notice it.
Hooked Execution when Pressing a Key
We've already said that in MSDOS, only one program can be running at a time, so we can't actually have a background process running and logging keystrokes. Also, when the program is done with the execution, it is completely wiped form the memory. This is a problem which we must somehow solve in order to be able to log the keystrokes. To solve this, we must use the TSR (Terminate and Stay Resident) technique. We've already mentioned that TSR can be used to terminate a program in such a way that it gives the control back to the MSDOS system, but doesn't actually wipe the program from memory. The program is kept in memory and its code can be called at any time we want by aninterrupt of some kind. To do that, we must write a program so it will terminate with a system call "int 21", which will keep its memory intact. Before the program terminates, it must install the interrupt handlers that point to itself in order to be called again. How else can the program be called again? It can't, except if we start it from the command line, but that will restart the program, not make it continue from where it left off. We can instruct the program to install a hardware or software interrupt, which will make the program callable by the hardware or the currently executing program.
We must write a program that does the following:
- Read the current address of the ISR from the IVT[9]
- Write a new address of the logging code ISR to the IVT[9]
- Call the original IVT[9] address after the logging code is done executing
Basically, when the program is done executing, the end result is shown below:
When the key is pressed, the address from IVT[9] will be read pointing to the new interrupt service routine ISR[X] that the current program has installed. After executing that service routine, the original service routine ISR[9] must be called, which will also do the rest of the job (what normally happens when the key is pressed). By doing that, we've just inserted a new ISR into the call chain, so when a key is pressed, two ISRs are actually called: one is the one we've provided and logs the key and the other is the original ISR.
But where can we save the logged keystrokes? There are multiple options, but in our case we'll be storing them in a global memory and then also write a separate program that will read those values from the memory and display them. Not exactly the most convenient solution, but it works.
We've just seen what we must do in order to be able to log keystrokes. But we must still execute the TSR program that does the above. We can execute the program manually in order to prove a point that this is possible, but back in the day, this technique is used by viruses and no user would intentionally run such a program after each reboot (well the virus that does that can be hidden inside other files and automatically executed when those files were opened, but let's not get into that now). What we usually want to achieve is that the above procedure is done automatically, which can be achieved by running the TSR program right after the operating system has booted. This can be done in various ways, but usually we want to load that in one of the programs that are always in memory and cannot be unloaded.
Writing the Keystrokes to the Buffer
We've seen how we must write a program that will hook a keystroke and now we're actually going to do it. First, we must set-up the custom interrupt service routine ISR[X]. Let's present all of the program from [2] that actually hooks the ISR when a key is being pressed. First, we're using the CSEG to specify the segment if the type 'CODE'. The following other types can also be used: CODE, DATA, CONST and STACK. The segment directive is used to instruct how the program is supposed to be loaded in memory. In our program, we have only the 'CODE' segment, so this isn't really a problem. All our code is enclosed in the following:
[plain]
CSEG SEGMENT BYTE PUBLIC 'CODE'
ASSUME CS:CSEG, DS:CSEG, SS:CSEG
; This label defines the starting point
_here:
JMP _main
...
CSEG ENDS
END _here
The assume directive tells the assembler that we have loaded the specified segment registers with the segment addresses. In our case, we've told the assembler that the CSEG segment is the code, data and stack segment. The ORG instruction tells the assembler that we would like to load the program at offset 100. After that, the program is starting by jumping to the _main label, which is presented below:
[plain]
PUBLIC _main
_main:
PUSH BP
MOV BP,SP
MOV AX,CS
MOV SS,AX
LEA AX, _localStk
ADD AX,100H
CALL NEAR PTR _install
MOV AH,31H
MOV AL,0H
MOV DX,200H
pop BP
RET
In the code above, we can see that we're first creating a new stack frame by pushing the BP to the stack and changing the value of BP into SP. Next, we're moving the address of the code segment into AX and SS and changing the AX to point to the first address after the _localStk variable (because we're adding the 0x100 value to the pointer, which is exactly the size of the _localStk data structure). The stack pointer register is then changed to point to that address, which is how we're initializing the stack.
Next, we're calling the _installfunction, which is presented below:
[plain]
_install:
; sets up the first ISR (vector 187 == 0xBB)
LEA DX,_getBufferAddr
MOV CX,CS
MOV DS,CX
MOV AH,25H
MOV AL,187
; get address of existing BIOS 0x9 interrupt
MOV AH,35H
MOV AL,09H
INT 21H
MOV WORD PTR _oldISR[0],BX
; get address of existing BIOS 0x16 interrupt
MOV AH,35H
MOV AL,16H
INT 21H
MOV WORD PTR _chkISR[0],BX
; set up BIOS ISR hook
LEA DX,_hookBIOS
MOV CX,CS
MOV DS,CX
MOV AH,25H
MOV AL,09H
RET
[/plain]
The _install function should set the interrupt vectors the way we've described before. First, we're loading the address of the _getBufferAddr into register DX, and loading the address of the code segment into register DS and using them to set the interrupt vector. The high byte of the AX register contains the 0x25 that is used to set the interrupt vector and the low byte of the AX register, which contains 0xCC and is used to set the interrupt. Basically, we're initializing the 187th interrupt vector to point to the _getBufferAddr, which has just become the ISR of the 187th interrupt. This means that whenever the 187th interrupt is invoked, the code at the _getBufferAddr address will be called.
The next step is getting the address of the interrupt service routine of the 9th interrupt. The AH=0x25 was used to set the interrupt vector and now the AH=0x35 is used to get the interrupt vector. The value in the AL tells which interrupt vector we would like to set or get. Essentially, we're saying that get the address of the interrupt service routine (mov ah,35h) of the 9th vector (mov al,09h) where the action is caused with the 'int 21h' instruction. After the 'int 21h' instruction, the address of the ISR is stored in the ES:BX registers, which we're saving into the _oldISR variable. The next piece of code block is exactly the same as the previous one, except that we're getting the address of the 16th vector and saving it into the _chkISR variable. The interrupt 0x9 is being invoked when the key has been pressed, while the interrupt 0x16 represents the keyboard interrupt routine.
The next piece of code loads the address of the _hookBIOS variable into the register DX, which is later used for setting the address of the ISR routine of the 9th vector in the IVT table. After that, we're returning from the _install function.
Conclusion
We've seen the basic overview of what we must do when hooking the 0x9 interrupt in the MSDOS environment. In the next article, we'll take a look at how to actually achieve that and log the keystrokes, like viruses used to do back in the days when we still used MSDOS.
References:
[1]: Terminate and Stay Resident, accessible at http://en.wikipedia.org/wiki/Terminate_and_Stay_Resident.
Become a certified reverse engineer!
[2]: Bill Blunder, The Rootkit Arsenal: Escape and Evasion in the Dark Corners of the System.