INTRODUCTION
NOTE: the example code which this article refers to was extracted from the implementation of the "BugChecker Video Driver" code. BugChecker is still an in-development project and so it is the video driver itself: the code presented herein is incomplete in several aspects and should not be considered for inclusion in a commercial/large-scale product, for example. This article and the accompanying source code have to be considered simply examples of a solution to a rather complex hooking problem.
I have decided to post the source code of the "BugChecker Video Driver" (more informations here) because it shows to the curious low-level programmer a lot of interesting hooking and programming techniques and how very complex hooking problems are resolved in actual code.
This driver is fundamental for the working of the BugChecker debugger driver: it is loaded at the operating system boot time and then it remains active, waiting for an other driver or an user application to send an IOCTL requesting its services. The duty of this driver is to hook some DirectDraw API functions and then notifying the subscribed clients (up to 16) when the display video mode changes.
The information that is returned by this driver is handled in the following structure:
typedef struct _VIDEOMEMORYINFO
{
FLATPTR fpPrimary; // offset to primary surface
DWORD dwFlags; // flags
DWORD dwDisplayWidth; // current display width
DWORD dwDisplayHeight; // current display height
LONG lDisplayPitch; // current display pitch
DDPIXELFORMAT ddpfDisplay; // pixel format of display
DWORD dwOffscreenAlign; // byte alignment for offscreen surfaces
DWORD dwOverlayAlign; // byte alignment for overlays
DWORD dwTextureAlign; // byte alignment for textures
DWORD dwZBufferAlign; // byte alignment for z buffers
DWORD dwAlphaAlign; // byte alignment for alpha
PVOID pvPrimary; // kernel-mode pointer to primary surface
} VIDEOMEMORYINFO;
The most important informations in this structure that interest BugChecker are the pointer to the display framebuffer in kernel memory and the current resolution (width and height) and the layout of the display memory. BugChecker needs to know these nasty details because it has to draw its own interface directly in the video mapped memory: no operating system service (such as GDI) at any level can be used by the debugger because it has to "stay above" the OS itself. In fact, when these details are known, BugChecker becomes independent from the OS and can debug and trace into any portion of the system (including the modules that actually provide graphics and video services...) displaying its own, independent, interface.
I will not discuss here the generic structure of an NT device driver (I assume that the reader has this type of knowledge), but I will go straight explaining the logic and implementation of this software and how I came to define the implementation approach for solving this specific problem. The first step when writing a driver or a user component that does this type of job is to study thoroughly the technology that the software will come to hook/hack. I say this because there can exist many ways to solve the same problem, some equivalent to each other, some less convenient in terms of development time and effort and specially in terms of longevity/maintenance (with respect to possible changes in the software to be hooked in future versions...).
HOW RELEVANT DIRECTDRAW INITIALIZATION WORKS
( in a nutshell ! )
In this specific case, I referred to the Windows 2000 DDK documentation that comes with the DDK standard installation archive.
So, when the system boots up, GDI looks up in the registry and determines which display driver (and video miniport driver) to load. A display driver, as stated in the documentation, is a DLL with the "primary responsibility of rendering". "When an application calls a Win32 function with device-independent graphics requests, the Graphics Device Interface (GDI) interprets these instructions and calls the display driver." The features of the display driver are exposed to GDI via the DDI interface: this is a set of functions that the driver must implement in order to be used by GDI. The display driver initialization itself occurs immediately after the system has loaded the display driver DLL in system space. The only function that the display driver exports to GDI is "DrvEnableDriver" that is actually the entry point itself of the module. Infact, on my system, the display driver that was actually loaded by GDI at boot-up time was "ati2dvai.dll": this dll doesn't export any function. The "DrvEnableDriver" function is the first step in the display driver initialization: GDI calls it passing a pointer to a structure of type DRVENABLEDATA. This structure is initialized to zeroes by GDI before the call, so the driver can fill its fields with relevant informations, that are needed for the initialization process to continue.
typedef struct tagDRVENABLEDATA {
ULONG iDriverVersion;
ULONG c;
DRVFN *pdrvfn;
} DRVENABLEDATA, *PDRVENABLEDATA;
In our case, the most important field is "pdrvfn": this is an array of function pointers that has to be filled by the driver itself. The functions comprised in the array are all the DDI functions that the display driver want to implement and expose to the system. Upon calling this function, the system knows all the pointers to the DDI functions the driver implements.
At this point, we are interested in a specific DDI function: "DrvGetDirectDrawInfo". This function is called as part of the DirectDraw initialization and in Windows 2000 the "DirectDraw initialization sequence is done at boot time and after each mode change" (see documentation for further details...). The prototype of this function is:
BOOL DrvGetDirectDrawInfo(
IN DHPDEV dhpdev,
OUT DD_HALINFO *pHalInfo,
OUT DWORD *pdwNumHeaps,
OUT VIDEOMEMORY *pvmList,
OUT DWORD pdwNumFourCC,
OUT DWORD *pdwFourCC
);
Looking through these parameters, the one that can be relevant for our purposes is the second one: "pHalInfo" of type "DD_HALINFO". The documentation states that the DD_HALINFO structure describes the capabilities of the hardware and driver and that it is required to be filled by the aforementioned "DrvGetDirectDrawInfo" API.
typedef struct _DD_HALINFO{
DWORD dwSize;
VIDEOMEMORYINFO vmiData;
DDNTCORECAPS ddCaps;
PDD_GETDRIVERINFO GetDriverInfo;
DWORD dwFlags;
LPVOID lpD3DGlobalDriverData;
LPVOID lpD3DHALCallbacks;
PDD_D3DBUFCALLBACKS lpD3DBufCallbacks;
} DD_HALINFO, *PDD_HALINFO;
Interestingly, the second field of this structure contains exactly the type of information that we are looking for: the VIDEOMEMORYINFO structure has references to the current video resolution metrics and comprises a pointer to the virtual framebuffer that is mapped in kernel memory.
So, being able to hook DrvGetDirectDrawInfo and allowing the display driver to execute its implementation of this function and being able immediately after to get a pointer to the resulting, filled data structure seems to be the key for resolving our initial problem...
FOLLOWING THE HOOK SEQUENCE
Everything begins in the "DriverEntry" function. Here we create our device object, the corresponding symbolic link and we define our IRP handlers. This is pretty standard to all common NT drivers.
Some interesting thing happens at the end of this function: here we setup using the NT API "PsSetLoadImageNotifyRoutine" a callback function that the system will call every time a system or a user module is mapped in virtual memory.
Besides this, it can be interesting to note that before setting up this callback and so effectively starting up the whole hook mechanism, the driver shows a message to the user asking whether he is sure to proceed with the hook driver initialization. Remember that our driver is loaded at boot time and at that time there is no graphical interface to which delegating the duty to show our warning message. This forces us to draw directly in the video memory our alert message and to poll the keyboard hardware (in this case we refer to PS/2 keyboards) for the stroke of a predefined hotkey that will prevent the "PsSetLoadImageNotifyRoutine" from being called.
This is accomplished by these two functions: WaitForKeyBeingPressed and OutputTextString. The first of the two functions has a pretty simple implementation:
NTSTATUS WaitForKeyBeingPressed( IN BYTE bKeyScanCode, IN ULONG ulElapseInHundredthsOfSec )
{
LARGE_INTEGER liWaitElapse;
ULONG i;
BYTE bKeybPortByte;
// Wait and Read the Keyboard Port.
liWaitElapse = RtlConvertLongToLargeInteger( - 100000 ); // Ten Milliseconds.
for ( i=0; i<ulElapseInHundredthsOfSec; i++ )
{
// Read from the Keyboard Port.
__asm
{
in al, 0x64
test al, 1
mov al, 0
jz _SkipKeybInputPortRead
in al, 0x60
_SkipKeybInputPortRead:
mov bKeybPortByte, al
}
// Check if we have to Exit.
if ( bKeybPortByte == bKeyScanCode )
return STATUS_SUCCESS;
// Wait.
KeDelayExecutionThread( UserMode, FALSE, & liWaitElapse );
}
// Return to the Caller.
return STATUS_UNSUCCESSFUL;
|