Reverse engineering

MSDOS and the Interrupt Vector Table (IVT)

Dejan Lukan
March 14, 2013 by
Dejan Lukan

Introduction

Upon booting up MSDOS, we can observe the memory using the "mem /d /p" command, which will show us exactly which part of memory is used by the system, processes, or for IVT, etc. For this article, we're particularly interested with the IVT table that contains the interrupt vectors. It is 1024 bytes in size, so it can hold 256 interrupt vectors.

In this tutorial, we'll take a look at the structure of the interrupt vector table, which we'll describe in detail. We'll also use the program KDOS.c from [1] that will serve as a reference.

The complete code of the KDOS.c program is taken from Appendix A in [1] and is presented here:

[cpp]

#include <stdio.h>

#define WORD unsigned short

#define IDT_001_ADDR 0 // start address of the first IVT vector

#define IDT_255_ADDR 1020 // start address of the last IVT vector

#define IDT_VECTOR_SZ 4 // size of the each IVT vector

#define BP __asm{ int 0x3 } // breakpoint

void main() {

WORD csAddr; // code segment of given interrupt

WORD ipAddr; // starting IP for given interrupt

short address; // address in memory (0-1020)

WORD vector ; // IVT entry ID (0..255)

char dummy; // to help pause program execution

vector = 0x0;

printf("n-- -Dumping IVT from bottom up ---n");

printf("VectortAddresstn");

for(address=IDT_001_ADDR; address<=IDT_255_ADDR; address=address+IDT_VECTOR_SZ,vector++) {

printf("%03dt%08pt", vector , address);

// IVT starts at bottom of memory, so CS is always 0x0

__asm {

PUSH ES

mov AX, 0

mov ES,AX

mov BX, address

mov AX, ES:[BX]

mov ipAddr ,AX

inc BX

inc BX

mov AX, ES:[BX]

mov csAddr, AX

pop ES

};

printf("[CS:IP] = [%04X,%04X]n" ,csAddr, ipAddr);

}

printf("press [ENTER] key to continue:");

scanf( "%c" , &amp;dummy) ;

printf("n---Overwrite IVT from top down---n");

/*

Program will die somewhere around ex4*

Note: can get same results via 005 debug. exe -e corrrnand

*/

for

(

address = IDT_255_ADDR;

address >= IDT_001_ADDR;

address = address - IDT_VECTOR_SZ, vector--

) {

printf("Nulling %03dt%08pn", vector, address);

_asm {

PUSH ES

mov AX,0

mov ES,AX

mov BX, address

mov ES:[BX],AX

INC BX

INC BX

mov ES:[BX], AX

POP ES

};

}

return;

}

[/cpp]

The problem is compiling the source code. Since MSDOS is a 16-bit environment, we need to compile it into a 16-bit executable but Visual Studio doesn't support that in newer versions anymore. In order to compile the above source code into the 16-bit executable, we'll use Open Watcom.

Installing and Compiling with Open Watcom

We can download Open Watcom from http://ftp.heanet.ie/pub/openwatcom/. I've chosen the open-watcom-c-dos-1.9.exe executable, downloaded it and started the installation. Upon starting the installation procedure, the following message will pop-up and ask you if you agree to the license:

Upon clicking I Agree, you have to select the installation directory, which is C:WATCOM by default.

After clicking Next, you have to specify whether you would like a full or selective Watcom installation. This step can be seen below:

The installation will then continue and add Watcom on the system. After this is done, we can start Watcom IDE by going to the C:WATCOMBINNT directory and double-clicking on the IDE.exe executable:

So, Watcom is now up and running. To create a new product, select File – New Project, and specify the name of the project and the Target Environment. This can be seen on the picture below, where we've typed killd as project name and selected the DOS – 16-bit target environment, which will compile the source code to a 16-bit executable that can be run in MSDOS.

Then we need to click on Sources – New Source and select the killdos.c file as can be seen on the picture below. When double-clicking on the source file, a text editor will open where we can add, modify and delete the source code of the project.

At first, this file will be empty as seen on the picture below, which is why we need to copy the above code to the text editor in order to compile it:

When we've inputted the right source code into the text editor and saved it to the disk, we can go back to the IDE and click on Targets – Make in order to compile the source code into an executable. If the compilation is successful, the IDE Log will display something like the following:

At the end of the execution log, we can see that there's a line "Execution complete" which notifies us that the compilation was successful and that the killd.exe executable was successfully created. If we try to run the executable in Windows XP right now, it will display something like the following to show that at first we're reading the values from IVT and after that, nulling them (note that the output was taken from the middle of the output of the killd.exe executable).

What happens when we run the application? The executable is 16-bit, so it must be executed under the 16-bit MSDOS subsystem that is still present on the current versions of Windows operating systems for backward compatibility. The program should be able to run successfully, which we've already seen on the picture above.

Actually, though, the program can't be executed successfully, since it's being run in protected mode, which isn't the same as the real mode. In order to run the program under protected mode, we would have to change the program accordingly, which we won't do here. Rather, we'll copy the killd.exe executable to the MSDOS environment and execute it there; after all, the executable was compiled for a 16-bit MSDOS environment.

Creating and Mounting the ISO in MSDOS

When starting MSDOS in VirtualBox, we don't actually have access to shared folders that is accessible under VirtualBox, but we can still make the files available to MSDOS that's being run under the VirtualBox. In order to that, we must first install cdrtools, which provides the mkisofs executable program that we can use to create an ISO.

To create an ISO, we must first create the folder MSDOS/ and copy some file into that folder for testing purposes, after which we can use mkisofs to actually create an ISO of the newly created directory. All of the commands that we'll use to achieve that can be seen below:

[plain]

# mkdir MSDOS

# echo "testing" > MSDOS/test.txt

# mkisofs -r -o msdos.iso MSDOS/

[/plain]

This will create the msdos.iso ISO file, which we need to mount in the VirtualBox under the Storage settings as presented on the picture below:

When starting MSDOS, the contents of the ISO should now be accessible via one of the drives C:, D:, or some other letter. In our case, this was the C: drive. On the picture below we first used the "c:" command to change our current directory to C: drive and then printed the contents of the C: drive with the dir command.

We can see that the TEST.TXT file is available on the C: drive and that the file actually contains the "testing" string, which is exactly what we've saved into the file. This confirms that we can indeed copy the files to the MSDOS/ folder and create the ISO the way we did. Now we need to copy the killd.exe executable into the MSDOS/ folder, recreate the ISO image, then reboot MSDOS. The C: drive now contains two files, one of which is also the KILLD.EXE executable.

Upon running the executable, the execution will first stop after reading all of the values from the IVT table. The program asked us to press Enter key to continue the execution. When we do that, the program will null every value in the IVT table.

Nulling the values in the IVT table will essentially kill the MSDOS system as we can see in the picture below:

Examining the Program

So far, we've seen the source code of the program and how to compile it and copy it to the MSDOS environment. We've also confirmed that the system crashes when running the program, which is kind of the point of the program.

The first for loop in the program is counting from 0 to 255 for every vector in the IVT table. At first, it prints the ID of the interrupt vector, which can be from 0 to 255. Then it prints the address of each vector in the IVT table from 0x0000 to 0x03FC.

After that we have a __asm block of code that holds assembly instructions. Those assembly instructions first push the ES register to the stack and move a value 0 to the AX register. Remember that we're in a MSDOS 16-bit environment, so the registers are 16-bit in size. After that we're moving the value 0 to the ES register and the address of the IVT vector into the BX register.

The "mov ax, es:[bx]" instruction reads the value from 0000:[bx] into the register AX. This essentially reads every value stored at the vector's address into the AX register. The "mov ipAddr, AX" instruction moves the value read into the ipAddr variable: this is the address that points to the first instruction of the interrupt service routine (ISR) of the corresponding vector in the IVT table.

Next, we're increasing the BX register by 1, twice. Note that we're not incrementing the BX register four times as we're used to, but twice, because we're operating with 16-bit registers and not 32-bit registers. This moves the pointer in the BX register to point to the next 16-bit value where the code segment of the interrupt is stored. Then we're reading the next 2 bytes (16-bits) into the register AX and moving the value into the csAddr variable. Through this, we're actually reading the code segment of the corresponding interrupt.

At the end of the __asm block, we're restoring the ES register. Therefore, the __asm block takes the address stored in the address variable and reads the first two bytes into the ipAddr variable and the second two bytes into the csAddr variable.

Let's change the program and delete the second loop and also change the first loop to display just the first 9 elements, not all of them. After changing the source, compiling it, creating the ISO and restarting MSDOS, the following will be shown when running the program:

In the [CS:IP] column, we can see the pointer to the address where the interrupt service routine is located and its corresponding code segment. If you review real mode memory management, you'll see that both of them are needed to access the actual physical memory address. The first printf statement in the for loop will print the vector variable, which is the starting 000, 001, 002, etc., numbers. The address variable will contain the 190a:0000, 0f9e:0004 addresses because the %p printf flag displays a pointer address.

In the second loop of the program, we're storing the number zero to the each and every address in the IVT vector table. We're zeroing the first ipAddr pointer to the interrupt service routine as well as the csAddr, which is the code segment register that actually contains the ISR. We're also printing the values as we go along.

Let's also print the memory segments with the "mem.exe /d /p" command. The results of running that command are presented on the picture below:

Let's also print all of the segment registers used by the IVT. I did this by including the following code in the first for loop of the program that pauses the printing every 20 lines, so we can read all the lines before proceeding to the execution. Otherwise, the whole table will be printed in one go, which will make some of the output pass by on our screen and we won't be able to read it.

[cpp]

if(vector % 20 == 0) {

scanf("%c", &amp;dummy);

}

[/cpp]

All of the values are presented on the pictures below:

I realize that this is quite a lot of pictures that we've presented. We didn't present all the values up to the 255 vector, because the rest of the values contain the CS as well as IP value of 0x0000, so there's no need to print those values as well.

Let's now summarize the segments that are used by the IVT table. The segments are presented below in incremental order:

  • 0000 : Interrupt Vector Table (IVT)
  • 0070 : System Data
  • 00C9 : System Data
  • 0207 : System Data
  • 0248 : System Data
  • 0C69 : System Data
  • 0D2A : System Program
  • 0D32 : Program
  • 0F93 : Program
  • C000 : Free
  • C90F : Free
  • F000 : Free

Conclusion

We've just seen the basic program that prints information from the IVT table and nulls the values in that table to crash the system. The program is a 16-bit executable that can be run in the MSDOS environment and has full access to the memory IVT table, because in real mode there are no protection mechanisms in place.

References:

Become a certified reverse engineer!

Become a certified reverse engineer!

Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst.

[1]: Bill Blunder, The Rootkit Arsenal: Escape and Evasion in the Dark Corners of the System.

Dejan Lukan
Dejan Lukan

Dejan Lukan is a security researcher for InfoSec Institute and penetration tester from Slovenia. He is very interested in finding new bugs in real world software products with source code analysis, fuzzing and reverse engineering. He also has a great passion for developing his own simple scripts for security related problems and learning about new hacking techniques. He knows a great deal about programming languages, as he can write in couple of dozen of them. His passion is also Antivirus bypassing techniques, malware research and operating systems, mainly Linux, Windows and BSD. He also has his own blog available here: http://www.proteansec.com/.