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