Writing drivers to perform kernel-level SSDT hooking
The goal of the reader is to perform the the below points while following the tutorial. The tutorial describes how to do this step by step.
Write a console application to load, and send commands to the driver
Have the driver process commands
Have the driver perform according SSDT hooks, or execute the received command.
There are several chapters to this tutorial. The are as described below.
Installing the Driver Development Kit, and setting up your working folder.
Creating a basic console application to load and start driver services, as well as perform IO to IO Devices.
Building a basic driver
Setting up driver major functions and basic IO
Creating a message protocol.
Getting access to protected memory using MDLs.
Writing the hook.
Before we begin the tutorial please note the following:
If you get any linking errors regarding unresolved external symbols, assure you have all your source files defined in SOURCES(you will learn about SOURCES later in this tutorial)
For debugging the driver, run the drivers in a VM, setting up DbgView client on VM, and then connecting to it on the host machine. This will give you the last debug messages recived before the driver crashed the host machine. And you can easily narrow down errors this way.
This is not worth your time if you plan to write a simple hook with it.
This driver is written in C, there are some big differences between C and C++, they will be noted as you follow the tutorial. If you know C++, you should have no problems following this tutorial.
Any errors, overflows, exceptions, etc will cause a BSOD, and could possibly damage your OS. And thus, when these drivers are being tested it is highly recommended you do testing on a virtual machine.
All drivers written with this tutorial should NOT be installed as services that startup with the operating system. If there are errors in the driver, it can prevent you from starting your computer, to fix this you can boot in to safe mode and manually remove the service.
The drivers you write using this tutorial will most likely be picked up as rootkits, disable your anti-virus during the whole tutorial, or configure it accordingly. This is because hooking the service dispatch table is a common technique used by rootkits to hide processes or files.
I don't know what MPGH's status is on including drivers in your hacks, so be sure to consult them about it before using this technique in hacking.
Lastly, for whatever you're trying to achive with a driver, will probably be achiveable by user-level hooking, so be sure you take advantage of user-level hooks before kernel-level hooks.
Installing the Driver Development Kit, and setting up your working folder
Now lets start to setup our DDK, and our development environment. We're actually going to be installing WDK, which contains the DDK. The DDK contains many useful headers and libraries, as well as the binaries we're going to be using to build our driver. I will start off by saying, Microsoft has successfully made this thee most difficult development kit to acquire(thanks Microsoft).
How to Get the WDK
1. Register a hotmail account if you don't already acquire one.
2. Login at Microsoft Connect - Product Feedback and Bug Reporting | Microsoft Connect
3. After logging in, there will be tabs on the top of the page, select Connection Directory.
4. To the left of the page, there is a navigation menu, select Developer Tools.
5. Search for : Windows Driver Kit (WDK) and Windows Driver Framework (WDF) and apply.
6. Now select 'Your Dashboard' on the top navigation menu.
7. Select Windows Driver Kit (WDK) and Windows Driver Framework (WDF).
8. Finally, press the download link under the text "The download package for this release is:"
8. After that download, either mount the downloaded ISO file, or extract it. Then install it.
After the installation, if you go to start->All Programs->Windows Driver Kits->WDK (version number)->Build Environments->Your OS->
You will see two executables. One is a free build environment, and the other is checked. If you build your driver in the checked environment, your driver will not be optimized, and if it is done in a free environment, it will be optimized. Optimization can sometimes cause errors. And thus drivers should be built in checked, then when they're completed, tested, built, and distributed in free. Throughout this tutorial we'll be using a checked environment.
And there you go, DDK should be located in InstalledDrive:/WinDDK/VersionNumber/
You can setup the DDK to MSVS, but it's not recommended, the staff over at MSDN are saying it wasn't built to compile drivers. There really isn't a need to use VS anyway, the console and notepad will do just fine. I tried setting it up with MSVS and I got a ton of errors. Not worth it for me.
Create a folder in either of your harddisks. Make it easy to access and without spaces. Ex: C:/hack/, not C:/my hack/. Also make it short, because you'll be navigating to it quite oftenly. In that folder you'll need to create two files:
SOURCES
MAKEFILE
No extensions. SOURCES should contain the following:
Code:
TARGETNAME=MYDRIVER
TARGETPATH=OUTPUT
TARGETTYPE=DRIVER
TARGETLIBS= $(BASEDIR)/lib/w2k/i386/ndis.lib
SOURCES=
TARGETNAME: Name of file to output file, excluding .sys extention.
TARGETPATH: The path, from the current directory, where the compiled file will be stored.
TARGETTYPE: The type of file source code is being compiled into.
TARGETLIBS: A list to all the libraries to include in the project. Where $(BASEDIR) is the base directory of wherever you installed the DDK. Usually this is InstalledDriver:/WinDDK/VersionNumber/
SOURCES: defines all the c files to compile and link in the project. Be sure to update this when you create a new source file. This is currently blank, and will be filled in later on in this tutorial.
And MAKEFILE should contain:
Code:
!INCLUDE $(NTMAKEENV)/makefile.def
Which essentially redirects to the makefile provided in the ddk. All drivers will share the same makefile.
Last but not least, download DebugView, which will allow you to view debug output messages from your driver. When you're debugging, you'll want to be viewing the output remotely. You can do this by starting the DebugView client(giving DebugView /c command argument when starting it) on the VM you'll be testing the driver is, then starting DebugView without any arguments on the host machine, and connecting to the local client by going to computer->connect-> and inserting the target machine's IP Address.
After you've got that, you're ready to continue to the next step.
Creating a basic console application to load and start driver services, as well as perform IO to IO Devices
This is probably the easiest part of the tutorial and won't be explained too much. For this part of the tutorial we'll be using C++.
To keep your code neat and organized, start by creating a source file, called serviceTools.h. We want to have a function load a driver using two parameters, the service name, and the path of it's corresponding executable.
Notice this code may be modifed by it's parent tags, and thus refer to download for valid source file.
PHP Code:
#pragma once
#include <stdio.h>
#include <windows.h>
int createService(char* serviceName, char* executablePath, bool relative);
int startService(char* serviceName);
int stopService(char* serviceName);
int deleteService(char* serviceName);
And the corresponding source file:
Notice this code may be modifed by it's parent tags, and thus refer to download for valid source file.
PHP Code:
#include "stdafx.h"
#include "serviceTools.h"
int createService(char* serviceName, char* executablePath, bool relative)
{
char pathBuffer[200];
if(relative)
{
GetCurrentDirectory(200, pathBuffer);
strcat(pathBuffer, "//");
}
strcat(pathBuffer, executablePath);
printf("Creating service %s ", serviceName);
SC_HANDLE sh = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS);
if(sh == INVALID_HANDLE_VALUE)
{
printf("Error opening handle to Service Manager./n");
return -1;
}
SC_HANDLE hService = CreateService(sh, serviceName, serviceName,
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
pathBuffer,
0, 0, 0, 0, 0);
CloseServiceHandle(sh);
if(hService == 0)
{
printf("Error creating service./n");
return -1;
}
printf("Service has been created under the display name of %s/n", serviceName);
CloseServiceHandle(hService);
return 1;
}
int startService(char* serviceName)
{
SC_HANDLE hService;
SC_HANDLE sh = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS);
if(!sh)
{
printf("Error opening service handler handle/n");
return -1;
}
hService = OpenService(sh, serviceName, SERVICE_ALL_ACCESS);
CloseServiceHandle(sh);
if(!hService)
{
printf("Error opening service handle/n");
return -1;
}
if(StartService(hService, 0, 0) != 0)
printf("Service started./n");
else
{
unsigned long eCode = GetLastError();
printf("Error starting service.(%d)/n", eCode);
CloseServiceHandle(hService);
return -1;
}
CloseServiceHandle(hService);
return 1;
}
int stopService(char* serviceName)
{
SC_HANDLE hService;
SC_HANDLE sh = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS);
if(!sh)
{
printf("Error opening service handler handle/n");
return -1;
}
hService = OpenService(sh, serviceName, SERVICE_ALL_ACCESS);
CloseServiceHandle(sh);
if(!hService)
{
printf("Error opening service handle/n");
return -1;
}
SERVICE_STATUS srvStatus;
if(ControlService(hService, SERVICE_CONTROL_STOP, &srvStatus) != 0)
printf("Service stopped./n");
else
{
printf("Error stopping service./n");
CloseServiceHandle(hService);
return -1;
}
CloseServiceHandle(hService);
return 1;
}
int deleteService(char* serviceName)
{
SC_HANDLE hService;
SC_HANDLE sh = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS);
if(!sh)
{
printf("Error opening service handler handle/n");
return -1;
}
hService = OpenService(sh, serviceName, SERVICE_ALL_ACCESS);
CloseServiceHandle(sh);
if(!hService)
{
printf("Error opening service handle/n");
return -1;
}
if(DeleteService(hService) != 0)
printf("Service deleted./n");
else
{
printf("Error deleting service./n");
CloseServiceHandle(hService);
return -1;
}
CloseServiceHandle(hService);
}
The above code is pretty simple, but I will go over createService, after that, the rest are pretty much the same.
First we notify the user a driver is being loaded
printf("Creating service %s ", serviceName);
Then we can go ahead and open a handle to the service control manager.
SC_HANDLE sh = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS);
The first two parameters are optional, the first being the name of the target machine, in our case we want to open the SC Manager of the local machine, and thus this can be set to null. The second parameter is the name of the SC Manager database. MSDN states this should be set to SERVICES_ACTIVE_DATABASE definition, if it is null it is set to this by default.
Now we can create our service.
Code:
SC_HANDLE hService = CreateService(sh, serviceName, serviceName,
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
pathBuffer,
0, 0, 0, 0, 0);
I don't plan to go through all of these parameters, they're all defined at MSDN library. Notice though, that the dwServiceType parameter is set to SERVICE_KERNEL_DRIVER definition. This API will create our service, and then return a handle to the newly created service.
Then we dispose of the handles using CloseServiceHandle, CloseHandle cannot be used when it comes to service related handles, instead CloseServiceHandle is used.
Now that that's over with, lets start writing the IO portion of the code. The objective of this code is to open handles to an IO device, and write to it. Create a new header file, call it deviceIo.h. We want a method to open a handle to an IO device, another to write to it, we wont need to read from the IO device in this tutorial.
PHP Code:
#pragma once
#include <windows.h>
HANDLE GetHandleToIo(char* deviceName);
int CloseIoHandle(HANDLE hIo);
int WriteToDevice(HANDLE hDevice, char* inBuffer);
And the corresponding source file
PHP Code:
#include "stdafx.h"
#include "deviceIo.h"
HANDLE GetHandleToIo(char* deviceName)
{
return (HANDLE)CreateFile(deviceName,
GENERIC_READ | GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
0,
0);
}
int CloseIoHandle(HANDLE hIo)
{
if(hIo == INVALID_HANDLE_VALUE)
return -1;
else
CloseHandle(hIo);
return 1;
}
int WriteToDevice(HANDLE hDevice, char* inBuffer)
{
WriteFile(hDevice, inBuffer, strlen(inBuffer) + 1, 0, 0);
return 1;
}
Pretty simple. No need to go into any details. If you need help with this portion of code, just say so and I will document it a bit more.
Lastly, we need to process all the commands thrown into our console. To do so, we're going to create yet another file called commandProcessor.h.
PHP Code:
#pragma once
#define CMD_UNKNOWN_COMMAND 0
#define CMD_SETUP_SERVICE 1
#define CMD_DELETE_SERVICE 2
#define CMD_START_SERVICE 3
#define CMD_STOP_SERVICE 4
#define CMD_WRITE 5
#define CMD_OPEN 6
#define CMD_CLOSE 7
#include "serviceTools.h"
#include "deviceIo.h"
unsigned int translateCommand(char* command);
int processCommand(char* command);
And the corresponding source file:
PHP Code:
#include "stdafx.h"
#include "commandProcessor.h"
const unsigned long CMD_ID_TABLE[] = {CMD_SETUP_SERVICE,
CMD_DELETE_SERVICE,
CMD_START_SERVICE,
CMD_STOP_SERVICE,
CMD_WRITE,
CMD_OPEN,
CMD_CLOSE,
CMD_QUIT};
const char* CMD_NAME_TABLE[] = {"ss" ,
"ds" ,
"start",
"stop" ,
"write",
"open",
"close",
"quit"};
unsigned int translateCommand(char* command)
{
int tblCmdSize = sizeof(CMD_ID_TABLE) / sizeof(unsigned long);
int tblCmdName = sizeof(CMD_NAME_TABLE) / sizeof(char*);
for(unsigned int i = 0; i < tblCmdSize && i < tblCmdName; i++)
if(!strcmp(command, CMD_NAME_TABLE[i]))
return CMD_ID_TABLE[i];
return CMD_UNKNOWN_COMMAND;
}
int processCommand(char* command)
{
static HANDLE hFileHandle = INVALID_HANDLE_VALUE;
char buffer[200];
ZeroMemory(&buffer, 200);
switch(translateCommand(command))
{
case CMD_SETUP_SERVICE:
scanf("%s", buffer);
createService("MYDRIVER", buffer, true);
break;
case CMD_DELETE_SERVICE:
scanf("%s", buffer);
deleteService(buffer);
break;
case CMD_START_SERVICE:
scanf("%s", buffer);
startService(buffer);
break;
case CMD_STOP_SERVICE:
scanf("%s", buffer);
stopService(buffer);
break;
case CMD_OPEN:
scanf("%s", buffer);
hFileHandle = GetHandleToIo(buffer);
break;
case CMD_CLOSE:
CloseIoHandle(hFileHandle);
break;
case CMD_WRITE:
scanf("%[^/n/t]s", buffer);
WriteToDevice(hFileHandle, buffer);
break;
case CMD_QUIT:
return 0;
break;
default:
printf("Unknown command inserted../n");
}
return 1;
}
Lets tie this all together into main.cpp
PHP Code:
// tutDriverConsole.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "commandProcessor.h"
int main(int argc, char* argv[])
{
char buffer[200];
int ret;
do
{
printf("/n >>");
scanf("%200s", buffer);
ret = processCommand(buffer);
}while(ret);
return 0;
}
Commands for console:
Quote:ss- Setup Service
ds- Delete service
start - start service
stop - stop service
open - open handle to IO device
close - close current IO handle
write - write to opened IO handle
quit - quit
Be sure to close your handle to the IO device, before attempting to stop and delete the service.
And that's about it for the console application. Now lets dive right into creating our first driver.
Building A Basic Driver
Like in a console application, where the entry point of the program is defined as main, the driver's entry point is usually defined as DriverEntry, two parameters are passed to the EP of a driver. The first is a pointer to a DRIVER_OBJECT structure, describing the current driver, and the second is a pointer to an initialized UNICODE_STRING, describing a registry path to the driver's registry key.
Code:
DriverEntry(
__in struct _DRIVER_OBJECT *DriverObject,
__in PUNICODE_STRING RegistryPath
)
Whenever an event occurs, such as a handle to a driver being open to, or the driver being written to, read from, etc there is a corresponding major function that must be defined to handle the event. The major functions we're going to be working with are:
IRP_MJ_CREATE - Called when a handle to driver has been open
IRP_MJ_CLOSE - Called when a handle to driver has been closed
IRP_MJ_WRITE - Called when driver has been written to.
The rest we can redirect to a function that handles unsupported events, such as reads from the driver.
After these major functions have been set, we're ready to create an Io Device, which can be opened and written to by our console application. When this Io Device is written to, our set MajorFunction, IRP_MJ_WRITE will be called to handle the event.
Lets start by first altering SOURCES in your newly setup working folder. You set this folder up in the first chapter of this tutorial. To sources, add entry.c.
Code:
TARGETNAME=MYDRIVER
TARGETPATH=OUTPUT
TARGETTYPE=DRIVER
TARGETLIBS= $(BASEDIR)/lib/w2k/i386/ndis.lib
SOURCES= entry.c
Remember to keep track of this yourself, the tutorial will not remind you every time you create a new source file. Any unresolved external symbols or assume imports is most likely due to you missing a source file.
First lets create the entry header.
PHP Code:
#ifndef ENTRY_H_
#define ENTRY_H_
#include "ntddk.h"
VOID OnUnload(IN PDRIVER_OBJECT driverObject);
#endif
We'll be advancing the header as we get further in the tutorial. Now, lets create the corresponding source file:
PHP Code:
#include "entry.h"
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING regPath)
{
driverObject->DriverUnload = OnUnload;
DbgPrint("My Driver loaded.");
return STATUS_SUCCESS;
}
VOID OnUnload(IN PDRIVER_OBJECT driverObject)
{
DbgPrint("My Driver unloaded.");
}
Now lets build your first driver. Open DebugView, and filter out any distracting messages that aren't from your driver. Then open up the checked build executable(read chapter one for help if you're lost) and type
cd YourWorkingDirectory
build
Ex.
cd C:/myDriver
build
After build has completed executing, you can open up your output folder, which should be in your working directory, and you'll see your first driver. MYDRIVER.sys. Go ahead, compile your console, register and start this driver. In DebugView, when the service is started, you should see "My Driver loaded" When you're done, don't forget to stop and delete the service.
Setting up driver major functions and basic IO
As said above, major functions are set to handle any common events for a driver, such as a child IO Device of the driver being open or written to. So lets setup some basic major functions. First open your SOURCES file, and add majorFunctions.c. Then create a new header file, call it majorFunctions.h.
majorFunctions.h will contain the following:
PHP Code:
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
And the correspond source file:
PHP Code:
#include "majorFunctions.h"
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Handle to IO Device has been opened.");
return NtStatus;
}
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Handle to IO Device has been closed.");
return NtStatus;
}
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pInBuffer = NULL;
DbgPrint("IO Device has been written to.");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp)
{
pInBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
if(pInBuffer)
{
DbgPrint("Processing Message: SZ:%u, STR:%s", pIoStackIrp->Parameters.Write.Length, pInBuffer);
//Later in this tutorial, we'll be writing code to enforce null-terminated strings
}
else
{
DbgPrint("Write called with null buffer pointer.");
}
}
else
{
DbgPrint("Invalid IRP stack pointer..");
}
return NtStatus;
}
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Unsupported Majour Function Requested. Returning STATUS_SUCCESS");
return NtStatus;
}
Major functions have two parameters passed to them when they're called, the first has already been explained, the second is an IRP structure describing the recived message.
The only method here that isn't self-explanatory is Buffered_Write, and I will now go over it.
PHP Code:
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pInBuffer = NULL;
IRP stands for Io Request Packet, and when write is called, there is an IRP placed on the Driver's stack, this IRP structure will contain more information about the message that has been sent to us. There are some pretty advanced sides to this structure, we won't need to go to deep into it though.
PHP Code:
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
This will set our pIoStackIrp pointer to a pointer to the most recent IRP on the stack. The first parameter is the IRP passed to our write event handler. The difference between this IRP and the one passed to our even handler, is that this will contain parameters on the request, Ex buffer length.
PHP Code:
pInBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
Contains a pointer to the recived buffer written to our IO Device. Note this returned string may not be null-terminated, and if it isn't, and it's passed to calls that read it as a character array, it will continue to read until is has encountered a null byte. This null byte might not be in memory with read access, which would throw an exception when read, and ultimatly a cause BSoD.
PHP Code:
pIoStackIrp->Parameters.Write.Length
This is an example of one of the parameters the stack IRP will contain. This will contain the length of our recived buffer. We'll be using this at later time to enforce null terminated strings.
Also notice how the event handler has various if's to catch null pointers. You should know that reading from a null pointer will throw an exception and cause a BSoD.
So now lets setup the major functions.
Your entry.h header file should now contain:
PHP Code:
#ifndef ENTRY_H_
#define ENTRY_H_
#include "ntddk.h"
#include "majorFunctions.h"
void OnUnload(IN PDRIVER_OBJECT driverObject);
#endif
And the source file:
PHP Code:
#include "entry.h"
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING regPath)
{
unsigned int i;
driverObject->DriverUnload = OnUnload;
for(i=0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
driverObject -> MajorFunction[i] = Io_Unsupported;
driverObject->MajorFunction[IRP_MJ_CREATE] = Create_DeviceIo;
driverObject->MajorFunction[IRP_MJ_CLOSE] = Close_DeviceIo;
driverObject->MajorFunction[IRP_MJ_WRITE] = Buffered_Write;
DbgPrint("My Driver loaded.");
return STATUS_SUCCESS;
}
VOID OnUnload(IN PDRIVER_OBJECT driverObject)
{
DbgPrint("My Driver unloaded.");
}
IRP_MJ_MAXIMUM_FUNCTION is defined when ntddk.h is included. It is the maximum major IRP methods in the MajorFunction array. IRP_MJ_CREATE, IRP_MJ_CLOSE, etc are defined indexes in the array. Whenever a major event occurs, this array is looked at to locate the corresponding handler.We first set the whole array to point to our method that handles unsupported events, then we set particular indexes to their corresponding handler. This will ensure everything is handled.
Now we can go ahead and create our IO-Device and symbolic link to it.
An Io device is a device created with the IoCreateDevice API that can be open, and read/written to. A symbolic link pretty much provides another way to access the device. Think of the symbolic link as the domain name, and the actual device as the IP Address in terms of access.
We're first going to create a method that will initilize all of our unicode strings, for methods that require unicode strings to be passed to them. In this case, IoCreateDevice requires a unicode string to be passed to it.
Open your entry header file, and add
int IniIoDevice();
PHP Code:
#ifndef ENTRY_H_
#define ENTRY_H_
#include "ntddk.h"
#include "majorFunctions.h"
void OnUnload(IN PDRIVER_OBJECT driverObject);
int InitUnicodeStrings();
#endif
Now in entry.c, we're going to define a couple of wide-character string and UNICODE_STRING structures to be initilized, as well as create our InitUnicodeStrings function.
PHP Code:
#include "entry.h"
//Define constant unicode strings, which are used to initlize their corresponding UNICODE_STRING structures.
//Name of the IO Device to be created
const WCHAR deviceName[] = L"//Device//MYDRIVER";
//Name of symbolic link to be created
const WCHAR deviceSymbolicLink[] = L"//DosDevices//MYDRIVER";
//Unicode structures, which are to be initilized using their corresponding constant character array.
UNICODE_STRING unicodeDeviceNameBuffer;
UNICODE_STRING unicodeSymLinkBuffer;
//Struct to hold information about our IO Device. More on this later in the tutorial.
PDEVICE_OBJECT g_pDeviceOvject = 0;
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING regPath)
{
unsigned int i;
driverObject->DriverUnload = OnUnload;
for(i=0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
driverObject -> MajorFunction[i] = Io_Unsupported;
driverObject->MajorFunction[IRP_MJ_CREATE] = Create_DeviceIo;
driverObject->MajorFunction[IRP_MJ_CLOSE] = Close_DeviceIo;
driverObject->MajorFunction[IRP_MJ_WRITE] = Buffered_Write;
if(!InitUnicodeStrings())
return STATUS_UNSUCCESSFUL; //report the driver could not be loaded.
DbgPrint("My Driver loaded.");
return STATUS_SUCCESS;
}
int InitUnicodeStrings()
{
//Initilize unicode structures.
RtlInitUnicodeString(&unicodeDeviceNameBuffer, deviceName);
RtlInitUnicodeString(&unicodeSymLinkBuffer, deviceSymbolicLink);
return 1;
}
VOID OnUnload(IN PDRIVER_OBJECT driverObject)
{
DbgPrint("My Driver unloaded.");
}
Read the comment I've placed in the code above.
RtlInitUnicodeString initializes the UNICODE_STRING structure, passed to it's first parameter, to the null-terminated string passed in the second parameter. More information on this API can be found at MSDN.
Next, go back to the header and create a new method header, call it:
int SetupIoDevice(PDRIVER_OBJECT pDriverObject);
In the source file, we will use the corresponding code:
PHP Code:
int SetupIoDevice(PDRIVER_OBJECT pDriverObject)
{
int ret;
DbgPrint("Creating I//O device under name: %ws", unicodeDeviceNameBuffer.Buffer);
ret = IoCreateDevice(pDriverObject,
0,
&unicodeDeviceNameBuffer,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&g_pDeviceObject);
g_pDeviceObject->Flags |= DO_BUFFERED_IO;
g_pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
if(ret != STATUS_SUCCESS)
{
DbgPrint("Error Creating Device");
return -1;
}
DbgPrint("Device Created");
ret = IoCreateSymbolicLink(&unicodeSymLinkBuffer, &unicodeDeviceNameBuffer);
if(ret != STATUS_SUCCESS)
{
DbgPrint("error creating symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, deviceSymbolicLink);
return -1;
}
DbgPrint("Created symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, unicodeSymLinkBuffer.Buffer);
return 1;
}
This is a lot simpler then it looks. First we make a call to IoCreateDevice, and provide the according parameters. Which are defined at MSDN. But hell, I'll relay them to you.
Code:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
The first parameter is a pointer to your DRIVER_OBJECT struct. You pass this through the first parameter of the SetupIoDevice function you created, the second defines the size of the device extensions. We aren't going to have any so we can simply set this to 0. The third is the device name, we pass a pointer to our corresponding initilized UNICODE_STRING structure here, the device type we're going to set to FILE_DEVICE_UNKNOWN, or if you can find a device type appropriate for your driver here: Specifying Device Types go ahead and use that. For the fourth, as I had said, we aren't using any extentions, we can ignore this parameter and set it to 0. The exclusive boolean can be set to true to allow only one open handle to the device at a time. If it it set to false, it will allow multiple handles open to the device. The last parameter is a pointer to a DEVICE_OBJECT structure to retrive information about the created device. When you want to delete an IO Device, you'll need to pass this struct. We're going to store it in a global variable called g_pDeviceObject.
After that we check to see if the device was created successfully, if it isn't, we return -1, otherwise we move on to setting the flags in our newly create device object.
There are various types of device IO, we're going to be using buffered IO in this tutorial, and thus we're going to OR in DO_BUFFERED_IO Which is defined when ntddk.h is included. This tells the IO device we're going to be using a buffered IO method. Then we're going to unset the DEVICE_INTERNALIZING flag. Which is set when the device is first created. We do this by ANDing in it's opposite. If the device's initializing flag remains set, we will not be able to access the device.
Then we create a symbolic link to the device using the IoCreateSymbolicLink API.
The first parameter it takes is a pointer to an initialized UNICODE_STRING structure describing the name of the symbolic link, the second is the name of the device it links to.
PHP Code:
#ifndef MAJORFUNCTIONS_H_
#define MAJORFUNCTIONS_H_
#include "ntddk.h"
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
#endif
PHP Code:
#include "entry.h"
const WCHAR deviceName[] = L"//Device//MYDRIVER";
const WCHAR deviceSymbolicLink[] = L"//DosDevices//MYDRIVER";
UNICODE_STRING unicodeDeviceNameBuffer;
UNICODE_STRING unicodeSymLinkBuffer;
PDEVICE_OBJECT g_pDeviceObject = 0;
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING regPath)
{
unsigned int i;
driverObject->DriverUnload = OnUnload;
for(i=0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
driverObject -> MajorFunction[i] = Io_Unsupported;
driverObject->MajorFunction[IRP_MJ_CREATE] = Create_DeviceIo;
driverObject->MajorFunction[IRP_MJ_CLOSE] = Close_DeviceIo;
driverObject->MajorFunction[IRP_MJ_WRITE] = Buffered_Write;
if(!InitUnicodeStrings())
return STATUS_UNSUCCESSFUL; //report the driver could not be loaded.
SetupIoDevice(driverObject);
DbgPrint("My Driver loaded.");
return STATUS_SUCCESS;
}
int InitUnicodeStrings()
{
RtlInitUnicodeString(&unicodeDeviceNameBuffer, deviceName);
RtlInitUnicodeString(&unicodeSymLinkBuffer, deviceSymbolicLink);
return 1;
}
int SetupIoDevice(PDRIVER_OBJECT pDriverObject)
{
int ret;
DbgPrint("Creating I//O device under name: %ws", unicodeDeviceNameBuffer.Buffer);
ret = IoCreateDevice(pDriverObject,
0,
&unicodeDeviceNameBuffer,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&g_pDeviceObject);
g_pDeviceObject->Flags |= DO_BUFFERED_IO;
g_pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
if(ret != STATUS_SUCCESS)
{
DbgPrint("Error Creating Device");
return -1;
}
DbgPrint("Device Created");
ret = IoCreateSymbolicLink(&unicodeSymLinkBuffer, &unicodeDeviceNameBuffer);
if(ret != STATUS_SUCCESS)
{
DbgPrint("error creating symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, deviceSymbolicLink);
return -1;
}
DbgPrint("Created symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, unicodeSymLinkBuffer.Buffer);
return 1;
}
void OnUnload(IN PDRIVER_OBJECT driverObject)
{
if(g_pDeviceObject)
{
IoDeleteDevice(g_pDeviceObject);
IoDeleteSymbolicLink(&unicodeSymLinkBuffer);
DbgPrint("Deleted IO Device and symbolic link.");
}
DbgPrint("My Driver unloaded.");
}
Notice the bolded text, where I added a call to SetupIoDevice, and where I added the cleanup code for our IO Device.
After that's done, we're nearly ready to test the driver. First, we need to enforce null-terminated strings in our write event. Create a new source in your driver working folder, call it stringTools.h.
PHP Code:
#ifndef STRINGTOOLS_H_
#define STRINGTOOLS_H_
int forceNullTermination(char* string, unsigned int len);
#endif
And the corresponding source:
PHP Code:
#include "stringTools.h"
int forceNullTermination(char* string, unsigned int len)
{
if(string[len] != 0)
string[len] = 0;
return 1;
}
That's pretty simple and doesn't need any explaining.
Now go back to majorFunctions.c and change it accordingly.
Code:
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pInBuffer = NULL;
DbgPrint("IO Device has been written to.");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp)
{
pInBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
if(pInBuffer)
{
forceNullTermination(pInBuffer, pIoStackIrp->Parameters.Write.Length);
DbgPrint("Processing Message: SZ:%u, STR:%s", pIoStackIrp->Parameters.Write.Length, pInBuffer);
}
else
{
DbgPrint("Write called with null buffer pointer.");
}
}
else
{
DbgPrint("Invalid IRP stack pointer..");
}
return NtStatus;
}
As well as the header:
Code:
#ifndef MAJORFUNCTIONS_H_
#define MAJORFUNCTIONS_H_
#include "ntddk.h"
#include "stringTools.h"
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
#endif
And now you're read to build your driver, with basic I/O functionality. Here's the source(refer to download for valid source files, parent tags might modify code)
MAKEFILE
PHP Code:
!INCLUDE $(NTMAKEENV)makefile.def
SOURCES
PHP Code:
TARGETNAME=MYDRIVER
TARGETPATH=OUTPUT
TARGETTYPE=DRIVER
TARGETLIBS= $(BASEDIR)libw2ki386ndis.lib
SOURCES= entry.c majorFunctions.c stringTools.c
entry.h
PHP Code:
#ifndef ENTRY_H_
#define ENTRY_H_
#include "ntddk.h"
#include "majorFunctions.h"
void OnUnload(IN PDRIVER_OBJECT driverObject);
int InitUnicodeStrings();
int SetupIoDevice();
#endif
entry.c
PHP Code:
#include "entry.h"
const WCHAR deviceName[] = L"//Device//MYDRIVER";
const WCHAR deviceSymbolicLink[] = L"//DosDevices//MYDRIVER";
UNICODE_STRING unicodeDeviceNameBuffer;
UNICODE_STRING unicodeSymLinkBuffer;
PDEVICE_OBJECT g_pDeviceObject = 0;
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING regPath)
{
unsigned int i;
driverObject->DriverUnload = OnUnload;
for(i=0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
driverObject -> MajorFunction[i] = Io_Unsupported;
driverObject->MajorFunction[IRP_MJ_CREATE] = Create_DeviceIo;
driverObject->MajorFunction[IRP_MJ_CLOSE] = Close_DeviceIo;
driverObject->MajorFunction[IRP_MJ_WRITE] = Buffered_Write;
if(!InitUnicodeStrings())
return STATUS_UNSUCCESSFUL; //report the driver could not be loaded.
SetupIoDevice(driverObject);
DbgPrint("My Driver loaded.");
return STATUS_SUCCESS;
}
int InitUnicodeStrings()
{
RtlInitUnicodeString(&unicodeDeviceNameBuffer, deviceName);
RtlInitUnicodeString(&unicodeSymLinkBuffer, deviceSymbolicLink);
return 1;
}
int SetupIoDevice(PDRIVER_OBJECT pDriverObject)
{
int ret;
DbgPrint("Creating I//O device under name: %ws", unicodeDeviceNameBuffer.Buffer);
ret = IoCreateDevice(pDriverObject,
0,
&unicodeDeviceNameBuffer,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&g_pDeviceObject);
g_pDeviceObject->Flags |= DO_BUFFERED_IO;
g_pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
if(ret != STATUS_SUCCESS)
{
DbgPrint("Error Creating Device");
return -1;
}
DbgPrint("Device Created");
ret = IoCreateSymbolicLink(&unicodeSymLinkBuffer, &unicodeDeviceNameBuffer);
if(ret != STATUS_SUCCESS)
{
DbgPrint("error creating symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, deviceSymbolicLink);
return -1;
}
DbgPrint("Created symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, unicodeSymLinkBuffer.Buffer);
return 1;
}
VOID OnUnload(IN PDRIVER_OBJECT driverObject)
{
if(g_pDeviceObject)
{
IoDeleteDevice(g_pDeviceObject);
IoDeleteSymbolicLink(&unicodeSymLinkBuffer);
DbgPrint("Deleted IO Device and symbolic link.");
}
DbgPrint("My Driver unloaded.");
}
majorFunctions.h
PHP Code:
#ifndef MAJORFUNCTIONS_H_
#define MAJORFUNCTIONS_H_
#include "ntddk.h"
#include "stringTools.h"
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
#endif
majorFunctions.c
PHP Code:
#include "majorFunctions.h"
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Handle to IO Device has been opened.");
return NtStatus;
}
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Handle to IO Device has been closed.");
return NtStatus;
}
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pInBuffer = NULL;
DbgPrint("IO Device has been written to.");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp)
{
pInBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
if(pInBuffer)
{
forceNullTermination(pInBuffer, pIoStackIrp->Parameters.Write.Length);
DbgPrint("Processing Message: SZ:%u, STR:%s", pIoStackIrp->Parameters.Write.Length, pInBuffer);
}
else
{
DbgPrint("Write called with null buffer pointer.");
}
}
else
{
DbgPrint("Invalid IRP stack pointer..");
}
return NtStatus;
}
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Unsupported Majour Function Requested. Returning STATUS_SUCCESS");
return NtStatus;
}
stringTools.h
PHP Code:
#ifndef STRINGTOOLS_H_
#define STRINGTOOLS_H_
int forceNullTermination(char* string, unsigned int len);
#endif
stringTools.c
PHP Code:
#include "stringTools.h"
int forceNullTermination(char* string, unsigned int len)
{
if(string[len] != 0)
string[len] = 0;
return 1;
}
So lets test this!
1. Open DebugView.
2. Filter out any annoying messages that aren't from your driver.
3. Copy your compile console application into the same directory as your compiled driver.
4. Write in the console:
Quote:ss MYDRIVER.sys
start MYDRIVER
open //./MYDRIVER
write haiThyur
close
stop
ds MYDRIVER
You should see some relevant messages in your DebugView console.
Here's what I got:
Quote:Creating I/O device under name: /Device/MYDRIVER
Device Created
Created symbolic link to /Device/MYDRIVER, named: /DosDevices/MYDRIVER
My Driver loaded.
Handle to IO Device has been opened.
IO Device has been written to.
Processing Message: SZ:4, STR:aaa
IO Device has been written to.
Processing Message: SZ:19, STR:aaaaaaaaaaaaaaaaaa
Unsupported Majour Function Requested. Returning STATUS_SUCCESS
Handle to IO Device has been closed.
If all that worked out, you're ready start programming and designing your own messaging protocol.
Creating A Message Protocol
Our messaging protocol is going to be pretty simple. It will be as displayed below.
command/param1/param2/param3
Before we start building the message processing parts of code, we need to build some methods we can use to do basic operations to our strings. Because we aren't provided with the standard C runtime libraries, we have to create our own methods. In this case, we need to create our own zeroMemory. So we'll start by now writing zeroMemory. Create a new header, call it memory.h.
PHP Code:
#ifndef MEMORY_H_
#define MEMORY_H_
void zeroMemory(void* loc, unsigned long size);
#endif
And the corresponding source file:
PHP Code:
#include "memory.h"
void zeroMemory(char* loc, unsigned long size)
{
unsigned long i = 0;
for(i = 0; i < size; i++)
loc[i] = 0;
}
And now lets move on to GetParam, and other important string related methods I've written for you. Open up stringTools.h
PHP Code:
#ifndef STRINGTOOLS_H_
#define STRINGTOOLS_H_
#include "memory.h"
int forceNullTermination(char* string, unsigned int len);
int getParam(char* source, char* out, unsigned int outSize, unsigned int param, char clipChar);
unsigned int getStrLen(char* str, int includeTerminator, unsigned int maxLen);
int strIsEqual(char* source, char* source2);
#endif
And the source file
PHP Code:
#include "stringTools.h"
int forceNullTermination(char* string, unsigned int len)
{
if(string[len] != 0)
string[len] = 0;
return 1;
}
unsigned int getStrLen(char* str, int includeTerminator, unsigned int maxLen)
{
unsigned int i = 0;
for(i = 0; i < maxLen; i++)
if(str[i] == 0)
break;
return (includeTerminator ? i + 1: i);
}
int getParam(char* source, char* out, unsigned int outSize, unsigned int param, char clipChar)
{
unsigned int clipCount = 0;
unsigned int i = 0;
int len = strlen(source);
zeroMemory((char*)out, outSize);
for(i = 0; i < len; i++)
{
if(source[i] == clipChar)
clipCount++;
if(clipCount == param)
{
int bufCount = 0;
if(source[i] == clipChar)
i++;
for(; i < len; i++)
if(source[i] == clipChar)
break;
else
{
out[bufCount] = source[i];
bufCount++;
}
break;
}
}
return 1;
}
int strIsEqual(char* source, char* source2)
{
unsigned int i;
unsigned int sourceLen;
sourceLen = getStrLen(source, 0, 100);
if(sourceLen != getStrLen(source2, 0, 100))
return 0;
for(i = 0; i < sourceLen; i++)
{
if(source[i] != source2[i])
return 0;
}
return 1;
}
All of these functions are pretty basic. After that's been written, we can continue writing the message protocol.
We're going to start be creating message.h for our driver. We want this to contain a function to convert a received character array, into a struct.
PHP Code:
#ifndef MESSAGE_H_
#define MESSAGE_H_
#include "stringTools.h"
#include "memory.h"
#define MSG_PARAM_SPLIT_CHAR '//'
typedef struct Msg MSG;
struct Msg
{
char command[80];
char param1[80];
char param2[80];
char param3[80];
}Msg;
int GetStructFromChar(char* input, MSG* msgOut);
#endif
And the corresponding source file:
PHP Code:
#include "message.h"
int GetStructFromChar(char* input, MSG* msgOut)
{
zeroMemory((char*)msgOut, sizeof(MSG));
getParam(input, msgOut->command, 80, 0, MSG_PARAM_SPLIT_CHAR);
getParam(input, msgOut->param1 , 80, 1, MSG_PARAM_SPLIT_CHAR);
getParam(input, msgOut->param2 , 80, 2, MSG_PARAM_SPLIT_CHAR);
getParam(input, msgOut->param3 , 80, 3, MSG_PARAM_SPLIT_CHAR);
return 1;
}
Again this is a pretty simple function, Be sure to add all these source files accordingly to SOURCES.
getParam
Param1: Pointer to char array to search for params
Param2: Pointer to Output buffer
Param3: Size of output buffer
Param4: Param number, starting from 0
Param5: char used to split parameters.
Now that this is all setup, we can actually start processing received messages from the command console. So lets create another header file, messageProcessor.h
PHP Code:
#ifndef MESSAGEPROCESSOR_H_
#define MESSAGEPROCESSOR_H_
#include "ntddk.h"
#include "message.h"
#include "stringTools.h"
int processMessage(char* msg);
#endif
We'll be adding to this one a little later. We're going to need the message struct and some of the methods written in message.c, so we're including it's header, we're also going to be comparing strings, and thus we should include stringTools.h. The job of processMessage is so take a string containing what has been written to the device, compile it into a message structure, and then execute functions corresponding to the requested commands and recived parameters.
Now lets create the source file:
PHP Code:
#include "messageProcessor.h"
int processMessage(char* msg)
{
MSG msgBuffer;
GetStructFromChar(msg, &msgBuffer);
DbgPrint("%s", msgBuffer.command);
DbgPrint("%s", msgBuffer.param1);
DbgPrint("%s", msgBuffer.param2);
DbgPrint("%s", msgBuffer.param3);
}
All we're doing here is testing the message protocol. In the next chapter we'll be using this to determine whether to disable or enable a hook. So lets open up majorFunctions.h, have it include messageProcessor.h, then in majorFunctions.c, on write event handler, to call processMessage();
Code:
#ifndef MAJORFUNCTIONS_H_
#define MAJORFUNCTIONS_H_
#include "ntddk.h"
#include "stringTools.h"
#include "messageProcessor.h"
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp);
#endif
Code:
#include "majorFunctions.h"
NTSTATUS Create_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Handle to IO Device has been opened.");
return NtStatus;
}
NTSTATUS Close_DeviceIo(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Handle to IO Device has been closed.");
return NtStatus;
}
NTSTATUS Buffered_Write(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pInBuffer = NULL;
DbgPrint("IO Device has been written to.");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp)
{
pInBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
if(pInBuffer)
{
forceNullTermination(pInBuffer, pIoStackIrp->Parameters.Write.Length);
DbgPrint("Processing Message: SZ:%u, STR:%s", pIoStackIrp->Parameters.Write.Length, pInBuffer);
processMessage(pInBuffer);
}
else
{
DbgPrint("Write called with null buffer pointer.");
}
}
else
{
DbgPrint("Invalid IRP stack pointer..");
}
return NtStatus;
}
NTSTATUS Io_Unsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_SUCCESS;
DbgPrint("Unsupported Majour Function Requested. Returning STATUS_SUCCESS");
return NtStatus;
}
Now that that's over with, we can test it, and then start writing out system service hook on the SSDT.
Getting access to protected memory using MDLs.
When your code is running in ring 0, there are various ways to access memory that shouldn't be accessible. The best method is using MDLs, but we'll be discussing three.
The first and most undocumented method, is changing the flags in the CONTROL 0 register in the processor, know as CR0. Refer to the Intel-Developer manual on specifics of the flags.
Another method, which isn't nearly convenient is modifying a registry key Microsoft allows you to access. Located here:
Code:
HKLM/SYSTEM/CurrentControlSet/Control/Session Manager/Memory/Management/EnforceWriteProtection
Altering the key is a messy inefficient and unreliable method, and lastly, we can use MDLs, which I will describe now.
MDL stands for Memory Descriptor list. You can use an MDL to describe a memory region, as well as alter the regions security. The System Service Dispatch Table is read only, and thus, in order to change the addresses of the services in the table, we're going to need to write to it, and thus we're going to need to change the table's permissions. More on the SSDT in the next chapter, we're only concerend about getting write rights to the SSDT in this chapter.
First of all, where is the SSDT located, and how can we find it? Well, the kernel actually exports a structure called the KeServiceDescriptorTable. Because there is really no reason for the average program to be modifying this table, and there really isn't a need to do so in Microsoft's eyes, the KeServiceDescriptorTable isn't documented by Microsoft itself. Infact, you need to define the structure yourself. If you google around, this is a structure that is well documented by the user, and is defined like so:
PHP Code:
typedef struct SystemServiceDescriptorTable {
void* ServiceTableBase;
void* ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char* ParamTableBase;
} SSDT;
I'll try to leave as much as I can for the next chapter, but I don't really think I can :S. The first parameter is the base address of the system service table, the third defines the number of services in the table, and the last one is a pointer to a table called the param table. I'm not 100% sure how a 64bit OS works when it comes to this, but I can presume the addresses in the Service Table are double the size. Anyway, the service table contains all the addresses to all the services. And the param table defines the size, in bytes, that the function takes as parameters.
Say 0x10000000 is the base address for the service table base.
0x10000000: 0xFirstPointerToASystemService
0x10000004: 0xSecondPointerToASystemService
0x10000008: 0xThirdPointerToASystemService
and 0x20000000 was the param table base address
0x20000000: (BYTE) size of first service's params
0x20000001: (BYTE) size of the second service's params
0x20000002: etc..
So in theory, the memory we need to get write access to is
ServicesTableBasePtr to ServicesTableBasePtr + (NumberOfServices * sizeOfAddress)
On a 32bit machine, the sizeOfAddress variable would be 4 bytes, or 32bits. I can only assume on 64 bit machine, it is 64 bits.
Create a new header file, called ssdt.h
PHP Code:
#ifndef SSDT_H_
#define SSDT_H_
#include "ntddk.h"
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)
typedef struct SystemServiceDescriptorTable {
void* ServiceTableBase;
void* ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char* ParamTableBase;
} SSDT;
int ssdt_init();
int ssdt_deinit();
#endif
I'm first going to explain the SYSCALL_INDEX macro I find useful, which I got from various websites. Basically, all of the addresses of Zw* functions, begin with the opcodes mov eax, xxx. Where xxx is an unsigned long defining the functions index in the SSDT.
Ex, say it were mov eax, 10000000
In bytes this is:
B8 00 00 00 10.
Cut off the first byte(mov instruction), and convert the remaining into an unsigned long and you have it's index in the SSDT.
ssdt_init will remove protection of the SSDT, and allows us to write to it and map the memory region into our driver, ssdt_deinit, will do exactly the opposite.
PHP Code:
#include "ssdt.h"
unsigned long* g_pMappedSCT;
MDL* g_pMdlSCT;
int g_iInitStatus;
__declspec(dllimport) SSDT KeServiceDescriptorTable;
int ssdt_init()
{
g_pMdlSCT = MmCreateMdl(0,KeServiceDescriptorTable.ServiceTableBase,
KeServiceDescriptorTable.NumberOfServices * 4);
if(!g_pMdlSCT)
{
g_iInitStatus = -1;
return -1;
}
MmBuildMdlForNonPagedPool(g_pMdlSCT);
g_pMdlSCT->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
g_pMappedSCT = MmMapLockedPages(g_pMdlSCT, KernelMode);
g_iInitStatus = 1;
return g_iInitStatus;
}
int ssdt_deinit()
{
if(g_iInitStatus)
{
MmUnmapLockedPages(g_pMappedSCT, g_pMdlSCT);
IoFreeMdl(g_pMdlSCT);
}
return 1;
}
In ssdt_init
First we make a call MmCreateMdl, describing the region of memory we want to create a MDL for. This will allocate an MDL for us, and then return a pointer to it. After that we need to update the MDL, so it describes the memory region we've requested, and to do this we can call MmBuildMdlForNonPagedPool, which takes a pointer to the MDL you wish to have updated. Then we can start modifying the page. We're going to be modifying the flags, so we can write to it. The MDL Flags is another poorly documented part of windows, setting this bit will allow us to map this memory region into our driver, and perform IO operations to the memory region. And then we call MmMapLockedPages, which returns a pointer to the first mapped page, we need to store this pointer for hooking, described later on, and unmapping the mapped pages.
The exact opposite is performed in ssdt_deinit.
First we assure the ssdt_init call was a success. If it failed and we try to perform this operation, we will encounter a blue screen of death for making use of uninitialized variables. Then we unmap the memory region, and free our previously allocated MDL.
Lets make a call to ssdt_init on our DriverEntry function, and ssdt_deinit on our driver unload function. First open up entry.h, and add ssdt.h, then open the source file, and add the call to ssdt_init. Like so:
Code:
#ifndef ENTRY_H_
#define ENTRY_H_
#include "ntddk.h"
#include "majorFunctions.h"
#include "ssdt.h"
void OnUnload(IN PDRIVER_OBJECT driverObject);
int InitUnicodeStrings();
int SetupIoDevice();
#endif
Code:
#include "entry.h"
const WCHAR deviceName[] = L"//Device//MYDRIVER";
const WCHAR deviceSymbolicLink[] = L"//DosDevices//MYDRIVER";
UNICODE_STRING unicodeDeviceNameBuffer;
UNICODE_STRING unicodeSymLinkBuffer;
PDEVICE_OBJECT g_pDeviceObject = 0;
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING regPath)
{
unsigned int i;
driverObject->DriverUnload = OnUnload;
for(i=0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
driverObject -> MajorFunction[i] = Io_Unsupported;
driverObject->MajorFunction[IRP_MJ_CREATE] = Create_DeviceIo;
driverObject->MajorFunction[IRP_MJ_CLOSE] = Close_DeviceIo;
driverObject->MajorFunction[IRP_MJ_WRITE] = Buffered_Write;
if(!InitUnicodeStrings())
return STATUS_UNSUCCESSFUL; //report the driver could not be loaded.
DbgPrint("ssdt inited with return code: %d", ssdt_init());
SetupIoDevice(driverObject);
DbgPrint("My Driver loaded.");
return STATUS_SUCCESS;
}
int InitUnicodeStrings()
{
RtlInitUnicodeString(&unicodeDeviceNameBuffer, deviceName);
RtlInitUnicodeString(&unicodeSymLinkBuffer, deviceSymbolicLink);
return 1;
}
int SetupIoDevice(PDRIVER_OBJECT pDriverObject)
{
int ret;
DbgPrint("Creating I//O device under name: %ws", unicodeDeviceNameBuffer.Buffer);
ret = IoCreateDevice(pDriverObject,
0,
&unicodeDeviceNameBuffer,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&g_pDeviceObject);
g_pDeviceObject->Flags |= DO_BUFFERED_IO;
g_pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
if(ret != STATUS_SUCCESS)
{
DbgPrint("Error Creating Device");
return -1;
}
DbgPrint("Device Created");
ret = IoCreateSymbolicLink(&unicodeSymLinkBuffer, &unicodeDeviceNameBuffer);
if(ret != STATUS_SUCCESS)
{
DbgPrint("error creating symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, deviceSymbolicLink);
return -1;
}
DbgPrint("Created symbolic link to %ws, named: %ws", unicodeDeviceNameBuffer.Buffer, unicodeSymLinkBuffer.Buffer);
return 1;
}
VOID OnUnload(IN PDRIVER_OBJECT driverObject)
{
if(g_pDeviceObject)
{
IoDeleteDevice(g_pDeviceObject);
IoDeleteSymbolicLink(&unicodeSymLinkBuffer);
DbgPrint("Deleted IO Device and symbolic link.");
}
ssdt_deinit();
DbgPrint("My Driver unloaded.");
}
Now you're ready to start writing the hook.
Writing The Hook
And now... We can finally write our hook. Open ssdt.h and add
unsigned long ssdt_writeService(unsigned long sysServiceFunction, unsigned long hookAddr);
PHP Code:
#ifndef SSDT_H_
#define SSDT_H_
#include "ntddk.h"
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)
typedef struct SystemServiceDescriptorTable {
void* ServiceTableBase;
void* ServiceCounterTableBase;
unsigned int NumberOfServices;
char* ParamTableBase;
} SSDT;
int ssdt_init();
int ssdt_deinit();
unsigned long ssdt_writeService(unsigned long sysServiceFunction, unsigned long hookAddr);
#endif
And the corresponding source file:
PHP Code:
unsigned long ssdt_writeService(unsigned long sysServiceFunction, unsigned long hookAddr)
{
unsigned long ret;
if(!g_iInitStatus)
return 0;
ret = (unsigned long)InterlockedExchange((PULONG)&g_pMappedSCT[SYSCALL_INDEX(sysServiceFunction)], (long)hookAddr);
return ret;
}
The only thing here that really need to be explained is the InterlockedExchange API. First of all, consider using InterlockedExchange64 when working with a 64 bit machine. In this case, I'm developing for a 32 bit machine. So I'm fine using InterlockedExchange. This exchanges a long(32 bits) worth of data at the address provided in the first param with the 32 bits of data provided in the second parameter. It then returns the orginal value, before the exchange occured. The advantage of using InterlockedExchange, is that it will prevent one thread from reading from the address, while our driver's thread is writing to it. Avoiding half-read pointers from being read, and causing a BSOD when a call to an inexistant method has been made(ex, half written to).
Only thing left to do is incorporate this hook with one of the system services. And have our message processor handle incoming messages to enable and disable the hook. First things first, lets setup our hooking code, then our message processor.
Lets start by creating a new header file, called hooking.h. In here we're going to have all hooked related code, and code that manages hooking requests. Of course we're going to be making use of our previously created ssdt.h.
PHP Code:
#ifndef HOOKING_H_
#define HOOKING_H_
#include "ntddk.h"
#include "ssdt.h"
#include "stringTools.h"
int HookZwQuerySystemInformation(char* param2, char* param3);
#endif
Later we'll be adding function prototypes and pointers to write the actual hook, but for now we're setting the console IO segment of the hook. HookZwQuerySystemInformation will create and disable the hook. We're going to pass it param2 and param3 to make use of(if it needs to make use of them). That's giving our message processor 2 level of input, and our hook managing code two input levels, seems fair enough.
And the corresponding source file
PHP Code:
#include "hooking.h"
int HookZwQuerySystemInformation(char* param2, char* param3)
{
if(strIsEqual(param2, "set"))
DbgPrint("Performing hook on ZwQuerySystemInformation");
else
DbgPrint("Removing hook on ZwQuerySystemInformation");
return 1;
}
Open up messageProcessor.h, add hooking.h. Then open messageProcess.c and start to make dicissions based on recived commands. Here's what I've got:
PHP Code:
#ifndef MESSAGEPROCESSOR_H_
#define MESSAGEPROCESSOR_H_
#include "ntddk.h"
#include "message.h"
#include "stringTools.h"
int processMessage(char* msg);
#endif
PHP Code:
#include "messageProcessor.h"
int processMessage(char* msg)
{
MSG msgBuffer;
GetStructFromChar(msg, &msgBuffer);
if(strIsEqual(msgBuffer.command, "hook.ssdt.ss"))
if(strIsEqual(msgBuffer.param1, "ZwQuerySystemInformation"))
HookZwQuerySystemInformation(msgBuffer.param2, msgBuffer.param3);
else
DbgPrint("Driver does not know how to hook this method.");
else
DbgPrint("Unknow command requested from console");
return 1;
}
HookZwQuerySystemInformation is a function we created back in hooking.c.
And you're good to go, give that a test, it should work to what you expect. Then we can start writing the actual hooking code.
In the console, type:
Quote:ss MYDRIVER.sys
start MYDRIVER
open //./MYDRIVER
write hook.ssdt.ss/ZwQuerySystemInformation/set
write hook.ssdt.ss/ZwQuerySystemInformation/unset
close
stop MYDRIVER
ds MYDRIVER
DebugView output
Quote:ssdt inited with return code: 1
Creating I/O device under name: /Device/MYDRIVER
Device Created
Created symbolic link to /Device/MYDRIVER, named: /DosDevices/MYDRIVER
My Driver loaded.
Handle to IO Device has been opened.
IO Device has been written to.
Processing Message: SZ:42, STR:hook.ssdt.ss/ZwQuerySystemInformation/set
Performing hook on ZwQuerySystemInformation
IO Device has been written to.
Processing Message: SZ:44, STR:hook.ssdt.ss/ZwQuerySystemInformation/unset
Removing hook on ZwQuerySystemInformation
No actual hooks were performed. This was just assuring you've got the basic IO code setup. Now lets finish up the hooking.
Open up hooking.h. First we're going to add the ZwQuerySystemInformation function prototype, the hook function header, and then define ZwQuerySystemInformation, which will then be imported, like so:
PHP Code:
#ifndef HOOKING_H_
#define HOOKING_H_
#include "ntddk.h"
#include "ssdt.h"
#include "stringTools.h"
typedef NTSTATUS (*ZwQuerySystemInformationPrototype)(ULONG SystemInformationCLass,PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);
NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
NTSTATUS Hook_ZwQuerySystemInformation(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);
int HookZwQuerySystemInformation(char* param2, char* param3);
#endif
and the corresponding source file
PHP Code:
#include "hooking.h"
ZwQuerySystemInformationPrototype origSysInfo = 0;
int HookZwQuerySystemInformation(char* param2, char* param3)
{
if(strIsEqual(param2, "set"))
{
if(!origSysInfo)
{
DbgPrint("Performing hook on ZwQuerySystemInformation.");
(unsigned long)origSysInfo = ssdt_writeService((unsigned long)ZwQuerySystemInformation, (long)Hook_ZwQuerySystemInformation);
}
else
{
DbgPrint("Hook has already been applied.");
}
}
else
{
if(origSysInfo)
{
DbgPrint("Removing hook on ZwQuerySystemInformation.");
ssdt_writeService((unsigned long)ZwQuerySystemInformation, (long)origSysInfo);
origSysInfo = 0;
}
else
{
DbgPrint("No current hook has been applied.");
}
}
return 1;
}
NTSTATUS Hook_ZwQuerySystemInformation(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength)
{
DbgPrint("ZwQuerySystemInformation hook called. Redirecting to orignal method.");
return origSysInfo(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
}
Our hook is pretty simple, it just calls DbgPrint, saying the call has been made, then calls the original method. Feel free to do some research on this method and elaborate a little more, filtering out processes and files.