_Direct Port I/O and Windows NT_ by Dale Roberts Listing One /****************************************************************************** TOTALIO.SYS -- by Dale Roberts Compile: Use DDK BUILD facility Purpose: Give direct port I/O access to the whole system. This driver grants total system-wide I/O access to all applications. Very dangerous, but useful for short tests. Note that no test application is required. Just use control panel or "net start totalio" to start the device driver. When the driver is stopped, total I/O is removed. Because no Win32 app needs to communicate with the driver, we don't have to create a device object. So we have a tiny driver here. Since we can safely extend the TSS only to the end of the physical memory page in which it lies, the I/O access is granted only up to port 0xf00. Accesses beyond this port address will still generate exceptions. ******************************************************************************/ #include /* Make sure our structure is packed properly, on byte boundary, not * on the default doubleword boundary. */ #pragma pack(push,1) /* Structures for manipulating the GDT register and a GDT segment * descriptor entry. Documented in Intel processor handbooks. */ typedef struct { unsigned short limit; GDTENT *base; } GDTREG; typedef struct { unsigned limit : 16; unsigned baselo : 16; unsigned basemid : 8; unsigned type : 4; unsigned system : 1; unsigned dpl : 2; unsigned present : 1; unsigned limithi : 4; unsigned available : 1; unsigned zero : 1; unsigned size : 1; unsigned granularity : 1; unsigned basehi : 8; } GDTENT; #pragma pack(pop) /* This is the lowest level for setting the TSS segment descriptor limit field. * We get the selector ID from the STR instruction, index into the GDT, and * poke in the new limit. In order for the new limit to take effect, we must * then read the task segment selector back into the task register (TR). */ void SetTSSLimit(int size) { GDTREG gdtreg; GDTENT *g; short TaskSeg; _asm cli; // don't get interrupted! _asm sgdt gdtreg; // get GDT address _asm str TaskSeg; // get TSS selector index g = gdtreg.base + (TaskSeg >> 3); // get ptr to TSS descriptor g->limit = size; // modify TSS segment limit // // MUST set selector type field to 9, to indicate the task is // NOT BUSY. Otherwise the LTR instruction causes a fault. // g->type = 9; // mark TSS as "not busy" // We must do a load of the Task register, else the processor // never sees the new TSS selector limit. _asm ltr TaskSeg; // reload task register (TR) _asm sti; // let interrupts continue } /* This routine gives total I/O access across the whole system. It does this * by modifying the limit of the TSS segment by direct modification of the TSS * descriptor entry in the GDT. This descriptor is set up just once at system * init time. Once we modify it, it stays untouched across all processes. */ void GiveTotalIO(void) { SetTSSLimit(0x20ab + 0xf00); } /* This returns the TSS segment to its normal size of 0x20ab, which * is two less than the default I/O map base address of 0x20ad. */ void RemoveTotalIO(void) { SetTSSLimit(0x20ab); } /****** Release all memory 'n' stuff. *******/ VOID TotalIOdrvUnload( IN PDRIVER_OBJECT DriverObject ) { RemoveTotalIO(); } /****** Entry routine. Set everything up. *****/ NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { DriverObject->DriverUnload = TotalIOdrvUnload; GiveTotalIO(); return STATUS_SUCCESS; } Listing Two /***************************************************************************** This code fragment illustrates the unsuccessful attempt to directly modify the IOPM base address. This code would appear in a kernel-mode device driver. Refer to the GIVEIO.C listing for a complete device driver example. ******************************************************************************/ /* Make sure our structure is packed properly, on byte boundary, not * on the default doubleword boundary. */ #pragma pack(push,1) /* Structure of a GDT (global descriptor table) entry; from processor manual.*/ typedef struct { unsigned limit : 16; unsigned baselo : 16; unsigned basemid : 8; unsigned type : 4; unsigned system : 1; unsigned dpl : 2; unsigned present : 1; unsigned limithi : 4; unsigned available : 1; unsigned zero : 1; unsigned size : 1; unsigned granularity : 1; unsigned basehi : 8; } GDTENT; /* Structure of the 48 bits of the GDT register that are stored * by the SGDT instruction. */ typedef struct { unsigned short limit; GDTENT *base; } GDTREG; #pragma pack(pop) /* This code demonstrates the brute force approach to modifying the IOPM base. * The IOPM base is stored as a two byte integer at offset 0x66 within the TSS, * as documented in the processor manual. In Windows NT, the IOPM is stored * within the TSS starting at offset 0x88, and going for 0x2004 bytes. This is * not documented anywhere, and was determined by inspection. The code here * puts some 0's into the IOPM so that we can try to access some I/O ports, * then modifies the IOPM base address. This code is unsuccessful because NT * overwrites the IOPM base on each process switch. */ void GiveIO() { GDTREG gdtreg; GDTENT *g; short TaskSeg; char *TSSbase; int i; _asm str TaskSeg; // get the TSS selector _asm sgdt gdtreg; // get the GDT address g = gdtreg.base + (TaskSeg >> 3); // get the TSS descriptor // get the TSS address TSSbase = (PVOID)(g->baselo | (g->basemid << 16) | (g->basehi << 24)); for(i=0; i < 16; ++i) // poke some 0's into the TSSbase[0x88 + i] = 0; // IOPM *((USHORT *)(TSSbase + 0x66)) = 0x88; } Listing Three /* From inpection of the TSS we know that NT's default IOPM offset is 0x20AD. * From an inspection of a dump of a process structure, we can find the bytes * 'AD 20' at offset 0x30. This is where NT stores the IOPM offset for each * process, so that I/O access can be granted on a process-by-process basis. * This portion of the process structure is not documented in the DDK. * This kernel mode driver fragment illustrates the brute force * method of poking the IOPM base into the process structure. */ void GiveIO() { char *CurProc; CurProc = IoGetCurrentProcess(); *((USHORT *)(CurProc + 0x30)) = 0x88; } Listing Four /********************************************************************* GIVEIO.SYS -- by Dale Roberts Compile: Use DDK BUILD facility Purpose: Give direct port I/O access to a user mode process. *********************************************************************/ #include #include /* The name of our device driver. */ #define DEVICE_NAME_STRING L"giveio" /* This is the "structure" of the IOPM. It is just a simple character array * of length 0x2000. This holds 8K * 8 bits -> 64K bits of the IOPM, which * maps the entire 64K I/O space of the x86 processor. Any 0 bits will give * access to the corresponding port for user mode processes. Any 1 * bits will disallow I/O access to the corresponding port. */ #define IOPM_SIZE 0x2000 typedef UCHAR IOPM[IOPM_SIZE]; /* This will hold simply an array of 0's which will be copied into our actual * IOPM in the TSS by Ke386SetIoAccessMap(). The memory is allocated at * driver load time. */ IOPM *IOPM_local = 0; /* These are the two undocumented calls that we will use to give the calling * process I/O access. Ke386IoSetAccessMap() copies the passed map to the TSS. * Ke386IoSetAccessProcess() adjusts the IOPM offset pointer so that the newly * copied map is actually used. Otherwise, the IOPM offset points beyond the * end of the TSS segment limit, causing any I/O access by the user-mode * process to generate an exception. */ void Ke386SetIoAccessMap(int, IOPM *); void Ke386QueryIoAccessMap(int, IOPM *); void Ke386IoSetAccessProcess(PEPROCESS, int); /***** Release any allocated objects. ******/ VOID GiveioUnload(IN PDRIVER_OBJECT DriverObject) { WCHAR DOSNameBuffer[] = L"\\DosDevices\\" DEVICE_NAME_STRING; UNICODE_STRING uniDOSString; if(IOPM_local) MmFreeNonCachedMemory(IOPM_local, sizeof(IOPM)); RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); IoDeleteSymbolicLink (&uniDOSString); IoDeleteDevice(DriverObject->DeviceObject); } /***************************************************************************** Set the IOPM (I/O permission map) of the calling process so that it is given full I/O access. Our IOPM_local[] array is all zeros, so IOPM will be all 0s. If OnFlag is 1, process is given I/O access. If it is 0, access is removed. ******************************************************************************/ VOID SetIOPermissionMap(int OnFlag) { Ke386IoSetAccessProcess(PsGetCurrentProcess(), OnFlag); Ke386SetIoAccessMap(1, IOPM_local); } void GiveIO(void) { SetIOPermissionMap(1); } /****************************************************************************** Service handler for a CreateFile() user mode call. This routine is entered in the driver object function call table by DriverEntry(). When the user-mode application calls CreateFile(), this routine gets called while still in the context of the user-mode application, but with the CPL (the processor's Current Privelege Level) set to 0. This allows us to do kernel-mode operations. GiveIO() is called to give the calling process I/O access. All the user-mode app needs do to obtain I/O access is open this device with CreateFile(). No other operations are required. *********************************************************************/ NTSTATUS GiveioCreateDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { GiveIO(); // give the calling process I/O access Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } /***************************************************************************** Driver Entry routine. This routine is called only once after the driver is initially loaded into memory. It allocates everything necessary for the driver's operation. In our case, it allocates memory for our IOPM array, and creates a device which user-mode applications can open. It also creates a symbolic link to the device driver. This allows a user-mode application to access our driver using the \\.\giveio notation. ******************************************************************************/ NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { PDEVICE_OBJECT deviceObject; NTSTATUS status; WCHAR NameBuffer[] = L"\\Device\\" DEVICE_NAME_STRING; WCHAR DOSNameBuffer[] = L"\\DosDevices\\" DEVICE_NAME_STRING; UNICODE_STRING uniNameString, uniDOSString; // Allocate a buffer for the local IOPM and zero it. IOPM_local = MmAllocateNonCachedMemory(sizeof(IOPM)); if(IOPM_local == 0) return STATUS_INSUFFICIENT_RESOURCES; RtlZeroMemory(IOPM_local, sizeof(IOPM)); // Set up device driver name and device object. RtlInitUnicodeString(&uniNameString, NameBuffer); RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); status = IoCreateDevice(DriverObject, 0, &uniNameString, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject); if(!NT_SUCCESS(status)) return status; status = IoCreateSymbolicLink (&uniDOSString, &uniNameString); if (!NT_SUCCESS(status)) return status; // Initialize the Driver Object with driver's entry points. // All we require are the Create and Unload operations. DriverObject->MajorFunction[IRP_MJ_CREATE] = GiveioCreateDispatch; DriverObject->DriverUnload = GiveioUnload; return STATUS_SUCCESS; } Listing Five /********************************************************************* TSTIO.EXE -- by Dale Roberts Compile: cl -DWIN32 tstio.c Purpose: Test the GIVEIO device driver by doing some direct port I/O. We access the PC's internal speaker. *********************************************************************/ #include #include #include #include typedef struct { short int pitch; short int duration; } NOTE; /* Table of notes. Given in half steps. Communication from "other side." */ NOTE notes[] = {{14, 500}, {16, 500}, {12, 500}, {0, 500}, {7, 1000}}; /***** Set PC's speaker frequency in Hz. The speaker is controlled by an ***** Intel 8253/8254 timer at I/O port addresses 0x40-0x43. *****/ void setfreq(int hz) { hz = 1193180 / hz; // clocked at 1.19MHz _outp(0x43, 0xb6); // timer 2, square wave _outp(0x42, hz); _outp(0x42, hz >> 8); } /********************************************************************* Pass a note, in half steps relative to 400 Hz. The 12 step scale is an exponential thing. Speaker control is at port 0x61. Setting lowest two bits enables timer 2 of the 8253/8254 timer and turns on the speaker. *********************************************************************/ void playnote(NOTE note) { _outp(0x61, _inp(0x61) | 0x03); // start speaker going setfreq((int)(400 * pow(2, note.pitch / 12.0))); Sleep(note.duration); _outp(0x61, _inp(0x61) & ~0x03); // stop that racket! } /********************************************************************* Open and close the GIVEIO device. This should give us direct I/O access. Then try it out by playin' our tune. *********************************************************************/ int main() { int i; HANDLE h; h = CreateFile("\\\\.\\giveio", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(h == INVALID_HANDLE_VALUE) { printf("Couldn't access giveio device\n"); return -1; } CloseHandle(h); for(i=0; i < sizeof(notes)/sizeof(int); ++i) playnote(notes[i]); return 0; }