A Sample Memory Spy Device
One of the frequently recurring Microsoft statements about Windows NT and 2000 is that it is a secure operating system. Along with user authentication issues in networking environments, this also includes robustness against bad applications that might compromise the system's integrity by misusing pointers or writing outside the bounds of a memory data structure. This has always been a nasty problem on Windows 3.x, in which the system and all applications shared a single memory space. Windows NT has introduced a clear separation between system and application memory and between concurrent processes. Each process gets its own 4-GB address space, as depicted in Figure 4-2. Whenever a task switch occurs, the current address space is switched out and another one is mapped in by selecting different values for the segment registers, page tables, and other memory management data specific to a process. This design prevents applications from inadvertently tampering with memory of other applications. Each process also requires access to system resources, so the 4-GB space always contains some system code and data. To protect these memory regions from being overwritten by hostile application code, a different trick is employed.
Windows 2000 Memory Segmentation
Windows 2000 has inherited the basic memory segmentation scheme of Windows NT 4.0, which divides the 4-GB process address space in two equal parts by default. The lower half, comprising the range 0x00000000 to 0x7FFFFFFF, contains application data and code running in user-mode, which is equivalent to Privilege Level 3 or "Ring 3" in Intel's terminology (Intel 1999a, pp. 4-8ff; Intel 1999c, pp. 4-8ff). The upper half, ranging from 0x80000000 to 0xFFFFFFFF, is reserved for the system, which is running in kernel-mode, also known as Intel's Privilege Level 0 or "Ring 0." The privilege level determines what operations may be executed and which memory locations can be accessed by the code. Especially, this means that certain CPU instructions are forbidden and certain memory regions are inaccessible, for low- privileged code. For example, if a user-mode application touches any address in the upper half of the 4-GB address space, the system will throw an exception and terminate the application process without giving it another chance.
Figure 4-5 demonstrates what happens if an application attempts to read from address 0x80000000. This strict access limitation is good for the integrity of the system but bad for debugging tools that should be able to show the contents of all valid memory regions. Fortunately, an easy workaround exists: Like the system itself, kernel-mode drivers run on the highest privilege level and therefore are allowed to execute all CPU instructions and to see all memory locations. The trick is to inject a spy driver into the system that reads the requested memory and sends the contents to a companion application waiting in user-mode. Of course, even a kernel-mode driver cannot read from virtual memory addresses that aren't backed up by physical or page file memory. Therefore, such a driver must check all addresses carefully before accessing them in order to avoid the dreaded Blue Screen Of Death (BSOD). Contrary to an application exception, which terminates the problem application only, a driver exception stops the entire system and forces a full reboot.
Figure 4-5. Addresses Starting at 0x80000000 Are Not Accessible in User-mode
The Device I/O Control Dispatcher
The companion CD of this book contains the source code of a versatile spy device implemented as a kernel-mode driver, which can be found in the \src\w2k_spy directory tree. This device is based on a driver skeleton generated by the driver wizard introduced in Chapter 3. The user-mode interface of w2k_spy.sys is based on Win32 Device I/O Control (IOCTL), briefly described in the same chapter. The spy driver defines a device named \Device\w2k_spy and a symbolic link, \DosDevices\ w2k_spy, required to make the device reachable from user-mode. It is funny that the namespace of symbolic links is called \DosDevices. We are certainly not working with DOS device drivers here. This name has historic roots and is now set in stone. With the symbolic link installed, the driver can be opened by any user-mode module via the standard Win32 API function CreateFile(), using the path \\.\w2k_spy.
The character sequence \\.\ is a general escape for local devices. For example, \\.\C: refers to hard disk C: of the local system. See the CreateFile() documentation in the Microsoft Platform SDK for more details.
Parts of the driver's header file w2k_spy.h are included above as Listings 4-2 to 4-5. This file is somewhat similar to a DLL header file: It contains definitions required by the module itself during compilation, but it also provides enough information for a client application that needs to interface to it. Both the DLL/driver and the client application include the same header file, and each module picks out the definitions it needs for proper operation. However, this Janus-headed nature of the header file creates many more problems for a kernel-mode driver than for a DLL because of the special development environment Microsoft provides for drivers. Unfortunately, the header files contained in the DDK are not compatible with the Win32 files in the Platform SDK. The header files cannot be mixed, at least not in C language projects, resulting in a deadlocked situation in which the kernel-mode driver has access to constants, macros, and data types not available to the client application, and vice versa. Therefore, w2k_spy.c defines a flag constant named _W2K_SPY_SYS_, and w2k_spy.h checks the presence or absence of this constant to define items that are missing in one or the other environment, using #ifdef...#else...#endif clauses. This means that all definitions found in the #ifdef _W2K_SPY_SYS_ branch are "seen" by the driver code only, whereas the definitions in the #else branch are evaluated exclusively by the client application. All parts of w2k_spy.h outside these conditional clauses apply to both modules.
In Chapter 3, in the discussion of my driver wizard, I presented the driver skeleton code provided by the wizard in Listing 3-3. The starting point of any new driver project created by this wizard is usually the DeviceDispatcher() function. It receives a device context pointer and a pointer to the I/O Request Packet (IRP) that is to be dispatched. The wizard's boilerplate code already handles the basic I/O requests IRP_MJ_CREATE, IRP_MJ_CLEANUP, and IRP_MJ_CLOSE, sent to the device when it is opened or closed by a client. The DeviceDispatcher() simply returns STATUS_SUCCESS for these requests, so the device can be opened and closed without error. For some devices, this behavior is sufficient, but others require more or less complex initialization and cleanup code here. All remaining requests return STATUS_NOT_IMPLEMENTED. The first step in the extension of the code is to change this default behavior by handling more requests. As already noted, one of the main tasks of w2k_spy.sys is to send data unavailable in user-mode to a Win32 application by means of IOCTL calls, so the work starts with the addition of an IRP_MJ_DEVICE_CONTROL case to the DeviceDispatcher() function. Listing 4-6 shows the updated code, as it appears in w2k_spy.c.
Listing 4-6. Adding an IRP_MJ_DEVICE_CONTROL Case to the Dispatcher
NTSTATUS DeviceDispatcher (PDEVICE_CONTEXT pDeviceContext, PIRP pIrp) { PIO_STACK_LOCATION pisl; DWORD dInfo = 0; NTSTATUS ns = STATUS_NOT_IMPLEMENTED; pisl = IoGetCurrentIrpStackLocation (pIrp); switch (pisl->MajorFunction) { case IRP_MJ_CREATE: case IRP_MJ_CLEANUP: case IRP_MJ_CLOSE: { ns = STATUS_SUCCESS; break; } case IRP_MJ_DEVICE_CONTROL: { ns = SpyDispatcher (pDeviceContext, pisl->Parameters.DeviceIoControl .IoControlCode, pIrp->AssociatedIrp.SystemBuffer, pisl->Parameters.DeviceIoControl .InputBufferLength, pIrp->AssociatedIrp.SystemBuffer, pisl->Parameters.DeviceIoControl .OutputBufferLength, &dInfo); break; } } pIrp->IoStatus.Status = ns; pIrp->IoStatus.Information = dInfo; IoCompleteRequest (pIrp, IO_NO_INCREMENT); return ns; }
The IOCTL handler in Listing 4-6 is fairly simpleit just calls SpyDispatcher() with parameters it extracts from the IRP structure and the current I/O stack location embedded in it. The SpyDispatcher(), shown in Listing 4-7, requires the following arguments:
pDeviceContext is the driver's device context. The basic Device_Context structure provided by the driver wizard contains the driver and device object pointers only (see Listing 3-4). The spy driver adds a couple of members to it for private use.
-
dCode specifies the IOCTL code that determines the command to be executed by the spy device. An IOCTL code is a 32-bit integer consisting of 4 bit-fields, as illustrated by Figure 4-6.
pInput points to the buffer providing the IOCTL input data.
dInput is the size of the input buffer.
pOutput points to the buffer receiving the IOCTL output data.
dOutput is the size of the output buffer.
pdInfo points to a DWORD variable that should receive the number of bytes written to the output buffer.
Depending on the IOCTL method used, the input and output buffers are passed differently from the system to the driver. The spy device uses buffered I/O, directing the system to copy the input data to a safe buffer allocated automatically by the system, and to copy a specified amount of data from the same system buffer to the caller's output buffer on return. It is important to keep in mind that the input and output buffers overlap in this case, so the IOCTL handler must save any input data it might need later before it writes any output data to the buffer. The pointer to this I/O buffer is stored in the SystemBuffer member of the AssociatedIrp union inside the IRP structure (cf. ntddk.h). The input and output buffer sizes are stored in a completely different location of the IRPthey are part of the DeviceIoControl member of the Parameters union inside the IRP's current stack location, named InputBufferLength and OutputBufferLength, respectively. The DeviceIoControl substructure also provides the IOCTL code via its IoControlCode member. More information about the Windows NT/2000 IOCTL methods and how they pass data in and out can be found in my article "A Spy Filter Driver for Windows NT" in Windows Developer's Journal (Schreiber 1997).
Listing 4-7. The Spy Driver's Internal Command Dispatcher
NTSTATUS SpyDispatcher (PDEVICE_CONTEXT pDeviceContext, DWORD dCode, PVOID pInput, DWORD dInput, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_MEMORY_BLOCK smb; SPY_PAGE_ENTRY spe; SPY_CALL_INPUT sci; PHYSICAL_ADDRESS pa; DWORD dValue, dCount; BOOL fReset, fPause, fFilter, fLine; PVOID pAddress; PBYTE pbName; HANDLE hObject; NTSTATUS ns = STATUS_INVALID_PARAMETER; MUTEX_WAIT (pDeviceContext->kmDispatch); *pdInfo = 0; switch (dCode) { case SPY_IO_VERSION_INFO: { ns = SpyOutputVersionInfo (pOutput, dOutput, pdInfo); break; } case SPY_IO_OS_INFO: { ns = SpyOutputOsInfo (pOutput, dOutput, pdInfo); break; } case SPY_IO_SEGMENT: { if ((ns = SpyInputDword (&dValue, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputSegment (dValue, pOutput, dOutput, pdInfo); } break; } case SPY_IO_INTERRUPT: { if ((ns = SpyInputDword (&dValue, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputInterrupt (dValue, pOutput, dOutput, pdInfo); } break; } case SPY_IO_PHYSICAL: { if ((ns = SpyInputPointer (&pAddress, pInput, dInput)) == STATUS_SUCCESS) { pa = MmGetPhysicalAddress (pAddress); ns = SpyOutputBinary (&pa, PHYSICAL_ADDRESS_, pOutput, dOutput, pdInfo); } break; } case SPY_IO_CPU_INFO: { ns = SpyOutputCpuInfo (pOutput, dOutput, pdInfo); break; } case SPY_IO_PDE_ARRAY: { ns = SpyOutputBinary (X86_PDE_ARRAY, SPY_PDE_ARRAY_, pOutput, dOutput, pdInfo); break; } case SPY_IO_PAGE_ENTRY: { if ((ns = SpyInputPointer (&pAddress, pInput, dInput)) == STATUS_SUCCESS) { SpyMemoryPageEntry (pAddress, &spe); ns = SpyOutputBinary (&spe, SPY_PAGE_ENTRY_, pOutput, dOutput, pdInfo); } break; } case SPY_IO_MEMORY_DATA: { if ((ns = SpyInputMemory (&smb, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputMemory (&smb, pOutput, dOutput, pdInfo); } break; } case SPY_IO_MEMORY_BLOCK: { if ((ns = SpyInputMemory (&smb, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputBlock (&smb, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HANDLE_INFO: { if ((ns = SpyInputHandle (&hObject, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputHandleInfo (hObject, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HOOK_INFO: { ns = SpyOutputHookInfo (pOutput, dOutput, pdInfo); break; } case SPY_IO_HOOK_INSTALL: { if (((ns = SpyInputBool (&fReset, pInput, dInput)) == STATUS_SUCCESS) && ((ns = SpyHookInstall (fReset, &dCount)) == STATUS_SUCCESS)) { ns = SpyOutputDword (dCount, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HOOK_REMOVE: { if (((ns = SpyInputBool (&fReset, pInput, dInput)) == STATUS_SUCCESS) && ((ns = SpyHookRemove (fReset, &dCount)) == STATUS_SUCCESS)) { ns = SpyOutputDword (dCount, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HOOK_PAUSE: { if ((ns = SpyInputBool (&fPause, pInput, dInput)) == STATUS_SUCCESS) { fPause = SpyHookPause (fPause); ns = SpyOutputBool (fPause, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HOOK_FILTER: { if ((ns = SpyInputBool (&fFilter, pInput, dInput)) == STATUS_SUCCESS) { fFilter = SpyHookFilter (fFilter); ns = SpyOutputBool (fFilter, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HOOK_RESET: { SpyHookReset (); ns = STATUS_SUCCESS; break; } case SPY_IO_HOOK_READ: { if ((ns = SpyInputBool (&fLine, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputHookRead (fLine, pOutput, dOutput, pdInfo); } break; } case SPY_IO_HOOK_WRITE: { SpyHookWrite (pInput, dInput); ns = STATUS_SUCCESS; break; } case SPY_IO_MODULE_INFO: { if ((ns = SpyInputPointer (&pbName, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputModuleInfo (pbName, pOutput, dOutput, pdInfo); } break; } case SPY_IO_PE_HEADER: { if ((ns = SpyInputPointer (&pAddress, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputPeHeader (pAddress, pOutput, dOutput, pdInfo); } break; } case SPY_IO_PE_EXPORT: { if ((ns = SpyInputPointer (&pAddress, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputPeExport (pAddress, pOutput, dOutput, pdInfo); } break; } case SPY_IO_PE_SYMBOL: { if ((ns = SpyInputPointer (&pbName, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputPeSymbol (pbName, pOutput, dOutput, pdInfo); } break; } case SPY_IO_CALL: { if ((ns = SpyInputBinary (&sci, SPY_CALL_INPUT_, pInput, dInput)) == STATUS_SUCCESS) { ns = SpyOutputCall (&sci, pOutput, dOutput, pdInfo); } break; } } MUTEX_RELEASE (pDeviceContext->kmDispatch); return ns; }
The main DDK header file ntddk.h, as well as the Win32 file winioctl.h in the Platform SDK, define the simple but highly convenient CTL_CODE() macro shown in Listing 4-8 to build IOCTL codes according to the diagram in Figure 4-6. The four parts serve the following purposes:
DeviceType is a 16-bit device type ID. ntddk.h lists a couple of predefined types, symbolized by the constants FILE_DEVICE_*. Microsoft reserves the range 0x0000 to 0x7FFF for internal use, while the range 0x8000 to 0xFFFF is available to developers. The spy driver defines its own device ID FILE_DEVICE_SPY and sets it to 0x8000.
Access specifies the 2-bit access check value determining the required access rights for the IOCTL operation. Possible values are FILE_ANY_ ACCESS (0), FILE_READ_ACCESS (1), FILE_WRITE_ACCESS (2), and the combination of the latter two, FILE_READ_ACCESS | FILE_WRITE_ ACCESS (3). See ntddk.h for more details.
Function is a 12-bit ID that selects the operation to be performed by the device. Microsoft reserves the values 0x000 to 0x7FF for internal use, and leaves range 0x800 to 0xFFF for developers. The IOCTL function IDs recognized by the spy device are drawn from the latter number pool.
Method consists of 2 bits, selecting one of four available I/O transfer methods named METHOD_BUFFERED (0), METHOD_IN_DIRECT (1), METHOD_ OUT_DIRECT (2), and METHOD_NEITHER (3), found in ntddk.h. The spy device uses METHOD_BUFFERED for all requests, which is a highly secure but also somewhat sluggish method because of the data copying between the client and system buffers. Because the I/O of the memory spy is not time-critical, it is a good idea to opt for security. If you want to know more about the other methods, please refer to my spy filter article mentioned on p.191. (Schreiber 1997).
Listing 4-8. The CTL_CODE() Macro Builds I/O Control Codes
#define CTL_CODE (DeviceType, Function, Method, Access) \ (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) (Method))
Figure 4-6. Structure of a Device I/O Control Code
Table 4-2 summarizes all IOCTL functions supported by w2k_spy.sys. The functions with IDs in the range 0 to 10 are memory exploration primitives that are sufficient to cover a wide range of tasks; they are discussed later in this chapter. The remaining functions with IDs of 11 and up belong to different IOCTL groups that will be described in detail in the next chapters, where Native API hooks and kernel calls from user-mode are discussed. Note that some IOCTL codes require the write access right, indicated by bit #15 being set (see Figure 4-6). That is, all IOCTL commands with a code of 0x80006nnn can be issued via a read-only device handle, and a code of 0x8000Ennn requires a read/write handle. The access rights are typically requested in the CreateFile() call that opens the device by specifying a combination of the GENERIC_READ and GENERIC_WRITE flags for the dwDesiredAccess argument.
The function names in the leftmost column of Table 4-2 also appear as cases of the large switch/case statement of the SpyDispatcher() function in Listing 4-7. This function first obtains the device's dispatcher mutex to guarantee that only a single request is executed at a time if more than one client or a multithreaded application communicates with the device. MUTEX_WAIT() is a wrapper macro for KeWaitForMutexObject(), which takes no less than five arguments. KeWaitForMutexObject() is a macro itself, forwarding its arguments to KeWaitForSingleObject(). MUTEX_WAIT(), along with its friends MUTEX_RELEASE() and MUTEX_INITIALIZE(), is shown in Listing 4-9. After the mutex object becomes signaled, SpyDispatcher() branches to various short code sequences, depending on the received IOCTL code. At the end, it releases the mutex and returns a status code to the caller.
The SpyDispatcher() uses a couple of helper functions to read input parameters, obtain the requested data, and write the data to the caller's output buffer. As already mentioned, a kernel-mode driver must be overly fussy with any user-mode
Table 4-2. IOCTL Functions Supported by the Spy Device
FUNCTION NAME |
ID |
IOCTL CODE |
DESCRIPTION |
SPY_IO_VERSION_INFO |
0 |
0x80006000 |
Returns spy version information |
SPY_IO_OS_INFO |
1 |
0x80006004 |
Returns operating system information |
SPY_IO_SEGMENT |
2 |
0x80006008 |
Returns the properties of a segment |
SPY_IO_INTERRUPT |
3 |
0x8000600C |
Returns the properties of an interrupt gate |
SPY_IO_PHYSICAL |
4 |
0x80006010 |
Linear-to-physical address translation |
SPY_IO_CPU_INFO |
5 |
0x80006014 |
Returns the values of special CPU registers |
SPY_IO_PDE_ARRAY |
6 |
0x80006018 |
Returns the PDE array at 0xC0300000 |
SPY_IO_PAGE_ENTRY |
7 |
0x8000601C |
Returns the PDE or PTE of a linear address |
SPY_IO_MEMORY_DATA |
8 |
0x80006020 |
Returns the contents of a memory block |
SPY_IO_MEMORY_BLOCK |
9 |
0x80006024 |
Returns the contents of a memory block |
SPY_IO_HANDLE_INFO |
10 |
0x80006028 |
Looks up object properties from a handle |
SPY_IO_HOOK_INFO |
11 |
0x8000602C |
Returns info about Native API hooks |
SPY_IO_HOOK_INSTALL |
12 |
0x8000E030 |
Installs Native API hooks |
SPY_IO_HOOK_REMOVE |
13 |
0x8000E034 |
Removes Native API hooks |
SPY_IO_HOOK_PAUSE |
14 |
0x8000E038 |
Pauses/resumes the hook protocol |
SPY_IO_HOOK_FILTER |
15 |
0x8000E03C |
Enables/disables the hook protocol filter |
SPY_IO_HOOK_RESET |
16 |
0x8000E040 |
Clears the hook protocol |
SPY_IO_HOOK_READ |
17 |
0x80006044 |
Reads data from the hook protocol |
SPY_IO_HOOK_WRITE |
18 |
0x8000E048 |
Writes data to the hook protocol |
SPY_IO_MODULE_INFO |
19 |
0x8000604C |
Returns information about loaded system modules |
SPY_IO_PE_HEADER |
20 |
0x80006050 |
Returns IMAGE_NT_HEADERS data |
SPY_IO_PE_EXPORT |
21 |
0x80006054 |
Returns IMAGE_EXPORT_DIRECTORY data |
SPY_IO_PE_SYMBOL |
22 |
0x80006058 |
Returns the address of an exported system symbol |
SPY_IO_CALL |
23 |
0x8000E05C |
Calls a function inside a loaded module |
parameters it receives. From a driver's perspective, all user-mode code is evil and has no other thing on its mind but to trash the system. This somewhat paranoid view is not absurdjust the slightest slip brings the whole system to an immediate stop, with the appearance of a BlueScreen. So, if a client application says: "Here's my bufferit can take up to 4,096 bytes," the driver does not accept itneither that the buffer
Listing 4-9. Kernel-Mutex Management Macros
#define MUTEX_INITIALIZE(_mutex) \ KeInitializeMutex \ (&(_mutex), 0) #define MUTEX_WAIT(_mutex) \ KeWaitForMutexObject \ (&(_mutex), Executive, KernelMode, FALSE, NULL) #define MUTEX_RELEASE(_mutex) \ KeReleaseMutex \ (&(_mutex), FALSE)
points to valid memory, nor that the buffer size is correct. In an IOCTL situation with buffered I/O (i.e., if the Method portion of the IOCTL code indicates METHOD_ BUFFERED), the system takes care of the sanity checks and allocates a buffer that is large enough to hold both the input and output data. However, the other I/O transfer methods, most notably METHOD_NEITHER, where the driver receives original user-mode buffer pointers, require more foresight.
Although the spy device uses buffered I/O, it has to check the input and output parameters for validity. It might be that the client application passes in less data than is required or provides an output buffer that is not large enough for the output data. The system cannot catch these semantic problems, because it doesn't know what kind of data is transferred in an IOCTL transaction. Therefore, SpyDispatcher() calls the SpyInput*() and SpyOutput*() helper functions to copy data from or to the I/O buffers. These functions execute the requested operation only if the buffer size matches the requirements of the operation. Listing 4-10 shows the basic input functions, and Listing 4-11 shows the basic output functions. SpyInputBinary() and SpyOutputBinary() are the workhorses. They test the buffer size, and, if it is OK, they copy the requested amount of data using the Windows 2000 Runtime Library function RtlCopyMemory(). The remaining functions are simple wrappers for the common data types DWORD, BOOL, PVOID, and HANDLE. Additionally, SpyOutputBlock() copies the data block specified by the caller in a SPY_MEMORY_BLOCK structure after verifying that all bytes in the indicated range are readable. The SpyInput*() functions return STATUS_ INVALID_BUFFER_SIZE if incomplete input data is passed in, and the SpyOutput*() functions return STATUS_ BUFFER_TOO_SMALL if the output buffer is smaller than required.
Listing 4-10. Reading Input Data from an IOCTL Buffer
NTSTATUS SpyInputBinary (PVOID pData, DWORD dData, PVOID pInput, DWORD dInput) { NTSTATUS ns = STATUS_OBJECT_TYPE_MISMATCH; if (dData <= dInput) { RtlCopyMemory (pData, pInput, dData); ns = STATUS_SUCCESS; } return ns; } // ----------------------------------------------------------------- NTSTATUS SpyInputDword (PDWORD pdValue, PVOID pInput, DWORD dInput) { return SpyInputBinary (pdValue, DWORD_, pInput, dInput); } // ----------------------------------------------------------------- NTSTATUS SpyInputBool (PBOOL pfValue, PVOID pInput, DWORD dInput) { return SpyInputBinary (pfValue, BOOL_, pInput, dInput); } // ----------------------------------------------------------------- NTSTATUS SpyInputPointer (PPVOID ppAddress, PVOID pInput, DWORD dInput) { return SpyInputBinary (ppAddress, PVOID_, pInput, dInput); } // ----------------------------------------------------------------- NTSTATUS SpyInputHandle (PHANDLE phObject, PVOID pInput, DWORD dInput) { return SpyInputBinary (phObject, HANDLE_, pInput, dInput); }
Listing 4-11. Writing Output Data to an IOCTL Buffer
NTSTATUS SpyOutputBinary (PVOID pData, DWORD dData, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { NTSTATUS ns = STATUS_BUFFER_TOO_SMALL; *pdInfo = 0; if (dData <= dOutput) { RtlCopyMemory (pOutput, pData, *pdInfo = dData); ns = STATUS_SUCCESS; } return ns; } // ----------------------------------------------------------------- NTSTATUS SpyOutputBlock (PSPY_MEMORY_BLOCK psmb, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { NTSTATUS ns = STATUS_INVALID_PARAMETER; if (SpyMemoryTestBlock (psmb->pAddress, psmb->dBytes)) { ns = SpyOutputBinary (psmb->pAddress, psmb->dBytes, pOutput, dOutput, pdInfo); } return ns; } // ----------------------------------------------------------------- NTSTATUS SpyOutputDword (DWORD dValue, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { return SpyOutputBinary (&dValue, DWORD_, pOutput, dOutput, pdInfo); } // ----------------------------------------------------------------- NTSTATUS SpyOutputBool (BOOL fValue, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { return SpyOutputBinary (&fValue, BOOL_, pOutput, dOutput, pdInfo); } // ----------------------------------------------------------------- NTSTATUS SpyOutputPointer (PVOID pValue, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { return SpyOutputBinary (&pValue, PVOID_, pOutput, dOutput, pdInfo); }
You might have noticed that the SpyDispatcher() in Listing 4-7 contains references to a few more SpyInput*() and SpyOutput*() functions. Although ultimately based on SpyInputBinary() and SpyOutputBinary(), they are slightly more complex than the basic functions in Listings 4-10 and 4-11 and, therefore, are discussed separately a little later in this chapter. So let's start at the beginning of SpyDispatcher() and work through the switch/case statement step by step.
The IOCTL Function SPY_IO_VERSION_INFO
The IOCTL SPY_IO_VERSION_INFO function fills a caller-supplied SPY_ VERSION_INFO structure with data about the spy driver itself. It doesn't require input parameters and uses the SpyOutputVersionInfo() helper function. This function, included in Listing 4-12 together with the SPY_VERSION_INFO structure, is trivial. It sets the dVersion member to the constant SPY_VERSION (currently 100, indicating V1.00) defined in w2k_spy.h, and copies the driver's name symbolized by the string constant DRV_NAME ("SBS Windows 2000 Spy Device") to the awName member. The major version number is obtained by dividing dVersion by 100. The remainder yields the minor version number.
Listing 4-12. Obtaining Version Information About the Spy Driver
typedef struct _SPY_VERSION_INFO { DWORD dVersion; WORD awName [SPY_NAME_]; } SPY_VERSION_INFO, *PSPY_VERSION_INFO, **PPSPY_VERSION_INFO;#define SPY_VERSION_INFO_ sizeof (SPY_VERSION_INFO) // ----------------------------------------------------------------- NTSTATUS SpyOutputVersionInfo (PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_VERSION_INFO svi; svi.dVersion = SPY_VERSION; wcscpyn (svi.awName, USTRING (CSTRING (DRV_NAME)), SPY_NAME_); return SpyOutputBinary (&svi, SPY_VERSION_INFO_, pOutput, dOutput, pdInfo); }
The IOCTL Function SPY_IO_OS_INFO
The IOCTL SPY_IO_OS_INFO function is much more interesting than the preceding one. It is another output-only function, expecting no input arguments and filling a caller-supplied SPY_OS_INFO structure with the values of several internal operating system parameters. Listing 4-13 shows the definition of this structure and the helper function SpyOutputOsInfo() called by the dispatcher. Some of the structure members are simply set to constants drawn from the DDK header files and w2k_spy.h; others receive "live" values read out from several internal kernel variables and structures. In Chapter 2, you became acquainted with the variables NtBuildNumber and NtGlobalFlag, exported by ntoskrnl.exe (see Table B-1 in Appendix B). Other than the other exported Nt* symbols, these don't point to API functions, but to variables in the kernel's .data section. In the Win32 world, it is quite uncommon to export variables. However, several Windows 2000 kernel modules make use of this technique. ntoskrnl.exe exports no fewer than 55 variables, ntdll.dll provides 4, and hal.dll provides 1. Of the set of ntoskrnl.exe variables, SpyOutputOsInfo() copies MmHighestUserAddress, MmUserProbeAddress, MmSystemRangeStart, NtGlobalFlag, KeI386MachineType, KeNumberProcessors, and NtBuildNumber to the output buffer.
When a module imports data from another module, it has to instruct the compiler and linker accordingly by using the extern keyword. This will cause the linker to generate an entry in the module's import section instead of trying to resolve the symbol to a fixed address. Some extern declarations are already included in ntddk.h. Those that are missing are included in Listing 4-13.
Listing 4-13. Obtaining Information About the Operating System
typedef struct _SPY_OS_INFO { DWORD dPageSize; DWORD dPageShift; DWORD dPtiShift; DWORD dPdiShift; DWORD dPageMask; DWORD dPtiMask; DWORD dPdiMask; PX86_PE PteArray; PX86_PE PdeArray; PVOID pLowestUserAddress; PVOID pThreadEnvironmentBlock; PVOID pHighestUserAddress; PVOID pUserProbeAddress; PVOID pSystemRangeStart; PVOID pLowestSystemAddress; PVOID pSharedUserData; PVOID pProcessorControlRegion; PVOID pProcessorControlBlock; DWORD dGlobalFlag; DWORD dI386MachineType; DWORD dNumberProcessors; DWORD dProductType; DWORD dBuildNumber; DWORD dNtMajorVersion; DWORD dNtMinorVersion; WORD awNtSystemRoot [MAX_PATH]; } SPY_OS_INFO, *PSPY_OS_INFO, **PPSPY_OS_INFO; #define SPY_OS_INFO_ sizeof (SPY_OS_INFO) // ----------------------------------------------------------------- extern PWORD NlsAnsiCodePage; extern PWORD NlsOemCodePage; extern PWORD NtBuildNumber; extern PDWORD NtGlobalFlag; extern PDWORD KeI386MachineType; // ----------------------------------------------------------------- NTSTATUS SpyOutputOsInfo (PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_SEGMENT ss; SPY_OS_INFO soi; NT_PRODUCT_TYPE NtProductType; PKPCR pkpcr; NtProductType = (SharedUserData->ProductTypeIsValid ? SharedUserData->NtProductType : 0); SpySegment (X86_SEGMENT_FS, 0, &ss); pkpcr = ss.pBase; soi.dPageSize = PAGE_SIZE; soi.dPageShift = PAGE_SHIFT; soi.dPtiShift = PTI_SHIFT; soi.dPdiShift = PDI_SHIFT; soi.dPageMask = X86_PAGE_MASK; soi.dPtiMask = X86_PTI_MASK; soi.dPdiMask = X86_PDI_MASK; soi.PteArray = X86_PTE_ARRAY; soi.PdeArray = X86_PDE_ARRAY; soi.pLowestUserAddress = MM_LOWEST_USER_ADDRESS; soi.pThreadEnvironmentBlock = pkpcr->NtTib.Self; soi.pHighestUserAddress = *MmHighestUserAddress; soi.pUserProbeAddress = (PVOID) *MmUserProbeAddress; soi.pSystemRangeStart = *MmSystemRangeStart; soi.pLowestSystemAddress = MM_LOWEST_SYSTEM_ADDRESS; soi.pSharedUserData = SharedUserData; soi.pProcessorControlRegion = pkpcr; soi.pProcessorControlBlock = pkpcr->Prcb; soi.dGlobalFlag = *NtGlobalFlag; soi.dI386MachineType = *KeI386MachineType; soi.dNumberProcessors = *KeNumberProcessors; soi.dProductType = NtProductType; soi.dBuildNumber = *NtBuildNumber; soi.dNtMajorVersion = SharedUserData->NtMajorVersion; soi.dNtMinorVersion = SharedUserData->NtMinorVersion; wcscpyn (soi.awNtSystemRoot, SharedUserData->NtSystemRoot, MAX_PATH); return SpyOutputBinary (&soi, SPY_OS_INFO_, pOutput, dOutput, pdInfo); }
The remaining members of the SPY_OS_INFO structure are filled with values from system data structures lying around in memory. For example, SpyOutputOsInfo() assigns the base address of the Kernel's Processor Control Region (KPCR) to the pProcessorControlRegion member. This is a very important data structure that contains lots of frequently used thread-specific data items, and therefore is placed in its own memory segment addressed by the CPU's FS register. Both Windows NT 4.0 and Windows 2000 set up FS to point to the linear address 0xFFDFF000 in kernel-mode. SpyOutputOsInfo() calls the SpySegment() function discussed later to query the base address of the FS segment in the linear address space. This segment also comprises the Kernel's Processor Control Block (KPRCB), pointed to by the Prcb member of the KPCR, immediately followed by a CONTEXT structure containing low-level CPU status information of the current thread. The definitions of the KPCR, KPRCB, and CONTEXT structures can be looked up in the ntddk.h header file. More on this topic follows later in this chapter.
Another internal data structure referenced in Listing 4-13 is SharedUserData. It is actually nothing but a "well-known address," typecast to a structure pointer. Listing 4-14 shows the definition as it appears in ntddk.h. Well-known addresses are locations within the linear address space that are set at compile time, and hence do not vary over time or with the configuration. Obviously, SharedUserData is a pointer to a KUSER_SHARED_DATA structure found at the fixed linear address 0xFFDF0000. This memory area is shared by the user-mode application and the system, and it contains such interesting things as the operating system's version number, which SpyOutputOsInfo() copies to the dNtMajorVersion and dNtMinorVersion members of the caller's SPY_OS_INFO structure. As I will show later, the KUSER_SHARED_DATA structure is mirrored to address 0x7FFE0000, where user-mode code can access it.
Following the explanation of the spy device's IOCTL functions is a demo application that displays the returned data on the screen.
Listing 4-14. Definition of SharedUserData
#define KI_USER_SHARED_DATA 0xffdf0000 #define SharedUserData ((KUSER_SHARED_DATA * const) KI_USER_SHARED_DATA)
The IOCTL Function SPY_IO_SEGMENT
Now this discussion becomes really interesting. The SPY_IO_SEGMENT function does some very-low-level operations to query the properties of a segment, given a selector. SpyDispatcher() first calls SpyInputDword() to get the selector value passed in by the calling application. You might recall that selectors are 16-bit quantities. However, I try to avoid 16-bit data types whenever possible because the native word size of the i386 CPUs in 32-bit mode is the 32-bit DWORD. Therefore, I have extended the selector argument to a DWORD where the upper 16 bits are always zero. If SpyInputDword() reports success, the SpyOutputSegment() function shown in Listing 4-15 is called. This function simply returns to the caller whatever the SpySegment() helper function, included in Listing 4-15, returns. Basically, SpySegment() fills a SPY_SEGMENT structure, defined at the top of Listing 4-15. It comprises the selector's value in the form of a X86_SELECTOR structure (see Listing 4-2), along with its 64-bit X86_DESCRIPTOR (Listing 4-2, again), the corresponding segment's linear base address, the segment limit (i.e., the segment size minus one), and a flag named fOk indicating whether the data in the SPY_SEGMENT structure is valid. The latter is required in the context of other functions (e.g., SPY_IO_CPU_INFO) that return the properties of several segments at once. In this case, the fOk member enables the caller to sort out any invalid segments contained in the output data.
Listing 4-15. Querying Segment Properties
typedef struct _SPY_SEGMENT { X86_SELECTOR Selector; X86_DESCRIPTOR Descriptor; PVOID pBase; DWORD dLimit; BOOL fOk; } SPY_SEGMENT, *PSPY_SEGMENT, **PPSPY_SEGMENT; #define SPY_SEGMENT_ sizeof (SPY_SEGMENT) // ----------------------------------------------------------------- NTSTATUS SpyOutputSegment (DWORD dSelector, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_SEGMENT ss; SpySegment (X86_SEGMENT_OTHER, dSelector, &ss); return SpyOutputBinary (&ss, SPY_SEGMENT_, pOutput, dOutput, pdInfo); } // ----------------------------------------------------------------- BOOL SpySegment (DWORD dSegment, DWORD dSelector, PSPY_SEGMENT pSegment) { BOOL fOk = FALSE; if (pSegment != NULL) { fOk = TRUE; if (!SpySelector (dSegment, dSelector, &pSegment->Selector)) { fOk = FALSE; } if (!SpyDescriptor (&pSegment->Selector, &pSegment->Descriptor)) { fOk = FALSE; } pSegment->pBase = SpyDescriptorBase (&pSegment->Descriptor); pSegment->dLimit = SpyDescriptorLimit (&pSegment->Descriptor); pSegment->fOk = fOk; } return fOk; }
SpySegment() relies on several other helper functions that provide the parts that make up the resulting SPY_SEGMENT structure. First, SpySelector() copies a selector value to the passed-in X86_SELECTOR structure (Listing 4-16). If the first argument, dSegment, is set to X86_SEGMENT_OTHER, the dSelector argument is assumed to specify a valid selector value, so this value is simply assigned to the wValue member of the output structure. Otherwise, dSelector is ignored, and dSegment is used in a switch/ case construct that selects one of the CPU's segment registers or its task register TR. Note that this requires a little bit of inline assemblythe C language doesn't provide a standard means for accessing processor-specific features such as segment registers.
Listing 4-16. Obtaining Selector Values
#define X86_SEGMENT_OTHER 0 #define X86_SEGMENT_CS 1 #define X86_SEGMENT_DS 2 #define X86_SEGMENT_ES 3 #define X86_SEGMENT_FS 4 #define X86_SEGMENT_GS 5 #define X86_SEGMENT_SS 6 #define X86_SEGMENT_TSS 7 // ----------------------------------------------------------------- BOOL SpySelector (DWORD dSegment, DWORD dSelector, PX86_SELECTOR pSelector) { X86_SELECTOR Selector = {0, 0}; BOOL fOk = FALSE; if (pSelector != NULL) { fOk = TRUE; switch (dSegment) { case X86_SEGMENT_OTHER: { if (fOk = ((dSelector >> X86_SELECTOR_SHIFT) <= X86_SELECTOR_LIMIT)) { Selector.wValue = (WORD) dSelector; } break; } case X86_SEGMENT_CS: { __asm mov Selector.wValue, cs break; } case X86_SEGMENT_DS: { __asm mov Selector.wValue, ds break; } case X86_SEGMENT_ES: { __asm mov Selector.wValue, es break; } case X86_SEGMENT_FS: { __asm mov Selector.wValue, fs break; } case X86_SEGMENT_GS: { __asm mov Selector.wValue, gs break; } case X86_SEGMENT_SS: { __asm mov Selector.wValue, ss break; } case X86_SEGMENT_TSS: { __asm str Selector.wValue break; } default: { fOk = FALSE; break; } } RtlCopyMemory (pSelector, &Selector, X86_SELECTOR_); } return fOk; }
SpyDescriptor() reads in the 64-bit descriptor pointed to by the segment selector (Listing 4-17). As you might recall, all selectors contain a Table Indicator (TI) bit that decides whether the selector refers to a descriptor in the Global Descriptor Table (GDT, TI=0) or Local Descriptor Table (LDT, TI=1). The upper half of Listing 4-17 handles the LDT case. First, the assembly language instructions SLDT and SGDT are used to read the LDT selector value and the segment limit and base address of the GDT, respectively. Remember that the linear base address of the GDT is specified explicitly, whereas the LDT is referenced indirectly via a selector that points into the GDT. Therefore, SpyDescriptor() first validates the LDT selector value. If it is not the null segment selector and does not point beyond the GDT limit, the SpyDescriptorType(), SpyDescriptorLimit(), and SpyDescriptorBase() functions attached to the bottom of Listing 4-17 are called to obtain the basic properties of the LDT:
SpyDescriptorType() returns the values of a descriptor's Type and S bit-fields (cf. Listing 4-2). The LDT selector must point to a system descriptor of type X86_DESCRIPTOR_SYS_LDT (2).
SpyDescriptorLimit() compiles the segment limit from the Limit1 and Limit2 bit-fields of a descriptor. If its G flag indicates a granularity of 4-KB, the value is shifted left by 12 bits, shifting in 1-bits from the right end.
SpyDescriptorBase() simply arranges the Base1, Base2, and Base3 bit-fields of a descriptor properly to yield a 32-bit linear address.
Listing 4-17. Obtaining Descriptor Values
BOOL SpyDescriptor (PX86_SELECTOR pSelector, PX86_DESCRIPTOR pDescriptor) { X86_SELECTOR ldt; X86_TABLE gdt; DWORD dType, dLimit; BOOL fSystem; PX86_DESCRIPTOR pDescriptors = NULL; BOOL fOk = FALSE; if (pDescriptor != NULL) { if (pSelector != NULL) { if (pSelector->TI) // ldt descriptor { __asm { sldt ldt.wValue sgdt gdt.wLimit } if ((!ldt.TI) && ldt.Index && ((ldt.wValue & X86_SELECTOR_INDEX) <= gdt.wLimit)) { dType = SpyDescriptorType (gdt.pDescriptors + ldt.Index, &fSystem); dLimit = SpyDescriptorLimit (gdt.pDescriptors + ldt.Index); if ((dType == X86_DESCRIPTOR_SYS_LDT) && ((DWORD) (pSelector->wValue & X86_SELECTOR_INDEX) <= dLimit)) { pDescriptors = SpyDescriptorBase (gdt.pDescriptors + ldt.Index); } } } else // gdt descriptor { if (pSelector->Index) { __asm { sgdt gdt.wLimit } if ((pSelector->wValue & X86_SELECTOR_INDEX) <= gdt.wLimit) { pDescriptors = gdt.pDescriptors; } } } } if (pDescriptors != NULL) { RtlCopyMemory (pDescriptor, pDescriptors + pSelector->Index, X86_DESCRIPTOR_); fOk = TRUE; } else { RtlZeroMemory (pDescriptor, X86_DESCRIPTOR_); } } return fOk; } // ----------------------------------------------------------------- PVOID SpyDescriptorBase (PX86_DESCRIPTOR pDescriptor) { return (PVOID) ((pDescriptor->Base1 ) | (pDescriptor->Base2 << 16) | (pDescriptor->Base3 << 24)); } // ----------------------------------------------------------------- DWORD SpyDescriptorLimit (PX86_DESCRIPTOR pDescriptor) { return (pDescriptor->G ? (pDescriptor->Limit1 << 12) | (pDescriptor->Limit2 << 28) | 0xFFF : (pDescriptor->Limit1 ) | (pDescriptor->Limit2 << 16)); } // ----------------------------------------------------------------- DWORD SpyDescriptorType (PX86_DESCRIPTOR pDescriptor, PBOOL pfSystem) { if (pfSystem != NULL) *pfSystem = !pDescriptor->S; return pDescriptor->Type; }
If the selector's TI bit indicates a GDT descriptor, things are much simpler. Again, the SGDT instruction is used to get the size and location of the GDT in linear memory, and if the descriptor index specified by the selector is within the proper range, the pDescriptors variable is set to point to the GDT base address. In both the LDT and GDT cases, the pDescriptor variable is non-NULL if the caller has passed in a valid selector. In this case, the 64-bit descriptor value is copied to the caller's X86_DESCRIPTOR structure. Otherwise, all members of this structure are set to zero with the kind help of RtlZeroMemory().
We are still in the discussion of the SpySegment() function shown in Listing 4-15. The SpySelector() and SpyDescriptor() calls have been handled. Only the concluding SpyDescriptorBase() and SpyDescriptorLimit() invocations are left, but you already know what these functions do (see Listing 4-17). If SpySelector() and SpyDescriptor() succeed, the data returned in the SPY_ SEGMENT structure is valid. SpyDescriptorBase() and SpyDescriptorLimit() don't return error flags because they cannot failthey just might return meaningless data if the supplied descriptor is invalid.
The IOCTL Function SPY_IO_INTERRUPT
SPY_IO_INTERRUPT is similar to SPY_IO_SEGMENT, except that this function works on interrupt descriptors stored in the system's Interrupt Descriptor Table (IDT), rather than on LDT or GDT descriptors. The IDT contains up to 256 descriptors that can represent task, interrupt, or trap gates (cf. Intel 1999c, pp. 5-11ff). By the way, interrupts and traps are quite similar in nature, differing in a tiny detail only: An interrupt handler is always entered with interrupts disabled, whereas the interrupt flag is left unchanged upon entering a trap handler. The SPY_IO_INTERRUPT caller supplies an interrupt number between 0 and 256 in its input buffer and a SPY_INTERRUPT structure as output buffer, which will contain the properties of the corresponding interrupt handler on successful return. The SpyOutputInterrupt() helper function invoked by the dispatcher is a simple wrapper that calls SpyInterrupt() and copies the returned data to the output buffer. Both functions, as well as the SPY_INTERRUPT structure they operate on, are shown in Listing 4-18. The latter is filled by SpyInterrupt() with the following items:
Selector specifies the selector of a Task-State Segment (TSS, see Intel 1999c, pp. 6-4ff) or a code segment. A code segment selector determines the segment where an interrupt or trap handler is located.
Gate is the 64-bit task, interrupt, or trap gate descriptor addressed by the selector.
Segment contains the properties of the segment addressed by the gate.
pOffset specifies the offset of the interrupt or trap handler's entry point relative to the base address of the surrounding code segment. Because task gates don't comprise an offset value, this member must be ignored if the input selector refers to a TSS.
fOk is a flag that indicates whether the data in the SPY_INTERRUPT structure is valid.
A TSS is typically used to guarantee that an error situation is handled by a valid task. It is a special system segment type that holds 104 bytes of processor state information needed to restore a task after a task switch has occurred, as outlined in Table 4-3. The CPU always forces a task switch and saves all CPU registers to the TSS when an interrupt associated with a TSS occurs. Windows 2000 stores task gates in the interrupt slots 0x02 (Nonmaskable Interrupt [NMI]), 0x08 (Double Fault), and 0x12 (Stack-Segment Fault). The remaining entries point to interrupt handlers. Unused interrupts are handled by dummy routines named KiUnexpectedInterruptNNN(), where "NNN" is a decimal ordinal number. These handlers branch to the internal function KiEndUnexpectedRange(), which in turn branches to KiUnexpected InterruptTail(), passing in the number of the unhandled interrupt.
Listing 4-18. Querying Interrupt Properties
typedef struct _SPY_INTERRUPT { X86_SELECTOR Selector; X86_GATE Gate; SPY_SEGMENT Segment; PVOID pOffset; BOOL fOk; } SPY_INTERRUPT, *PSPY_INTERRUPT, **PPSPY_INTERRUPT;#define SPY_INTERRUPT_ sizeof (SPY_INTERRUPT) // ----------------------------------------------------------------- NTSTATUS SpyOutputInterrupt (DWORD dInterrupt, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_INTERRUPT si; SpyInterrupt (dInterrupt, &si); return SpyOutputBinary (&si, SPY_INTERRUPT_, pOutput, dOutput, pdInfo); } // ----------------------------------------------------------------- BOOL SpyInterrupt (DWORD dInterrupt, PSPY_INTERRUPT pInterrupt) { BOOL fOk = FALSE; if (pInterrupt != NULL) { if (dInterrupt <= X86_SELECTOR_LIMIT) { fOk = TRUE; if (!SpySelector (X86_SEGMENT_OTHER, dInterrupt << X86_SELECTOR_SHIFT, &pInterrupt->Selector)) { fOk = FALSE; } if (!SpyIdtGate (&pInterrupt->Selector, &pInterrupt->Gate)) { fOk = FALSE; } if (!SpySegment (X86_SEGMENT_OTHER, pInterrupt->Gate.Selector, &pInterrupt->Segment)) { fOk = FALSE; } pInterrupt->pOffset = SpyGateOffset (&pInterrupt->Gate); } else { RtlZeroMemory (pInterrupt, SPY_INTERRUPT_); } pInterrupt->fOk = fOk; } return fOk; } // ----------------------------------------------------------------- PVOID SpyGateOffset (PX86_GATE pGate) { return (PVOID) (pGate->Offset1 | (pGate->Offset2 << 16)); }
Table 4-3. CPU Status Fields in the Task State Segment (TSS)
OFFSET |
BITS |
ID |
DESCRIPTION |
0x00 |
16 |
|
Previous Task Link |
0x04 |
32 |
ESP0 |
Stack Pointer Register for Privilege Level 0 |
0x08 |
16 |
SS0 |
Stack Segment Register for Privilege Level 0 |
0x0C |
32 |
ESP1 |
Stack Pointer Register for Privilege Level 1 |
0x10 |
16 |
SS1 |
Stack Segment Register for Privilege Level 1 |
0x14 |
32 |
ESP2 |
Stack Pointer Register for Privilege Level 2 |
0x18 |
16 |
SS2 |
Stack Segment Register for Privilege Level 2 |
0x1C |
32 |
CR3 |
Page-Directory Base Register (PDBR) |
0x20 |
32 |
EIP |
Instruction Pointer Register |
0x24 |
32 |
EFLAGS |
Processor Flags Register |
0x28 |
32 |
EAX |
General-Purpose Register EAX |
0x2C |
32 |
ECX |
General-Purpose Register ECX |
0x30 |
32 |
EDX |
General-Purpose Register EDX |
0x34 |
32 |
EBX |
General-Purpose Register EDX |
0x38 |
32 |
ESP |
Stack Pointer Register |
0x3C |
32 |
EBP |
Base Pointer Register |
0x40 |
32 |
ESI |
Source Index Register |
0x44 |
32 |
EDI |
Destination Index Register |
0x48 |
16 |
ES |
Extra Segment Register |
0x4C |
16 |
CS |
Code Segment Register |
0x50 |
16 |
SS |
Stack Segment Register |
0x54 |
16 |
DS |
Data Segment Register |
0x58 |
16 |
FS |
Additional Data Segment Register #1 |
0x5C |
16 |
GS |
Additional Data Segment Register #2 |
0x60 |
16 |
LDT |
Local Descriptor Table Segment Selector |
0x64 |
1 |
T |
Debug Trap Flag |
0x66 |
16 |
|
I/O Map Base Address |
0x68 |
|
|
End of CPU State Information |
The SpySegment() and SpySelector() functions called by SpyInterrupt() have already been presented in Listings 4-15 and 4-16. SpyGateOffset(), included at the end of Listing 4-18, works analogous to SpyDescriptorBase() and SpyDescriptorLimit(), picking up the Offset1 and Offset2 bit-fields of an X86_GATE structure and arranging them properly to yield a 32-bit address. SpyIdtGate() is defined in Listing 4-19. It bears a strong similarity to SpyDescriptor() in Listing 4-17 if the LDT clause would be omitted. The assembly language instruction SIDT stores the 48-bit contents of the CPU's IDT register, comprising the 16-bit table limit and the 32-bit linear base address of the IDT. The remaining code in Listing 4-19 compares the descriptor index of the supplied selector to the IDT limit, and, if it is valid, the corresponding interrupt descriptor is copied to the caller's X86_GATE structure. Otherwise, all gate structure members are set to zero.
Listing 4-19. Obtaining IDT Gate Values
BOOL SpyIdtGate (PX86_SELECTOR pSelector, PX86_GATE pGate) { X86_TABLE idt; PX86_GATE pGates = NULL; BOOL fOk = FALSE; if (pGate != NULL) { if (pSelector != NULL) { __asm { sidt idt.wLimit } if ((pSelector->wValue & X86_SELECTOR_INDEX) <= idt.wLimit) { pGates = idt.pGates; } } if (pGates != NULL) { RtlCopyMemory (pGate, pGates + pSelector->Index, X86_GATE_); fOk = TRUE; } else { RtlZeroMemory (pGate, X86_GATE_); } } return fOk; }
The IOCTL Function SPY_IO_PHYSICAL
The IOCTL SPY_IO_PHYSICAL function is simple, because it relies entirely on the MmGetPhysicalAddress() function exported by ntoskrnl.exe. The IOCTL function handler simply calls SpyInputPointer() (see Listing 4-10) to get the linear address to be converted, lets MmGetPhysicalAddress() look up the corresponding physical address, and returns the resulting PHYSICAL_ADDRESS value to the caller. Note that PHYSICAL_ADDRESS is a 64-bit LARGE_INTEGER. On most i386 systems, the upper 32 bits will be always zero. However, on systems with Physical Address Extension (PAE) enabled and more than 4 GB of memory installed, these bits can assume nonzero values.
MmGetPhysicalAddress() uses the PTE array starting at linear address 0xC0000000 to find out the physical address. The basic mechanism works as follows:
If the linear address is within the range 0x80000000 to 0x9FFFFFFF, the three most significant bits are set to zero, yielding a physical address in the range 0x00000000 to 0x1FFFFFFF.
Otherwise, the upper 20 bits are used as an index into the PTE array at address 0xC0000000.
If the P bit of the target PTE is set, indicating that the corresponding page is present in physical memory, all PTE bits except for the 20-bit PFN are stripped, and the least significant 12 bits of the linear address are added, resulting in a proper 32-bit physical address.
If the physical page is not present, MmGetPhysicalAddress() returns zero.
It is interesting to see that MmGetPhysicalAddress() assumes 4-KB pages for all linear addresses outside the kernel memory range 0x80000000 to 0x9FFFFFFF. Other functions, such as MmIsAddressValid(), first load the PDE of the linear address and check its PS bit to find out whether the page size is 4 KB or 4 MB. This is a much more general approach that can cope with arbitrary memory configurations. Both functions return correct results, because Windows 2000 happens to use 4-MB pages in the 0x80000000 to 0x9FFFFFFF memory area only. Some kernel API functions, however, are apparently designed to be more flexible than others.
The IOCTL Function SPY_IO_CPU_INFO
Several CPU instructions are available only to code running on privilege level zero, which is the most privileged of the four available levels. In Windows 2000 terminology, this means kernel-mode. Among the forbidden instructions are those that read the contents of the control registers CR0, CR2, and CR3. Because these registers contain interesting information, an application might wish to find a way to access them, and the SPY_IO_CPU_INFO function is the solution. As Listing 4-20 shows, the SpyOutputCpuInfo() function invoked by the IOCTL handler uses some ASM inline code to read the control registers, along with other valuable information, such as the contents of the IDT, GDT, and LDT registers and the segment selectors stored in the registers CS, DS, ES, FS, GS, SS, and TR. The Task Register TR contains a selector that refers to the TSS of the current task.
Listing 4-20. Querying CPU State Information
typedef struct _SPY_CPU_INFO { X86_REGISTER cr0; X86_REGISTER cr2; X86_REGISTER cr3; SPY_SEGMENT cs; SPY_SEGMENT ds; SPY_SEGMENT es; SPY_SEGMENT fs; SPY_SEGMENT gs; SPY_SEGMENT ss; SPY_SEGMENT tss; X86_TABLE idt; X86_TABLE gdt; X86_SELECTOR ldt; } SPY_CPU_INFO, *PSPY_CPU_INFO, **PPSPY_CPU_INFO; #define SPY_CPU_INFO_ sizeof (SPY_CPU_INFO) // ----------------------------------------------------------------- NTSTATUS SpyOutputCpuInfo (PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_CPU_INFO sci; PSPY_CPU_INFO psci = &sci; __asm { push eax push ebx mov ebx, psci mov eax, cr0 mov [ebx.cr0], eax mov eax, cr2 mov [ebx.cr2], eax mov eax, cr3 mov [ebx.cr3], eax sidt [ebx.idt.wLimit] mov [ebx.idt.wReserved], 0 sgdt [ebx.gdt.wLimit] mov [ebx.gdt.wReserved], 0 sldt [ebx.ldt.wValue] mov [ebx.ldt.wReserved], 0 pop ebx pop eax } SpySegment (X86_SEGMENT_CS, 0, &sci.cs); SpySegment (X86_SEGMENT_DS, 0, &sci.ds); SpySegment (X86_SEGMENT_ES, 0, &sci.es); SpySegment (X86_SEGMENT_FS, 0, &sci.fs); SpySegment (X86_SEGMENT_GS, 0, &sci.gs); SpySegment (X86_SEGMENT_SS, 0, &sci.ss); SpySegment (X86_SEGMENT_TSS, 0, &sci.tss); return SpyOutputBinary (&sci, SPY_CPU_INFO_, pOutput, dOutput, pdInfo); }
The segment selectors are obtained with the help of the SpySegment() function discussed earlier. See Listing 4-15 for details.
The IOCTL Function SPY_IO_PDE_ARRAY
SPY_IO_PDE_ARRAY is another trivial function that simply copies the entire page-directory from address 0xC0300000 to the caller's output buffer. This buffer has to take the form of a SPY_PDE_ARRAY structure shown in Listing 4-21. As you might have guessed, this structure's size is exactly 4 KB, and it comprises 1,024 32-bit PDE values. The X86_PE structure used here, which represents a generalized page entry, can be found in Listing 4-3, and the constant X86_PAGES_4M is defined in Listing 4-5. Because the items in a SPY_PDE_ARRAY are always page-directory entries, the embedded X86_PE structures are either of type X86_PDE_4M or X86_PDE_4K, depending on the value of the page size bit PS.
It usually is not a good idea to copy memory contents without ensuring that the source page is currently present in physical memory. However, the page-directory is one of the few exceptions. The page-directory of the current task is always present in physical memory while the task is running (Intel 1999c, pp. 3-23). It cannot be swapped out to a pagefile unless another task is switched in. That's why the CPU's Page-Directory Base Register (PDBR) doesn't have a P (present) bit, like the PDEs and PTEs. Please refer to the definition of the X86_PDBR structure in Listing 4-3 to verify this.
Listing 4-21. Definition of SPY_PDE_ARRAY
typedef struct _SPY_PDE_ARRAY { X86_PE apde [X86_PAGES_4M]; } SPY_PDE_ARRAY, *PSPY_PDE_ARRAY, **PPSPY_PDE_ARRAY; #define SPY_PDE_ARRAY_ sizeof (SPY_PDE_ARRAY)
The IOCTL Function SPY_IO_PAGE_ENTRY
If you are interested in the page entry of a given linear address, this is the function of choice. Listing 4-22 shows the internals of the SpyMemoryPageEntry() function that handles this IOCTL request. The SPY_PAGE_ENTRY structure it returns is basically a X86_PE page entry, as defined in Listing 4-3, plus two convenient additions: The dSize member indicates the page size in bytes, which is either X86_PAGE_4K (4,096 bytes) or X86_PAGE_4M (4,194,304 bytes), and the fPresent member indicates whether the page is present in physical memory. This flag must be contrasted to the return value of SpyMemoryPageEntry() itself, which can be TRUE even if fPresent is FALSE. In this case, the supplied linear address is valid, but points to a page currently swapped out to a pagefile. This situation is indicated by bit #10 of the page entryreferred to as PageFile in Listing 4-22being set while the P bit is clear. Please refer to the introduction to the X86_PNPE structure earlier in this chapter for details. X86_PNPE represents a page-not-present entry and is defined in Listing 4-3.
SpyMemoryPageEntry() first assumes that the target page is a 4-MB page, and, therefore, copies the PDE of the specified linear address from the system's PDE array at address 0xC0300000 to the pe member of the SPY_PAGE_ENTRY structure. If the P bit is set, the subordinate page or page-table is present, so the next test checks the PS bit for the page size. If it is set, the PDE addresses a 4-MB page, and the work is doneSpyMemoryPageEntry() returns TRUE, and the fPresent member of the SPY_PAGE_ENTRY structure is set to TRUE as well. If the PS bit is zero, the PDE refers to a PTE, so the code extracts this PTE from the array at address 0xC0000000 and checks its P bit. If set, the 4-KB page comprising the linear address is present, and both SpyMemoryPageEntry() and fPresent report TRUE. Otherwise, the retrieved value must be a page-not-present entry, so fPresent is FALSE, and SpyMemoryPageEntry() returns TRUE only if the PageFile bit of the page entry is set.
Listing 4-22. Querying PDEs and PTEs
typedef struct _SPY_PAGE_ENTRY { X86_PE pe; DWORD dSize; BOOL fPresent; } SPY_PAGE_ENTRY, *PSPY_PAGE_ENTRY, **PPSPY_PAGE_ENTRY; #define SPY_PAGE_ENTRY_ sizeof (SPY_PAGE_ENTRY) // ----------------------------------------------------------------- BOOL SpyMemoryPageEntry (PVOID pVirtual, PSPY_PAGE_ENTRY pspe) { SPY_PAGE_ENTRY spe; BOOL fOk = FALSE; spe.pe = X86_PDE_ARRAY [X86_PDI (pVirtual)]; spe.dSize = X86_PAGE_4M; spe.fPresent = FALSE; if (spe.pe.pde4M.P) { if (spe.pe.pde4M.PS) { fOk = spe.fPresent = TRUE; } else { spe.pe = X86_PTE_ARRAY [X86_PAGE (pVirtual)]; spe.dSize = X86_PAGE_4K; if (spe.pe.pte4K.P) { fOk = spe.fPresent = TRUE; } else { fOk = (spe.pe.pnpe.PageFile != 0); } } } if (pspe != NULL) *pspe = spe; return fOk; }
Note that SpyMemoryPageEntry() does not identify swapped-out 4-MB pages. If a 4-MB PDE refers to an absent page, there is no indication whether the linear address is invalid or the page is currently kept in a pagefile. 4-MB pages are used in the kernel memory range 0x80000000 to 0x9FFFFFFF only. I have never seen one of these pages swapped out, even in extreme low-memory situations, so I was not able to examine any associated page-not-present entries.
The IOCTL Function SPY_IO_MEMORY_DATA
The SPY_IO_MEMORY_DATA function is certainly one of the most important ones, because it copies arbitrary amounts of memory data to a buffer supplied by the caller. As you might recall, user-mode applications are readily passed in invalid addresses. Therefore, this function is very cautious and verifies the validity of all source addresses before touching them. Remember, the Blue Screen is lurking everywhere in kernel-mode.
The calling application requests the contents of a memory block by passing in a SPY_MEMORY_BLOCK structureshown at the top of Listing 4-23that specifies its address and size. For convenience, the address is defined as a union, allowing interpretation as a byte array (PBYTE pbAddress) or an arbitrary pointer (PVOID pAddress). The SpyInputMemory() function in Listing 4-23 copies this structure from the IOCTL input buffer. The companion function SpyOutputMemory(), concluding Listing 4-23, is a wrapper around SpyMemoryReadBlock(), which is shown in Listing 4-24. The main duty of SpyOutputMemory() is to return the appropriate NTSTATUS values while SpyMemoryReadBlock() provides the data.
SpyMemoryReadBlock() returns the memory contents in a SPY_MEMORY_DATA structure, defined in Listing 4-25. I have chosen a different approach than in the previous definitions because SPY_MEMORY_DATA is a data type of variable size. Essentially, it consists of a SPY_MEMORY_BLOCK structure named smb, followed by an array of WORDs called awData[]. The length of the array is determined by the dBytes member of smb. To allow easy definition of SPY_MEMORY_DATA instances as global or local variables of a predetermined size, this structure's definition is based on the macro SPY_MEMORY_DATA_N(). The single argument of this macro specifies the size of the awData[] array. The actual structure definition follows the macro definition, providing SPY_MEMORY_DATA with a zero-length awData[] array. The SPY_MEMORY_DATA__() macro computes the overall size of a SPY_MEMORY_DATA structure given the size of its data array, and the remaining definitions allow packing and unpacking the data WORDs in the array. Obviously, the lower half of each WORD contains the memory data byte and the upper half specifies flags. Currently, only bit #8 has a meaning, indicating whether the data byte in bits #0 to #7 is valid.
Listing 4-23. Handling Memory Blocks
typedef struct _SPY_MEMORY_BLOCK { union { PBYTE pbAddress; PVOID pAddress; }; DWORD dBytes; } SPY_MEMORY_BLOCK, *PSPY_MEMORY_BLOCK, **PPSPY_MEMORY_BLOCK; #define SPY_MEMORY_BLOCK_ sizeof (SPY_MEMORY_BLOCK) // ----------------------------------------------------------------- NTSTATUS SpyInputMemory (PSPY_MEMORY_BLOCK psmb, PVOID pInput, DWORD dInput) { return SpyInputBinary (psmb, SPY_MEMORY_BLOCK_, pInput, dInput); } // ----------------------------------------------------------------- NTSTATUS SpyOutputMemory (PSPY_MEMORY_BLOCK psmb, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { NTSTATUS ns = STATUS_BUFFER_TOO_SMALL; if (*pdInfo = SpyMemoryReadBlock (psmb, pOutput, dOutput)) { ns = STATUS_SUCCESS; } return ns; }
Listing 4-24. Copying Memory Block Contents
DWORD SpyMemoryReadBlock (PSPY_MEMORY_BLOCK psmb, PSPY_MEMORY_DATA psmd, DWORD dSize) { DWORD i; DWORD n = SPY_MEMORY_DATA__ (psmb->dBytes); if (dSize >= n) { psmd->smb = *psmb; for (i = 0; i < psmb->dBytes; i++) { psmd->awData [i] = (SpyMemoryTestAddress (psmb->pbAddress + i) ? SPY_MEMORY_DATA_VALUE (psmb->pbAddress [i], TRUE) : SPY_MEMORY_DATA_VALUE (0, FALSE)); } } else { if (dSize >= SPY_MEMORY_DATA_) { psmd->smb.pbAddress = NULL; psmd->smb.dBytes = 0; } n = 0; } return n; } // ----------------------------------------------------------------- BOOL SpyMemoryTestAddress (PVOID pVirtual) { return SpyMemoryPageEntry (pVirtual, NULL); // ----------------------------------------------------------------- BOOL SpyMemoryTestBlock (PVOID pVirtual, DWORD dBytes) { PBYTE pbData; DWORD dData; BOOL fOk = TRUE; if (dBytes) { pbData = (PBYTE) ((DWORD_PTR) pVirtual & X86_PAGE_MASK); dData = (((dBytes + X86_OFFSET_4K (pVirtual) - 1) / PAGE_SIZE) + 1) * PAGE_SIZE; do { fOk = SpyMemoryTestAddress (pbData); pbData += PAGE_SIZE; dData -= PAGE_SIZE; } while (fOk && dData); } return fOk; }
The validity of a data byte is determined by the function SpyMemoryTest Address(), which is called by SpyMemoryReadBlock() for the address of each byte before it is copied to the buffer. SpyMemoryTestAddress(), included in the lower half of Listing 4-24, simply calls SpyMemoryPageEntry() with the second argument set to NULL. The latter function has just been introduced in the course of the discussion of the IOCTL function SPY_IO_PAGE_ENTRY (Listing 4-22). Setting its PSPY_PAGE_ENTRY pointer argument to NULL means that the caller is not interested in the page entry of the supplied linear address, so all that remains is the function's return value, which is TRUE if the linear address is valid. In the context of SpyMemoryPageEntry(), an address is valid if the page it is contained in is either present in physical memory or resident in one of the system's pagefiles. Note that this behavior is not compatible with the ntoskrnl.exe API function MmIsAddressValid(), which will always return FALSE if the page is not present, even if it is a valid page currently kept in a pagefile. Also included in Listing 4-24 is the function
Listing 4-25. Definition of SPY_MEMORY_DATA
#define SPY_MEMORY_DATA_N(_n) \ struct _SPY_MEMORY_DATA_##_n \ { \ SPY_MEMORY_BLOCK smb; \ WORD awData [_n]; \ } typedef SPY_MEMORY_DATA_N (0) SPY_MEMORY_DATA, *PSPY_MEMORY_DATA, **PPSPY_MEMORY_DATA; #define SPY_MEMORY_DATA_ sizeof (SPY_MEMORY_DATA) #define SPY_MEMORY_DATA__(_n) (SPY_MEMORY_DATA_ + ((_n) * WORD_)) #define SPY_MEMORY_DATA_BYTE 0x00FF #define SPY_MEMORY_DATA_VALID 0x0100 #define SPY_MEMORY_DATA_VALUE(_b,_v) \ ((WORD) (((_b) & SPY_MEMORY_DATA_BYTE ) | \ ((_v) ? SPY_MEMORY_DATA_VALID : 0)))
SpyMemoryTestBlock(), which is an enhanced version of SpyMemoryTestAddress(). It tests a memory range for validity by walking across the specified block in 4,096-byte steps, testing whether all pages it spans are accessible.
Accepting swapped-out pages as valid address ranges has the important advantage that the page will be pulled back into physical memory as soon as SpyMemoryReadBlock() tries to access one of its bytes. The sample memory dump utility presented later would not be quite useful if it relied on MmIsAddressValid(). It would sometimes refuse to display the contents of certain address ranges, even if it was able to display them 5 minutes before, because the underlying page recently would have been transferred to a pagefile.
The IOCTL Function SPY_IO_MEMORY_BLOCK
The SPY_IO_MEMORY_BLOCK function is related to SPY_IO_MEMORY_DATA in that it also copies data blocks from arbitrary addresses to a caller-supplied buffer. The main difference is that SPY_IO_MEMORY_DATA attempts to copy all bytes that are accessible, whereas SPY_IO_MEMORY_BLOCK fails if the requested range comprises any invalid addresses. This function will be needed in Chapter 6 to deliver the contents of data structures living in kernel memory to a user-mode application. It is obvious that this function must be very restrictive. A structure that contains inaccessible bytes cannot be copied safelythe copy would be lacking parts of the data.
Like SPY_IO_MEMORY_DATA, the SPY_IO_MEMORY_BLOCK function expects input in the form of a SPY_MEMORY_BLOCK structure that specifies the base address and size of the memory range to be copied. The returned copy is a faithful 1:1 reproduction of the original data. The output buffer must be large enough to hold the entire copy. Otherwise, an error is reported, and no data is sent back.
The IOCTL Function SPY_IO_HANDLE_INFO
Like the SPY_IO_PHYSICAL function introduced above, this function allows a user-mode application to call kernel-mode API functions that are otherwise unreachable. A kernel-mode driver can always get a pointer to an object represented by a handle by simply calling the ntoskrnl.exe function ObReferenceObjectByHandle(). There is no equivalent function in the Win32 API. However, the application can instruct the spy device to execute the function call on its behalf and to return the object pointer afterward. Listing 4-26 shows the SpyOutputHandleInfo() function called by the SpyDispatcher() after obtaining the input handle via SpyInputHandle(), defined in Listing 4-10.
The SPY_HANDLE_INFO structure at the beginning of Listing 4-26 receives the pointer to the body of the object associated with the handle and the handle attributes, both returned by ObReferenceObjectByHandle(). It is important to call ObDereferenceObject() if ObReferenceObjectByHandle() reports success to reset the object's pointer reference count to its previous value. Failing to do so constitutes an "object reference leak."
Listing 4-26. Referencing an Object by Its Handle
typedef struct _SPY_HANDLE_INFO { PVOID pObjectBody; DWORD dHandleAttributes; } SPY_HANDLE_INFO, *PSPY_HANDLE_INFO, **PPSPY_HANDLE_INFO; #define SPY_HANDLE_INFO_ sizeof (SPY_HANDLE_INFO) // ----------------------------------------------------------------- NTSTATUS SpyOutputHandleInfo (HANDLE hObject, PVOID pOutput, DWORD dOutput, PDWORD pdInfo) { SPY_HANDLE_INFO shi; OBJECT_HANDLE_INFORMATION ohi; NTSTATUS ns = STATUS_INVALID_PARAMETER; if (hObject != NULL) { ns = ObReferenceObjectByHandle (hObject, STANDARD_RIGHTS_READ, NULL, KernelMode, &shi.pObjectBody, &ohi); } if (ns == STATUS_SUCCESS) { shi.dHandleAttributes = ohi.HandleAttributes; ns = SpyOutputBinary (&shi, SPY_HANDLE_INFO_, pOutput, dOutput, pdInfo); ObDereferenceObject (shi.pObjectBody); } return ns;