Abstract This chapter covers how
operating systems use software interrupts, why
software interrupts need hooking, and how to hook
software interrupts. An example hooks INT 2E (the
system service interrupt) in Windows NT.
WHAT ARE
INTERRUPTS?
An interrupt
refers to a mechanism that breaks into the normal
execution of an application program and transfers
control to operating system code. There are three
kinds of interrupts: hardware interrupts, software
interrupts, and exceptions.
Hardware
interrupts come from the physical devices in the
machine. For example, whenever there is a
character waiting on the COM port, a hardware
interrupt will be triggered. When an I/O operation
completes, a hardware interrupt also will be
triggered.
Software interrupts occur as a
result of an explicit INT nn request from
the application. Applications typically use this
mechanism to get different services from the
operating system. Exceptions occur as a result of
an application’s attempt to perform illegal
operations, such as dividing by zero.
The
next sections detail how processors handle
software interrupts in real, protected, and V86
modes.
Interrupt
Processing in Real Mode In real mode,
the lower 1K of memory holds a data structure
known as the Interrupt Vector Table (IVT). There
are nominally 256 entries in this table. (Since
the 80286, the IVT is not required to have 256
entries or start at physical address 0. The base
and address and length of the IVT are determined
by looking at the Interrupt Descriptor Table
Register.) Each entry contains a far pointer to an
Interrupt Service Routine. Any type of interrupt
routes to the appropriate Interrupt Service
Routine through this table. The processor indexes
the interrupt number in this table; pushes current
CS, IP, and flags on the stack; and calls the far
pointer specified in the IVT. The handler
processes the interrupt and then executes an IRET
instruction to return control to the place where
the processor executed at the time of the
interrupt.
Interrupt Processing in
Protected Mode In protected mode,
interrupts are handled in a similar way as real
mode. The Interrupt Descriptor Table (IDT) does
what the IVT does in real mode. IDT consists of an
array of 8-byte segment descriptors called gates.
The Interrupt Descriptor Table Register (IDTR)
holds the base address and the limit of IDT. The
IDT must exist in physical memory and should never
swap out to virtual memory. This is because if an
interrupt were to occur while the IDT were swapped
out, the processor would generate an exception,
requiring the IDT to get the handler for handling
this exception, and so on until the system
crashed. The gates in the IDT can consist of three
types: interrupt gates, trap gates, and task
gates. We won’t dwell on the details of the trap
and task gates. For further information, refer to
Intel processor documentation.
Interrupt
gates interest us. The important fields of
interrupt gates include the code segment selector
and the offset of the code for execution for this
interrupt, as well as the privilege level of the
interrupt descriptor. The interrupt processing
closely resembles that in real mode. When the
interrupt occurs, the processor indexes the
interrupt number in IDT, pushes EFLAGS, CS, and
EIP onto the stack, and calls the handler
specified in the IDT. When the handler finishes
executing, it should execute the IRET instruction
to return control. Depending upon the type of
interrupt, an error code may be pushed on the
stack. The handler must clear this error code from
the stack. The DPL field in the interrupt gate
controls the software interrupts. The current
privilege level must be at least as privileged as
DPL to call these software interrupts. If not,
then a General Protection Fault is triggered. This
protection feature permits the operating system to
reserve certain software interrupts for its own
use. Hardware interrupts and exceptions process
without regard to the current privilege
level.
Interrupt
Processing in V86 Mode In V86 mode, any
INT nn instruction causes a General
Protection Fault. Windows NT uses this to map INT
21h calls made from an MS-DOS application to Win32
API calls. This mapping occurs as part of a GPF
handler for Windows NT. Other types of interrupts
are handled similarly to those in protected
mode.
HOW OPERATING SYSTEMS USE
SOFTWARE INTERRUPTS
MS-DOS uses INT
21 to provide core system services to the
applications. Other software interrupts are also
provided, such as multiplex interrupt 2F.
Applications fill in the parameters in various
registers and execute the INT nn
instruction to access these services from the
operating system. Various compiler libraries
provide wrappers around these interrupt interfaces
and provide useful C functions, such as _open,
_read, _write, and others.
Not much changes
in the way software interrupts are used in Windows
95/98 and Windows NT. Windows NT provides
user-callable software interrupts. The following
table lists the important software interrupts
provided.
TABLE
9-1 WINDOWS SOFTWARE INTERRUPTS
WHY SOFTWARE
INTERRUPTS NEED TO BE
HOOKED
Software interrupts need to
be hooked for several reasons. One reason is to
change the behavior of the system services
exported by the operating system. By hooking the
software interrupts, you can write monitoring
applications. Hooking can prove useful in studying
operating system internals. This can also serve as
a way to hook system services, although the
mechanism discussed in Chapter 6 provides a better
way of doing that.
MS-DOS provides system
services to hook software interrupts by means of
INT 21h, and functions 25h and 35h. Compiler
libraries provide wrapper functions such as
_dos_getvect and _dos_setvect to hook software
interrupts. Windows 95 provides a mechanism to
hook software interrupts by means of
Set_PM_Int_Vector and Hook_V86_Int_Chain VxD
services. However, Windows NT does not officially
support any way to hook software interrupts. The
DDK does provide functions such as
HalGetInterruptVector() and IoConnectInterrupt()
to hook hardware interrupts. Once we understand
Intel data structures such as IDT and interrupt
gates, we can easily hook software interrupts in
Windows NT. Hooking software interrupts basically
amounts to changing the code selector and offset
fields in the Interrupt Gate Descriptor. However,
this certainly becomes a platform-dependent
situation. It will work only on an Intel
implementation of Windows NT.
You can apply
the same technique for hooking software interrupts
to hook hardware interrupts or exceptions although
you should use the documented IoConnectInterrupt()
function to hook hardware interrupts. You have to
write an interrupt handler keeping in mind the
type of interrupt it is hooking into because the
stack frame might differ in various situations.
The new interrupt handler must be written in
Assembly language because of the restrictions
imposed by 32-bit compilers.
HOW TO HOOK
SOFTWARE INTERRUPTS
As we already
discussed, the two Intel data structures–IDTR and
Interrupt Gate Descriptor–play crucial roles in
interrupt processing. You can discover the
contents of IDTR with the sidt Assembly
instruction. This instruction places the base and
limit of IDT in a 6-byte location specified by the
operand. Once you get the base address of IDT, you
can index the interrupt number you want to hook in
this table and change the code selector and offset
specified. Before doing this, you must save the
old code selector and offset. Also, your new
handler should ensure that the interrupt is
chained properly to the old handler, meaning the
new handler should maintain the state of registers
and stack in such a way that the old handler
should be called as if it were directly called by
the processor through the IDT.
The sample
application that we write in this chapter hooks
INT 2Eh (System Service Interrupt) and maintains
the counters of how many times a particular system
service was called. The sample maintains only the
counter of system services provided by
NTOSKRNL.EXE. The user-level application issues
DeviceIoControl to this driver to obtain the
statistics about the service usage. As we already
saw in Chapter 7, there are a total of 0xC4 system
services in NT 3.51, 0xD3 services in NT 4.0, and
0xF4 services in Windows 2000 provided by
NTOSKRNL.EXE. This sample works on all versions of
Windows NT to date. HOOKINT.C
#include "ntddk.h"
#include "stdarg.h"
#include "stdio.h"
#include "Hookint.h"
#define TEST_PAGING
#define DRIVER_SOURCE
#include "..\..\include\intel.h"
#include "..\..\include\wintype.h"
#include "..\..\include\undocnt.h"
/* Interrupt to be hooked */
#define HOOKINT 0x2E
int OldHandler;
ULONG *ServiceCounterTable;
ULONG ServiceCounterTableSize;
int NumberOfServices;
#ifdef TEST_PAGING
void *PagedData;
#endif
extern void _cdecl NewHandler();
/* Buffer to store result of sidt instruction */
char buffer[6];
/* Pointer to structure to identify the limit and
* base of IDTR
*/
PIdtr_t Idtr=(PIdtr_t)buffer;
#pragma pack()
void NewHandlerCFunc(int ServiceId)
{
if (ServiceId>NumberOfServices)
return;
#ifdef TEST_PAGING
memset(PagedData, 0, 100000);
#endif
ServiceCounterTable[ServiceId+1]++;
return;
}
NTSTATUS DriverSpecificInitialization()
{
PIdtEntry_t IdtEntry;
extern PServiceDescriptorTableEntry_t
KeServiceDescriptorTable;
NumberOfServices =
KeServiceDescriptorTable->NumberOfServices;
ServiceCounterTableSize =
(NumberOfServices+1)*sizeof(int);
ServiceCounterTable = ExAllocatePool(PagedPool,
ServiceCounterTableSize);
if (!ServiceCounterTable) {
return STATUS_INSUFFICIENT_RESOURCES;
}
#ifdef TEST_PAGING
PagedData=ExAllocatePool(PagedPool, 100000);
if (!PagedData) {
ExFreePool(ServiceCounterTable);
return STATUS_INSUFFICIENT_RESOURCES;
}
#endif
memset(ServiceCounterTable,0,
ServiceCounterTableSize);
*ServiceCounterTable=NumberOfServices;
trace(("NumberOfServices=%x, "
"ServiceCounterTableSize=%x, @%x\n",
NumberOfServices, ServiceCounterTableSize,
ServiceCounterTable));
/* Get the Base and Limit of IDTR Register */
_asm sidt buffer
IdtEntry=(PIdtEntry_t)Idtr->Base;
/* Index the interrupt number to be hooked specified
by "HOOKINT define" in * appropriate IDT entry, extract and save
away the Old
* handler’s address
*/
OldHandler =
((unsigned int)IdtEntry[HOOKINT].OffsetHigh<<16U)|
(IdtEntry[HOOKINT].OffsetLow);
/* Plug into the interrupt by changing the offset
* field to point to NewHandler function
*/
_asm cli
IdtEntry[HOOKINT].OffsetLow =
(unsigned short)NewHandler;
IdtEntry[HOOKINT].OffsetHigh =
(unsigned short)((unsigned int)NewHandler>16);
_asm sti
return STATUS_SUCCESS;
}
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
MYDRIVERENTRY(L"hookint",
FILE_DEVICE_HOOKINT,
DriverSpecificInitialization());
return ntStatus;
}
NTSTATUS
DriverDispatch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PIO_STACK_LOCATION irpStack;
PVOID ioBuffer;
ULONG inputBufferLength;
ULONG outputBufferLength;
ULONG ioControlCode;
NTSTATUS ntStatus;
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
irpStack = IoGetCurrentIrpStackLocation (Irp);
ioBuffer = Irp->AssociatedIrp.SystemBuffer;
inputBufferLength = irpStack->Parameters.
DeviceIoControl.InputBufferLength;
outputBufferLength = irpStack->Parameters.
DeviceIoControl.OutputBufferLength;
switch (irpStack->MajorFunction)
{
case IRP_MJ_DEVICE_CONTROL:
trace(("HOOKINT.SYS: IRP_MJ_DEVICE_CONTROL\n"));
ioControlCode = irpStack->Parameters.
DeviceIoControl.IoControlCode;
switch (ioControlCode)
{
case IOCTL_HOOKINT_SYSTEM_SERVICE_USAGE:
{
int i;
/* Check if sufficient sized buffer is
* provided to hold the counters for system
* service usage
*/
if (outputBufferLength >=
ServiceCounterTableSize) {
/* Output the counters describing the
* system service usage
*/
trace((for (i=1;
i<=NumberOfServices;
i++)
DbgPrint("%x ",
ServiceCounterTable[i])));
trace((DbgPrint("\n")));
/* Copy the counter information in the user
* supplied buffer
*/
memcpy(ioBuffer, ServiceCounterTable,
ServiceCounterTableSize);
/* Fill in the number of bytes to be
* returned to the caller
*/
Irp->IoStatus.Information =
ServiceCounterTableSize;
} else {
Irp->IoStatus.Status =
STATUS_INSUFFICIENT_RESOURCES;
}
break;
}
default:
Irp->IoStatus.Status =
STATUS_INVALID_PARAMETER;
trace(("HOOKINT.SYS: unknown "
"IRP_MJ_DEVICE_CONTROL\n"));
break;
}
break;
}
ntStatus = Irp->IoStatus.Status;
IoCompleteRequest (Irp,IO_NO_INCREMENT);
return ntStatus;
}
VOID
DriverUnload(
IN PDRIVER_OBJECT DriverObject
)
{
WCHAR deviceLinkBuffer[]=L"\\DosDevices\\hookint";
UNICODE_STRING deviceLinkUnicodeString;
PIdtEntry_t IdtEntry;
ExFreePool(ServiceCounterTable);
#ifdef TEST_PAGING
ExFreePool(PagedData);
#endif
/* Reach to IDT */
IdtEntry=(PIdtEntry_t)Idtr->Base;
/* Unplug the interrupt by replacing the offset
* field in the Interrupt Gate Descriptor by the
* old handler address.
*/
_asm cli
IdtEntry[HOOKINT].OffsetLow =
(unsigned short)OldHandler;
IdtEntry[HOOKINT].OffsetHigh =
(unsigned short)((unsigned int)OldHandler>16);
_asm sti
RtlInitUnicodeString (&deviceLinkUnicodeString,
deviceLinkBuffer
);
IoDeleteSymbolicLink (&deviceLinkUnicodeString);
IoDeleteDevice (DriverObject->DeviceObject);
trace(("HOOKINT.SYS: unloading\n"));
}
HANDLER.ASM
.386
.model small
.code
include ..\..\include\undocnt.inc
public _NewHandler
extrn _OldHandler:near
extrn _NewHandlerCFunc@4:near
_NewHandler proc near
Ring0Prolog
STI
push eax
call _NewHandlerCFunc@4
CLI
Ring0Epilog
jmp dword ptr cs:[_OldHandler]
_NewHandler endp
END
SUMMARY
In this
chapter, we discussed interrupt processing in
various modes of Intel processors. Then, we saw
how the operating system makes use of interrupts.
Next, we discussed the need for hooking software
interrupts. We also explored a mechanism for
hooking software interrupts. We concluded the
chapter with an example that hooks Int 2E (the
system service interrupt) in Windows N.
|