Been busy with work and such lately, but I took the time to put this code together. This implementation in no way represents a new concept as IDT (Interrupt Descriptor Table) hooking has been around for quite some time, but the code is clean and is a good example for those of you who are unfamiliar with the practice.
The IDT is a table that has entries referencing service routines associated with interrupts. A system can have up too 0xFF (256) such interrupts and routines, but in general most systems use less. The IDT is reponsible for handling software interrupts, hardware interrupts, and exceptions. This means that the IDT handles issues ranging from a device polling for attention to a divide by zero exception. Here are two articles to check out for more information about the IDT, Bran’s Kernel Dev on IDT and IDT Wiki.
The code is fairly straightforward, but here is a brief run down of how it works. First we attain a descriptor that will give us a virtual address referencing the IDT. We also save the current state of the IDT so that we can restore it at a later time (Note that the assembly instruction sidt simply loads a pointer to the IDT into a memory location). From here we are free to set hooks using hook_function and then remove hooks using unhook_function. Hooks are applied using a small section of assembly instructions which masks interrupts, applies the hooks, and then unmasks interrupts. There is also a function called enumerate_idt_entries that will output interrupt numbers and associated addresses for the service routines. When the driver is stopped it will also attempt to restore the IDT to the way it was when the driver first loaded.
Here is the code with some example usage.
// IDT Hooking/Unhooking Module
// Creative Commons - Aaron Burrow
#include "ntddk.h"
#pragma pack(1)
// Took these structures from Bran's Kernel Dev Tutorial
typedef struct _idt_entry
{
USHORT base_lo;
USHORT sel;
UCHAR always0;
UCHAR flags;
USHORT base_hi;
} idt_entry, *pidt_entry;
typedef struct _idt_ptr
{
USHORT limit;
ULONG base;
} idt_ptr, *pidt_ptr;
VOID driver_unload(IN DRIVER_OBJECT* driver_object);
PVOID make_idt_writable(PMDL* mdl);
NTSTATUS save_original_idt(VOID);
VOID enumerate_idt_entries(VOID);
NTSTATUS hook_interrupt(PVOID writable_idt, ULONG interrupt_number, PVOID replacement_address, PVOID* old_address);
NTSTATUS unhook_interrupt(PVOID writable_idt, ULONG interrupt_number);
VOID cleanup_idt_memory(PVOID writable_idt, PMDL* mdl);
// Global Variables
// a prefix of '_' indicates a global variable that shouldn't be directly modified
pidt_entry _original_idt_vectors;
PVOID reg_call0x70;
PVOID reg_call0x71;
// Example Routines use in Hooks
__declspec(naked) hook_call0x70()
{
__asm {
jmp reg_call0x70;
};
}
__declspec(naked) hook_call0x71()
{
__asm {
jmp reg_call0x71;
};
}
NTSTATUS DriverEntry(IN DRIVER_OBJECT* driver_object, IN UNICODE_STRING* registry_path)
{
PMDL mdl = NULL;
PVOID old_address, writable_idt;
driver_object->DriverUnload = driver_unload;
if ((writable_idt = make_idt_writable(&mdl))) {
reg_call0x70 = ((ULONG)_original_idt_vectors[0x70].base_hi << 16) | _original_idt_vectors[0x70].base_lo;
reg_call0x71 = ((ULONG)_original_idt_vectors[0x71].base_hi << 16) | _original_idt_vectors[0x71].base_lo;
enumerate_idt_entries();
if (hook_interrupt(writable_idt, 0x70, (PVOID)&hook_call0x70, &old_address) == STATUS_SUCCESS) {
if (hook_interrupt(writable_idt, 0x71, (PVOID)&hook_call0x71, &old_address) == STATUS_SUCCESS) {
enumerate_idt_entries();
unhook_interrupt(writable_idt, 0x70);
unhook_interrupt(writable_idt, 0x71);
cleanup_idt_memory(writable_idt, &mdl);
return STATUS_SUCCESS;
}
}
}
return STATUS_UNSUCCESSFUL;
}
/*
Get a virtual address that allows us to write to the IDT
On success the caller is responsible for calling cleanup_idt_memory
@mdl: The memory descriptor for the addresses referencing the IDT
return: NULL on failure, address to IDT on success
*/
PVOID make_idt_writable(PMDL* mdl)
{
idt_ptr p_idt;
*mdl = NULL;
if (save_original_idt() != STATUS_SUCCESS)
return NULL;
// Get a pointer to the IDT
__asm {
sidt p_idt
};
if (!(*mdl = IoAllocateMdl((PVOID)p_idt.base, p_idt.limit, FALSE, FALSE, NULL)))
return NULL;
MmBuildMdlForNonPagedPool(*mdl);
(*mdl)->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
// Causes a bug check on failure
return MmMapLockedPages(*mdl, KernelMode);
}
/*
Cleanup all of the resources that have been allocated.
return: None
*/
VOID cleanup_idt_memory(PVOID writable_idt, PMDL* mdl)
{
MmUnmapLockedPages(writable_idt, *mdl);
IoFreeMdl(*mdl);
ExFreePoolWithTag(_original_idt_vectors, 'idt');
return;
}
/*
Displays information about the current IDT
return: None
*/
VOID enumerate_idt_entries(VOID)
{
USHORT i;
idt_ptr p_idt;
pidt_entry idt_vectors;
// Get a pointer to the IDT
__asm {
sidt p_idt
};
idt_vectors = (pidt_entry)p_idt.base;
for (i = 0; i < p_idt.limit/sizeof(idt_entry); ++i)
DbgPrint("Interrupt #: %X Base Lo: %.4X Base High: %.4X\n", i, idt_vectors[i].base_lo, idt_vectors[i].base_hi);
return;
}
/*
Make a copy of the original IDT so that it can be restored later on
return: STATUS_UNSUCCESSFUL on failure, STATUS_SUCCESS on success
*/
NTSTATUS save_original_idt(VOID)
{
idt_ptr p_idt;
// Get a pointer to the IDT
__asm {
sidt p_idt
};
if (!(_original_idt_vectors = ExAllocatePoolWithTag(PagedPool, p_idt.limit, 'idt')))
return STATUS_UNSUCCESSFUL;
RtlCopyMemory((PVOID)_original_idt_vectors, (PVOID)(p_idt.base), p_idt.limit);
return STATUS_SUCCESS;
}
/*
Applies a hook to the specified interrupt
A point to be made is that the function employes a caching mechanism which allows the caller
to pass NULL for writable_idt and the function will use the last non-NULL address passed
as that parameter. Obviously there needs to originally be a valid address passed for the
mechanism to work.
@writable_idt: The virtual address to the IDT
@interrupt_number: The number/index to the interrupt to be hooked
@replacement_address: Address to the code to be executed instead of the interrupt service routine
@old_address: Gives the caller the former address associated with the interrupt
return: STATUS_SUCCESSFUL on failure, STATUS_SUCCESS on success
*/
NTSTATUS hook_interrupt(PVOID writable_idt, ULONG interrupt_number, PVOID replacement_address, PVOID* old_address)
{
idt_ptr p_idt;
pidt_entry idt_vectors, current_idt_entry;
static PVOID s_writable_idt;
// Get a pointer to the IDT
__asm {
sidt p_idt
};
if (writable_idt) {
// Cache the last non-Null idt virtual address in s_writable_idt
s_writable_idt = writable_idt;
}
idt_vectors = (pidt_entry)s_writable_idt;
if (!writable_idt)
return STATUS_UNSUCCESSFUL;
// Check if the interrupt number is out of range
if (interrupt_number > p_idt.limit/sizeof(idt_entry))
return STATUS_UNSUCCESSFUL;
/*
The following assembly code is the same thing as this
C code, except they force the processor to ignore
and then acknowledge interrupts.
idt_vectors[interrupt_number].base_lo = replacement_address & 0xFFFF;
idt_vectors[interrupt_number].base_hi = (replacement_address & 0xFFFF0000) >> 16;
*/
// Get the address to the idt entry we are dealing with
current_idt_entry = &(idt_vectors[interrupt_number]);
// Give the caller the old address as they'll probably need it
*old_address = ((ULONG)current_idt_entry->base_hi << 16) | current_idt_entry->base_lo;
__asm {
cli // mask interrupts
mov eax, replacement_address
mov ebx, current_idt_entry
mov [ebx], ax // .base_lo offset = 0x0
shr eax, 16
mov [ebx + 0x6], ax // .base_hi offset = 0x6
sti // acknowledge interrupts
};
return STATUS_SUCCESS;
}
/*
Restores an interrupt to it's original state.
Employes the same caching mechanism as unhook_interrupt, addresses
are shared between the functions, so a valid one must be pased
to only one or the other
@writable_idt: Virtual address of the IDT
@interrupt_number: Index/Number of the IDT to be unhooked
*/
NTSTATUS unhook_interrupt(PVOID writable_idt, ULONG interrupt_number)
{
idt_ptr p_idt;
PVOID orig_addr, temp_addr;
// Get a pointer to the IDT
__asm {
sidt p_idt
};
// Check if the interrupt number is out of range
if (interrupt_number > p_idt.limit/sizeof(idt_entry))
return STATUS_UNSUCCESSFUL;
// Make sure memory has actually been allocated for _original_idt_vectors
if (!(_original_idt_vectors))
return STATUS_UNSUCCESSFUL;
orig_addr = ((ULONG)_original_idt_vectors[interrupt_number].base_hi << 16) | _original_idt_vectors[interrupt_number].base_lo;
return hook_interrupt(writable_idt, interrupt_number, orig_addr, &temp_addr);
}
/*
This function will unhook all interrupts
@driver_object: The driver object
return: None
*/
VOID driver_unload(DRIVER_OBJECT* driver_object)
{
PMDL mdl = NULL;
USHORT i;
idt_ptr p_idt;
PVOID writable_idt;
// Get a pointer to the IDT
__asm {
sidt p_idt
};
if ((writable_idt = make_idt_writable(&mdl))) {
for (i = 0; i < p_idt.limit/sizeof(idt_entry); ++i) {
unhook_interrupt(NULL, i);
}
cleanup_idt_memory(writable_idt, &mdl);
}
return;
}
This output represents the changes in the IDT (from unhooked, to hooked, to unhooked again) that occur on my system when running that code.
Interrupt #: 70 Base Lo: D3C0 Base High: 81C4
Interrupt #: 71 Base Lo: D3CA Base High: 81C4
Interrupt #: 70 Base Lo: 1010 Base High: AC20
Interrupt #: 71 Base Lo: 1020 Base High: AC20
Interrupt #: 70 Base Lo: D3C0 Base High: 81C4
Interrupt #: 71 Base Lo: D3CA Base High: 81C4
Until later.