Counting Interrupts

 

r.c

#include <ntddk.h>

#include <stdio.h>

#define MAKELONG(a, b) ((unsigned long) (((unsigned short) (a)) | ((unsigned long) ((unsigned short) (b))) << 16))

unsigned long g_i_count[0xff+1],old_ISR_pointers[0xff+1];

//90 60 9c b8 00 00 00 00 50 9a f7 80 7b f7 08 00 58 9d 61 ea 50 f3 4d 80 08 00 90

//idt_detour_tablebase=88f2e000 count_interrupts=f77b80f7

//Count 0 old isr pointer=804df350

char jump_template[] = {

 0x90, //nop, debug

 0x60, //pushad

 0x9C, //pushfd

 0xB8, 0xAA, 0x00, 0x00, 0x00,    //mov eax, AAh

 0x50, //push eax

 0x9A, 0x11, 0x22, 0x33, 0x44, 0x08, 0x00, //call 08:44332211h

 0x58, //pop eax

 0x9D, //popfd

 0x61, //popad

 0xEA, 0x11, 0x22, 0x33, 0x44, 0x08, 0x00 //jmp 08:44332211h

};

unsigned char *idt_detour_tablebase;

#pragma pack(1)

struct IDTENTRY

{

unsigned short LowOffset , selector;

unsigned char unused_lo, segment_type:4,system_segment_flag:1,DPL:2,P:1;

unsigned short HiOffset;

} ;

struct IDTINFO

{

unsigned short IDTLimit;

unsigned short LowIDTbase;

unsigned short HiIDTbase;

};

#pragma pack()

VOID OnUnload(PDRIVER_OBJECT DriverObject )

{   

int i;

struct IDTINFO idt_info;

struct IDTENTRY *idt_entries;

char _t[255];

DbgPrint("ROOTKIT: OnUnload called\n");

__asm     sidt idt_info 

idt_entries = (struct IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);

for(i=0;i<=0xff;i++)

if (  g_i_count[i] )

DbgPrint("Interrupt %02x called %d times",i, g_i_count[i]);

for(i=0;i<=0xff;i++)

{

__asm cli

idt_entries[i].LowOffset = (unsigned short) old_ISR_pointers[i];

idt_entries[i].HiOffset = (unsigned short)((unsigned long)old_ISR_pointers[i] >> 16);

__asm sti

}

}

void __stdcall count_interrupts(unsigned long inumber)

{

unsigned long aNumber ,*aCountP ;

__asm mov eax, [ebp+0Ch]

__asm mov aNumber, eax

aNumber = aNumber & 0x000000FF;

aCountP = &g_i_count[aNumber];

InterlockedIncrement(aCountP);

}

int j;

NTSTATUS DriverEntry(PDRIVER_OBJECT d,PUNICODE_STRING r)

{

struct IDTINFO idt_info;

struct IDTENTRY *idt_entries,*i;

unsigned long count,addr;

d->DriverUnload  = OnUnload;

__asm sidt idt_info

DbgPrint("IDTLimit=%x LowIDTbase=%x HiIDTbase=%x",idt_info.IDTLimit,idt_info.LowIDTbase,idt_info.HiIDTbase);

idt_entries = (struct IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);

DbgPrint("idt_entries=%x %x", idt_entries , idt_info.HiIDTbase * 65536+ idt_info.LowIDTbase);

for(count=0;count <= 0xff;count++)

{

i = idt_entries + count;  // i = &idt_entries[count]

addr = MAKELONG(i->LowOffset, i->HiOffset);

DbgPrint("Interrupt %02x at %x LowOffset=%x HiOffset Selector=%x segment_type=%x segment_flag=%02x DPL=%x P=%d",count , addr ,

i->LowOffset, i->HiOffset, i->selector,i->segment_type,i->system_segment_flag,i->DPL,i->P);

old_ISR_pointers[count] = MAKELONG(idt_entries[count].LowOffset,idt_entries[count].HiOffset);

}

idt_detour_tablebase = ExAllocatePool(NonPagedPool,sizeof(jump_template)*256);

DbgPrint("idt_detour_tablebase=%x count_interrupts=%08x",idt_detour_tablebase,count_interrupts);

for(count=0;count<=0xff;count++)

{

int offset = 26*count; 

char *entry_ptr = idt_detour_tablebase + offset;

memcpy(entry_ptr, jump_template, 26);

entry_ptr[4] = (char)count;

*( (unsigned long *)(&entry_ptr[10]) ) = (unsigned long)count_interrupts;

*( (unsigned long *)(&entry_ptr[20]) ) = old_ISR_pointers[count];

__asm cli

idt_entries[count].LowOffset = (unsigned short)entry_ptr;

idt_entries[count].HiOffset = (unsigned short)((unsigned long)entry_ptr >> 16);

__asm sti

DbgPrint("offset=%d entry_ptr=%x idt_entries[count].LowOffset=%x

idt_entries[count].HiOffset=%x",offset,entry_ptr,idt_entries[count].LowOffset,idt_entries[count].HiOffset);

}

DbgPrint("%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x

%02x %02x %02x %02x",idt_detour_tablebase[0], idt_detour_tablebase[1],idt_detour_tablebase[2],

idt_detour_tablebase[3],idt_detour_tablebase[4], idt_detour_tablebase[5],idt_detour_tablebase[6],

idt_detour_tablebase[7],idt_detour_tablebase[8], idt_detour_tablebase[9],idt_detour_tablebase[10],

idt_detour_tablebase[11],idt_detour_tablebase[12], idt_detour_tablebase[13],idt_detour_tablebase[14],

idt_detour_tablebase[15],idt_detour_tablebase[16], idt_detour_tablebase[17],idt_detour_tablebase[18],

idt_detour_tablebase[19],idt_detour_tablebase[20], idt_detour_tablebase[21],idt_detour_tablebase[22],

idt_detour_tablebase[23], idt_detour_tablebase[24], idt_detour_tablebase[25], idt_detour_tablebase[26]);

DbgPrint("Count 0 old isr pointer=%08x",old_ISR_pointers[0]);

return STATUS_SUCCESS;

}

 

IDTLimit=7ff LowIDTbase=f400 HiIDTbase=8003

idt_entries=8003f400 8003f400

Interupt 00 at 804df350 LowOffset=f350 HiOffset Selector=804d segment_type=8 segment_flag=0e DPL=0 P=0

 

idt_detour_tablebase=88f09000 count_interrupts=b20a90c2

 

offset=0 entry_ptr=88f09000 idt_entries[count].LowOffset=9000 idt_entries[count].HiOffset=88f0

 

90 60 9c b8 00 00 00 00 50 9a c2 90 0a b2 08 00 58 9d 61 ea 50 f3 4d 80 08 00 90

Count 0 old isr pointer=804df350

 

The above program displays how many times a certain interrupt has been called. We call an interrupt using the int assembler command. This int 3 calls interrupt 3. As always we start with the DriverEntry method.

 

The code that you see above has been written by Greg Hoglund and the only value we have done is simply explained it in a different way.

 

We start by creating a structure idt_info that is of type IDTINFO. This structure has three shorts, a limit and a low and high offset. The problem with pointers is that we need 4 bytes and thus we use the two shorts for a pointer. The assembler instruction sidt  fills up an actual structure of 6 bytes with a limit field has a value 7ff and the address of where the idt table begins. In our case the low field has  a value f400 and the high a value 8003. Thus the idt table begins at 8003f400. The macro MAKELONG simply multiplies the high by 2 ^ 16 or 65536 or left shits it 16 bytes. The variable idt_entries tells us where in memory the interrupt structures start.

 

We want to loop through the 256 interrupts from 0 to 255. idt_enteries is a pointer to a structure that is 8 bytes large. These structures are present back to back. The variable idt_enteries points to the first structure and as count becomes 1 it points to the second and so forth. Each time we add 1 to idt_enteries we are actually increasing its value by 8. Thus I points to a different idt entry structure each time. The members LowOffset and HiOffset contain the address of the function that will be called to handle the interrupt. For some reason we do not have the pointer to the function called but two individual shorts that need to be converted to the actual pointer to the function. Thus variable addr contains the actual pointer to the original function. The segment_flag if it has value of 0x0e, the entry is a interrupt gate. We then store this original address of addr into an array old_ISR_pointers. Thus array 0 contains the original address of the function that will be called when interrupt 0 occurs and so on. We did not have to use the macro MAKELONG and could have used the addr variable instead. Thus we simply display all the ISR’s and also store their values in a array.

 

Now for the real work. We then use the DDK method ExAllocatePool which has been declared obsolete where we allocate fresh memory for 256 times 26 bytes. This function is another malloc where we first specify the type of memory and then the amount of memory. In our case the memory is allocated at 88f09000. Your mileage will vary, it will be a remarkable coincidence if you get a similar value. There are 6 pool types we can use and NonPAgePool memory comes from memory that cannot be paged to disk ever. This is scarce memory and must be used only when necessary. This memory can be accessed from any IRQL. Four of the above pool types are for internal use only  and the only other type that we can use is PAGEDPOOL which is pageble system memory. Thus variable idt_detour_tablebase now contains the address of memory which is 256 * 26 bytes large.

 

The offset variable starts at 0 and increase by 26 each time we enter the loop. Its values are therefore 0 , 26 52 etc. The variable entry_ptr starts at 88f09000 and increases by 26 each times. Lets now understand what this magic number 26 is all about. We have some standard code that we would like to be called each time an interrupts occurs. This code happens to be 26 bytes large and is in an array called jump_template. Thus the memory be allocated was for 256 such blocks of code that is 26 bytes large. The first thing we do is copy these 26 bytes of code at 88f09000 and then at 88f0901a. Thus we take these 26 bytes of code and copy them across our array 256 times. We have there 256 copies of this 26 byte code. The variable entry_ptr points to the start of this code each time. We first put the number 0, 1 2 etc at the 4 byte of the start of the code. If we look at the code in the  array jump_template, it starts with a nop 90, a push all registers and push flags. Then we mov a value into the eax register. In the code we move a fixed value 0xaa which we replace by the current interrupt number 0, 1 2 etc.

 

 

We then need to place the address of our method at offset 10 that we want to be called each time any interrupt occurs, This method is called count_interrupts and in our it address is b20a90c2. We want this address to be placed at bytes 10 to 13 from the start. We then place the address of the original ISR stored in the old_ISR_pointers array at location 20 to 23 from the start. We then do the last act which is place this address of these 26 bytes of code into idt_enteries which is what the processor uses to figure which method to call. Thus each time an interrupt occurs the processor calls one of these 256 26 byte code arrays which in turn calls the method count_interrupt and then calls the original code. The opcode 0x9a is a 6 byte call and the opcode 0xea is a 6 byte jump. In the method count_interrupts we have on the stack a return value which is 8 bytes and hence the first parameter is 12 bytes from ebp. Actually the ret value normally is 4 bytes followed by ebp and thus the first parameter should by 8 bytes from the start of ebp.

 

We move this value 12 bytes from ebp into eax and then w e move this into the variable aNumber. This value is the number of the interrupt that gets called. The processor does not put this value on the stack, our 26 bytes of code is what created this magic. For some reason we mask off all the bits but the first 8. We then get a pointer to the array with aNumber as the offset. The InterlockedIncrement then increments the memory by 1. Thus each time an interrupt occurs we are adding 1 to the corresponding global array g_I_count. When we unload the driver, we do two things one we display this global array and then we also change the idt_enteries to point to what they were before our driver came along. These original values we stored in the array old_ISR_pointers.

 

The only reason we use the InterlockedIncrement method is because it increments a variable whose address we pass as an atomic operation. Good code written by experts do not take chances and if we increment the array ourselves it may not get done atomically before another interrupt occurs. If we look at the 26 bytes of code, byte 5 contains 0 as this is code for interrupt 0. Byte 5 will contain for the next interrupt and so on. The method count_interrupts starts b20a90c2 and this bytes 10 to 14 contain c2 90 0a b2. Finally the original ISR is at 804df350 and hence bytes 20 to 23 contain 50 f3 4d 80. The address are little endian and in the reverse order.

 

The initial and final nop are there for good luck. The sidt opcode is used by operating systems only and as the rest of the world do not want to know how interrupts get handled by the system.

 

Thus in our code we are simply pointing the ISR tour 26 bytes code where we place the interrupt number on the stack and then call the method count_interrupts. This method increments an array member by 1 and then calls the original method by jumping to it. The count_interrupts method is stdcall so that it cleans up the stack when it gets over. Otherwise we would have to write code in our 26 bytes to set the stack back up by 4. There is a push eax but no pop eax after the call to count_interrupts.

 

The pushfd pushes the EFLAGS register on the stack and moves the stack down by 4. The D stands fro Double. The pushad pushes the registers in the following order Push EAX, ECX, EDX, EBX, original ESP, EBP, ESI, and EDI.

 

The pragma pack preprocessor directive simply decides how much memory to leave as padding. Pragma pack(1) is for no padding.

 

Intel processors have 4 types of gates, interrupt, call , task and trap. The interrupt and trap gates do the same thing, they take care of interrupts from hardware and the int instruction. The call gate allows us to execute code in a lower ring and the task is meant for a task switch. The P bit is set tells us that this segment or the method that will be called to handle the interrupt is in memory.

 

The DPL or the Descriptor Privilege can be either 0 or 3. In our case all point to ring 0 code. The segment type 0 means not used, 5 is a task gate. Interrupts that we deal with are not the interrupts of dos which happened in real mode, all our interrupts in windows are protected mode.

 

This IDT unlike the dos one can be anywhere in memory and hence we have the sidt instruction that tells us where in memory this table resides.

 

!idt windbg