Assembler
|
One of our strongest beliefs is that every programmer worth his salt should understand assembler. Our view is however a minority view and 99.9999999% of all programmers in the world do not understand any assembly. So we thought why not reduce one 9 from the above percentage. Thus this chapter will teach you all about assembler assuming you know nothing about it. On a more serious note some great looking code for device drivers on the net is written only in assembler. So it makes sense to learn assembler. Also when it comes to hard core programming at times C gives way and we have to embed assembler in our C code.
P1
y.c
#include <stdio.h>
#include <windows.h>
#include <malloc.h>
#include <tlhelp32.h>
#include <stdio.h>
#define DRV_NAME "vijayd"
#define DRV_FILENAME "vijay.sys"
#define DIRECTORY "C:\\driverasm"
typedef struct
{
unsigned short Length;
unsigned short MaximumLength;
char * Buffer;
} ANSI_STRING, *PANSI_STRING;
typedef struct
{
unsigned short Length;
unsigned short MaximumLength;
unsigned short *Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
long (_stdcall * _RtlAnsiStringToUnicodeString)(PUNICODE_STRING DestinationString,PANSI_STRING SourceString,unsigned char);
VOID (_stdcall *_RtlInitAnsiString)(PANSI_STRING DestinationString,char * SourceString);
long (_stdcall * _ZwLoadDriver)(PUNICODE_STRING DriverServiceName);
long (_stdcall * _ZwUnloadDriver)(PUNICODE_STRING DriverServiceName);
ANSI_STRING aStr;
UNICODE_STRING uStr;
HMODULE hntdll;
unsigned long byteRet;
HANDLE hDevice;
HKEY hkey;
DWORD val,b;
char *imgName = "System32\\DRIVERS\\"DRV_FILENAME;
void main(int argc, char* argv[])
{
hntdll = GetModuleHandle("ntdll.dll");
_ZwLoadDriver = GetProcAddress(hntdll, "NtLoadDriver");
_ZwUnloadDriver = GetProcAddress(hntdll, "NtUnloadDriver");
_RtlAnsiStringToUnicodeString = GetProcAddress(hntdll,
"RtlAnsiStringToUnicodeString");
_RtlInitAnsiString = GetProcAddress(hntdll,
"RtlInitAnsiString");
if ( strcmp(argv[1],"-i") == 0)
{
CopyFile(DIRECTORY"\\"DRV_FILENAME,"C:\\winnt\\system32\\drivers\\"DRV_FILENAME,1);
RegCreateKey(HKEY_LOCAL_MACHINE,"System\\CurrentControlSet\\Services\\"DRV_NAME,&hkey);
val = 1;
RegSetValueEx(hkey, "Type", 0, REG_DWORD, (PBYTE)&val,
sizeof(val));
RegSetValueEx(hkey, "ErrorControl", 0, REG_DWORD,
(PBYTE)&val, sizeof(val));
val = 3;
RegSetValueEx(hkey, "Start", 0, REG_DWORD, (PBYTE)&val,
sizeof(val));
RegSetValueEx(hkey,"ImagePath",0,REG_EXPAND_SZ,(PBYTE)imgName,strlen(imgName));
_RtlInitAnsiString(&aStr,"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\"DRV_NAME);
_RtlAnsiStringToUnicodeString(&uStr, &aStr, TRUE);
val = _ZwLoadDriver(&uStr);
//hDevice = CreateFile("\\\\.\\"DRV_NAME, GENERIC_WRITE |
GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
//printf("Val=%d hDevice=%x",val,hDevice);
//DeviceIoControl (hDevice, 2 << 3 , 0, 0, 0, 0, &b, 0);
}
if ( strcmp(argv[1],"-u") == 0)
{
_RtlInitAnsiString(&aStr,"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\"DRV_NAME);
_RtlAnsiStringToUnicodeString(&uStr, &aStr, TRUE);
_ZwUnloadDriver(&uStr);
DeleteFile("C:\\winnt\\system32\\drivers\\"DRV_FILENAME);
RegDeleteKey(HKEY_LOCAL_MACHINE,
"System\\CurrentControlSet\\Services\\"DRV_NAME"\\Enum");
RegDeleteKey(HKEY_LOCAL_MACHINE,
"System\\CurrentControlSet\\Services\\"DRV_NAME);
}
}
The only change we have made in the loader program is to change the
directory name to driverasm. The batch file z.bat also remains the same as
before. We install and uninstall the driver as before, y –I and y –u.
z.bat
cl y.c advapi32.lib
P2
r.asm
.386
.model flat, stdcall
DbgPrint PROTO C :DWORD, :VARARG
.data
string1 db "Vijay2",0
.code
DriverEntry proc d:DWORD , r:DWORD
invoke DbgPrint, offset string1
mov eax, 0
ret
DriverEntry endp
end DriverEntry
Lets start with the smallest driver that displays my name in the
DbgPrint program. To start with all assembler programs start with words that
start with a dot. These are called directives. They change the mood or the type
of code the assembler generates. Intel has introduced a zillion processors like
the 386, 586, Pentium etc.
Each new family of microprocessors introduces some new instructions. The
assembler would like to know which chip you want to run the final code on so
that it can use the instruction set of that chip. Thus if we specify .586, the
code generated may use instructions found on the 586 chip.
This will make sure that our code will not run on machines less than a
586 say a 386 chip. Thus the first directive is always the processor name. All
code that we see uses /386 even though we do not know of anyone who uses a 386
machine any more. Thus use the .586 or .686 directive for the Pentium class
machines. The second directive .model tells us the assembler two things.
First is the memory model that should be used. In the good old days of
the 8086 processor, a pointer was denoted by a segment and a offset value.
Today all this is history and thus we will use the word flat always with the
model directive. The second value is the calling convention to be used for
functions.
Two possible values are stdcall and C. In the Windows world we prefer
the stdcall calling convention. This is not that important and optional but as
normally if we call or create a
function and do not specify a calling convention, this directive helps the
assembler to decide which calling convention to use. Thus the memory model can
be tiny, small , large etc. We will always use flat. This is followed by the
calling convention used, options here are pascal, syscall , stdcall, basic etc.
Finally we can also specify a stack option.
Only the memory model is mandatory and the default is C.
As mentioned before we will be calling the function DbgPrint. We need to
specify the function prototype by specifying the name followed by the
parameters. The first parameter is a char * in C, but in assembler we use DWORD
to specify 4. The point we are making is that in assembler we specify data
types at a lower level.
We specify the number of bytes that data type occupies. Thus DWORD is
another word for 4. The second parameter onwards for DbgPrint is variable and
thus we use VARARG. In C we would have used three dots to specify a variable
number of parameters. The reserved word PROTO is followed by the calling
convention which we use C.
Had we used stdcall, we would have got an error as we cannot use the
stdcall calling convention when we have a variable number of arguments. This is
because as the callee restores the stack it needs to know how many parameters
it has been passed. This number is only available with the caller.
If we do no specify the calling convention after proto, the default of
stdcall gets used and we get an error. If we specify for the calling convention
after the model directive, then we do have to specify PROTO but not the calling
convention for the function prototype for DbgPrint.
Thus to sum up, a function prototype is a must for every function we are
calling. The calling convention after PROTO is optional, the system will use
the calling convention specified by the model directive. In this specific
example we do not have to specify the VARARG as we are calling DbgPrint with
only a single parameter.
Thus a prototype begins with a word or a label followed by the directive
PROTO. Everything else is optional. The default calling convention is C.
If we were asked to write a C compiler or an assembler we would choose a
assembler as it is simpler. When we use
a programming language like C, we do specify what our data is placed and where
is code. It is the job of the compiler to take all our data and place it in one
section of the file and the code in another and the resources in the third.
When our exe file is loaded into memory, data , code, resources are laid
out in separate sections of memory. A section contains similar entities. Thus
in our assembler program we have to use the data directive to specify that our
data begins. All our variables are created here. Then we use the code directive
to specify our code. These are the two most basic sections present in our exe
file.
In our data section we create a variable by specifying its name first,
in our case string1. We next specify its size, db means 1 bytes and then the
actual value. As this is a string we are allowed to use double inverted commas
but as we want to null terminate the string we end with a 0. As we need to
create a single variable we then start with the code directive.
To create a function we start with the name or label of the function. In
our case we need a function called DriverEntry. This is followed by the
directive proc. Then we specify the parameters to be passed to this function or
procedure. Each parameter starts with a name then a colon and its data type.
We know that the driver entry function gets called with two parameters
and as each one has a data type of a
pointer we specify DWORD or 4 bytes as their size. We call them d and r. Now we
have to call a function DbgPrint. We use the Invoke directive where we specify
the function name DbgPrint and a comma and then the parameters passed. Here we
need the address of the string string1 to be passed.
The offset directive gives us the address of a variable or a function.
We need to pass the address of the string string1 which the DbgPrint function
will display.
In C the return statement simply places a value in the eax register and
quits out. In assembler we use the mov
instruction to mov a value in a register. A register is nothing but a named
area in a microprocessor. The syntax for mov is source first and then
destination. This is how we move 0 into the eax register.
The OS will call the DriverEntry function of our driver, wait for it to finish
and then look into the eax register to tell it whether the driver was
successful or not. The value STATUS_SUCCESS equals 0. The ret instruction is
needed at the end of every function in assembler. This internally denotes end
of function.
We also need to specify that our function is over by using the endp
directive. Thus we start a function with its name and its name. We end a
function also with the same name and the directive endp. Finally we have to use
the end directive to say end of module or program and also to specify that the
first function to be called is DriverEntry.
a.bat
set path=c:\winddk\2600.1106\bin\x86;%PATH%
b.bat
ml /c r.asm
link /driver /out:vijay.sys /subsystem:native r.obj
c:\winddk\2600.1106\lib\wxp\i386\ntoskrnl.lib
We now have two bat files to be used. The first one simply sets the path
variable to a certain winddk directory. This is because the program ml is
stored there. Ml is the assembler that we will be using. We will run a.bat only
the first time in the directory.
In b.bat we first run the assembler with one option /c to compile to obj
and not call the linker which is the default behavior for cl also. We then call
our good old linker link specifying the /driver option which is optional as we
are specifying /subsystem:native that specifies that we want a device driver.
Adding /driver makes no difference but most programs we see on the net use this
option. The rest remain the same as before.
When we run the driver using y –I, we see the DbgPrint output Vijay2 in
DbgView, DeviceTree also shows our driver vijayd. The only problem is that it
does not uninstall as our driver has no Driver Unload function. Lets set it
right.
P3
r.asm
.386
.model flat, stdcall
DbgPrint PROTO C :DWORD,:VARARG
.data
string1 db "Vijay1",0
string2 db "Unload",0
.code
DriverUnLoad proc d:DWORD
invoke DbgPrint, offset string2
ret
DriverUnLoad endp
DriverEntry proc d:DWORD , r:DWORD
invoke DbgPrint, offset string1
mov eax,d
mov [eax+34h],offset DriverUnLoad
mov eax, 0
ret
DriverEntry endp
end DriverEntry
We know that the first parameter passed to the DriverEntry function is
the address of a pointer to a DRIVER_OBJECT structure. This structure has a
member DriverUnLoad that we set to the address of a function. We looked at this
structure in the ntddk header file and found it was at a offset 34h from the
beginning.
So all that we do is move this pointer d into the eax register using the
mov instruction. To find out the address of a variable we use offset , to find
out the address of a function we also use the directive offset. Thus offset
DriverUnLoad will gives us the address of the function, and we want to place
this in 34h from the start of the pointer d.
So we add 34h to the variable d and wherever we use a * or a -> in C,
in assembler we use [] brackets. Thus [] means a indirection. This is how we
set a member of a structure in assembler. To create the DriverUnLoad function,
we start with a label DriverUnLoad and then the name proc followed by the name
of the single parameter passed.
We have created one more string string2 and invoke function DbgPrint. If
we forget the ret, the assembler does not complain but we get a blue screen of
death. Now both y –I and y –u work like
before.
P4
r.asm
.386
.model flat, stdcall
DbgPrint PROTO C :DWORD,:VARARG
.data
string1 db "Vijay4",0
string2 db "Unload",0
.code
DriverUnLoad proc d:DWORD
invoke DbgPrint, offset string2
ret
DriverUnLoad endp
DriverEntry proc d:DWORD , r:DWORD
push offset string1
call DbgPrint
add esp,4
mov eax,d
mov [eax+34h],offset DriverUnLoad
mov eax, 0
ret
DriverEntry endp
end DriverEntry
Back in the good old days of DOS, when we first started learning about
assembler programming there was no invoke instruction. We would do things the
hard way. Before we call a functions, it expects its parameters in a area of
memory called the stack.
To place something on the stack we use the push instruction. Thus we
first push the address of the variable string1 on the stack using this push
instruction. There is a register called esp that tracks the stack or tells us
where the stack is currently. Thus the push instruction decrements the stack by
4 as the stack moves downwards.
We then use the call instruction that actually calls the function whose
name we specify. As function DbgPrint uses the C calling convention, we have to
restore the stack by adding 4 to esp
using the add instruction. If the stack is not restored all hell breaks loose.
The invoke directive does all this for us. It pushes the parameters on the
stack, calls the function and also restores the stack if the callee does not.
Thus from now on we will always use the invoke directive to call a
function, but remember the way the oldies would do it. At times it takes fun
out of assembler programming.
P5
r.asm
.386
.model flat, stdcall
DbgPrint PROTO C :DWORD,:VARARG
PWSTR typedef PTR WORD
UNICODE_STRING STRUCT
_Length WORD ?
MaximumLength WORD ?
Buffer PWSTR ?
UNICODE_STRING ENDS
PUNICODE_STRING typedef PTR UNICODE_STRING
.data
string1 db "Vijay4",0
string2 db "Unload",0
string3 db "%S",0
.code
DriverUnLoad proc d:DWORD
invoke DbgPrint, offset string2
ret
DriverUnLoad endp
DriverEntry proc d:DWORD , r:PUNICODE_STRING
invoke DbgPrint, offset string1
mov eax,r
assume eax:PUNICODE_STRING
invoke DbgPrint, addr string3 ,
addr [eax].Buffer
mov eax,d
assume eax:nothing
mov [eax+34h],offset DriverUnLoad
mov eax, 0
ret
DriverEntry endp
end DriverEntry
The DriverEntry function is passed a pointer to a UNIOCDE_STRING. So far
have treated it like a long and not a pointer to a structure. To create
anything in assembler, we start with a label or name and then specify its type.
Thus as we want to create a structure UNICODE_STRING, we start with this name
followed by the reserved word struct.
We end the structure by the same name UNICODE_STRING followed by the
reserved word ENDS. In between we specify all our members. Each member starts
with its name, then the data type, the first two members have a data type of
short or i.e. WORD. The last is Buffer whose type is PWSTR which is another
type as we will explain later. Finally we have to specify a value for the
members of the structure ? means no value or initializer.
Like the typedef of C we are allowed to create our own types. The
internal PTR type means a pointer of C. To create our own type we start as
always with the name PWSTR, followed with the obvious keyword typedef and then
a list of predefined types. In the case of PWSTR we use PTR WORD which makes it
a pointer to a short.
This is the how we create the UNICODE_STRING structure in C also. The
type PUNICODE_STRING is nothing but a PTR or pointer to a UNIOCDE_STRING
structure.
In the DriverEntry function we replace the DWORD after the last
parameter to a PUNICODE_STRING type. We place this value of r in the eax
register. We then use the assume directive to type cast the value in the eax
register to a PUNICODE_STRING type. The assume directive takes name of register
colon followed by a type.
From now on the assembler will assume that register eax contains a
pointer to a structure UNICODE_STRING. The DbgPrint function is now passed 2
parameters, the first is the address of a string string3 whose value is
%S. This is because we want to display
the value of a Unicode string.
The last parameter is the Buffer member which stores the string. As we
have cast the value in eax to a pointer to a structure, the [] brackets give us
the actual structure. Then we use the dot notation to reference the actual
member Buffer. This is not enough as we have to pass the address of this member
and thus use the addr keyword. A slight confusion, for the address of a
function use offset, for the address of a variable use offset or addr.
For the address of a member of a structure use addr. Confusion
personified. At the end we once again use the assume directive to set the type
of eax to nothing which is the default value. If we do not, then the line where
we mov the offset of the DriverUnLoad function will give us an error.
P6
r.asm
DriverEntry proc d:DWORD , r:PUNICODE_STRING
invoke DbgPrint, offset string1
mov eax,r
invoke DbgPrint, offset string3 , addr (UNICODE_STRING PTR [eax]).Buffer
mov eax,d
mov [eax+34h],offset
DriverUnLoad
mov eax, 0
ret
DriverEntry endp
What if there was no assume keyword, then what would we do. We first get
the structure the pointer is pointing to by using [eax]. The PTR tells the
assembler it is a pointer and the UNIOCDE_STRING tells the assembler that we
have a structure of a certain type.
Then we use a dot to get at the Buffer member and the addr keyword gives
us the address of the buffer member. As there are many ways to skin a cat, we
will use any of the above two ways depending upon the phases of the moon.
P7
r.asm
.386
.model flat, stdcall
DbgPrint PROTO C :DWORD, :VARARG
IRP_MJ_MAXIMUM_FUNCTION equ 1Bh
PWSTR typedef PTR WORD
PVOID typedef PTR
UNICODE_STRING STRUCT
_Length WORD ?
MaximumLength WORD ?
Buffer PWSTR ?
UNICODE_STRING ENDS
PUNICODE_STRING typedef PTR UNICODE_STRING
DRIVER_OBJECT STRUCT
_Type
SWORD ?
_Size
SWORD ?
DeviceObject
PVOID ?
Flags
DWORD ?
DriverStart
PVOID ?
DriverSize
DWORD ?
DriverSection
PVOID ?
DriverExtension
PVOID ?
DriverName
UNICODE_STRING <>
HardwareDatabase
PVOID ?
FastIoDispatch
PVOID ?
DriverInit
PVOID ?
DriverStartIo
PVOID ?
DriverUnload
PVOID ?
MajorFunction
PVOID
(IRP_MJ_MAXIMUM_FUNCTION + 1) dup(?)
DRIVER_OBJECT ENDS
PDRIVER_OBJECT typedef PTR DRIVER_OBJECT
.const
string1 db "Vijay2",0
string2 db "Unload",0
string3 db "%x",0
.code
DriverUnload proc p:PDRIVER_OBJECT
invoke DbgPrint, addr string2
ret
DriverUnload endp
DriverEntry proc p:DRIVER_OBJECT, registry:PUNICODE_STRING
invoke DbgPrint, addr string1
assume eax:ptr DRIVER_OBJECT
mov eax,p
mov [eax].DriverUnload, offset DriverUnload
assume eax:nothing
mov eax, 0
ret
DriverEntry endp
end DriverEntry
We now create a DRIVER_OBJECT structure using the Struct keyword. This is
a pretty large structure and the PVOID type is nothing but a typedef for a PTR.
If we do not specify a initializer for a structure member a ? is a must. For a
structure within a structure we use <> instead.
The DriverUnLoad member is 34h bytes from the start and as we have set
the type of eax to PDRIVER_OBJECT , we do not use the offset but a more decent
dot DriverUnload instead. The last member is the MajorFunction array. We use a
() brackets to supply a size. The word IRP_MJ_MAXIMUM_FUNCTION is no #define
but a equ to a value 1B. This is how we have created an array 1c large. The
dup(?) repeats the ? mark that many times.
P8
r.asm
.386
.model flat, stdcall
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
.data
string1 db "Vijay2",0
string2 db "Unload",0
.code
DriverUnload proc p:PDRIVER_OBJECT
invoke DbgPrint, addr string2
ret
DriverUnload endp
DriverEntry proc p:PDRIVER_OBJECT, r:PUNICODE_STRING
invoke DbgPrint, addr string1
assume eax:ptr DRIVER_OBJECT
mov eax,p
mov [eax].DriverUnload, offset DriverUnload
assume eax:nothing
mov eax, 0
ret
DriverEntry endp
end DriverEntry
If we start creating these structures ourselves, we will never ever
write code in assembler. For some reason Microsoft choose not to gives us include
files for ml. They have given us header files for C/C++. As we mentioned before
rootkits is an international phenomena.
There are a group of Russians at www.freewebs.com/four-f/. Here you
can download a file kmdkit17.zip. When you unzip this file in a directory and
run install, it creates a directory masm32 in C drive root. Here we find a list
of inc files that ml understands. We copied some fragments of the inc files
where we displayed the structures UNICODE_STRING and DRIVER_OBJECT. Thus we
have installed the above kit, we assume you also have.
The same code as above but now all our structure definitions are present
in the inc files ntoskrnl.inc and ntddk.inc. Look at the time and effort that
these guys put in to convert header
files from the ddk to assembler header files. They have also given us a set of
lib files but we choose to use the ones from the ddk as lib files carry no
code. The kmdkit also contains a 100 page tutorial in assembler. We read it, so
should you.
P9
r.asm
.386
.model flat, stdcall
option casemap:none
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
.const
CCOUNTED_UNICODE_STRING "\\Device\\vijayd",
g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\DosDevices\\vijayd",
g_usSymbolicLinkName, 4
.data
string1 db "Vijay6",0
string2 db "Unload",0
string3 db "First If",0
string4 db "Second If",0
.code
DriverUnload proc p:PDRIVER_OBJECT
invoke DbgPrint, addr string2
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax,p
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
ret
DriverUnload endp
DriverEntry proc p:PDRIVER_OBJECT, r:PUNICODE_STRING
local pDeviceObject:PDEVICE_OBJECT
invoke DbgPrint, addr string1
invoke IoCreateDevice, p,0,addr
g_usDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,addr pDeviceObject
.if eax == STATUS_SUCCESS
invoke DbgPrint, addr string3
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr
g_usDeviceName
.if eax == STATUS_SUCCESS
invoke DbgPrint, addr string4
.endif
.endif
mov eax,p
assume eax:ptr DRIVER_OBJECT
mov [eax].DriverUnload, offset DriverUnload
assume eax:nothing
mov eax, 0
ret
DriverEntry endp
end DriverEntry
Vijay6
First If
Second If
Unload
The above program creates for us a symbolic link for use by CreateFile
function and then deletes them. In C all variables that we create in a function
are called local variables. We use the local keyword to create a variable
pDeviceObject of type PDEVICE_OBJECT which is where we will store our newly
created device object. This local keyword has to be present only at the
beginning of our procedure.
We then use invoke to call our function IoCreateDevice. We pass it the
DRIVER_OBJECT parameter p, then the extension size 0, our device name vijayd,
our device number unknown, two zeroes and the last parameter being the address
of a DEVICE_OBJECT pointer as we use the addr keyword.
This function will create a Device for us and if successful return 0 in
the eax register. We use the if keyword to check the value of the eax register.
If it is STATUS_SUCCESS which is a equ for 0, we enter the if statement. In the
good old days assembler programming was a pain as they was no if or while
keywords.
In the if statement we execute a DbgPrint and then the function
IoCreateSymbolicLink where we pass addresses of the two names Symbolic and Device vijayd. If this function also
returns 0 we display another DbgPrint statement. In the DriverUnLoad function
we first call IoDeleteSymbolicLink passing the address of the Symbolic name.
For the IoDeleteDevice we need the DeviceObject pointer and what we have a
DRIVER_OBJECT pointer.
So we move this value into eax. Cast it to a DRIVER_OBJECT pointer and
then retrieve the DeviceObject member which we pass to the IoDeleteDevice
function. All this without using assume.
In C programming we used to create a structure UNICODE_STRING and then
use the Rtl functions to initialize this structure. In assembler we have the
concept of macros that bring in lots of code and simplifies life for us.
The Russians gave us a macro CCOUNTED_UNICODE_STRING that does lots f
things. It creates a structure UNICODE_STRING variable of the second parameter
g_usDeviceName and sets it to the string we specify in the first. Even though
we specify a ascii string the macro converts it into a Unicode string and initializes the structure for us.
All that we do is use this macro for creating our structure and then use
addr variable name later in our code. The directive .const is like .data but we
are not allowed to change the variable value ever again. The last parameter is
the alignment which we will not explain now but keep it at 4 always. To see the
effect we hope you have uncommented the last three lines of y.c
P10
r.asm
.386
.model flat, stdcall
option casemap:none
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
.const
CCOUNTED_UNICODE_STRING "\\Device\\vijayd",
g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\DosDevices\\vijayd",
g_usSymbolicLinkName, 4
.data
string1 db "Vijay6",0
string2 db "Unload",0
string3 db "First If",0
string4 db "Second If",0
string5 db "DispatchFunction ",0
string6 db "DispatchIOCTLFunction ",0
.code
DispatchIOCTLFunction proc pdo:PDEVICE_OBJECT, pIrp:PIRP
invoke DbgPrint, addr string6
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchIOCTLFunction endp
DispatchFunction proc pdo:PDEVICE_OBJECT, pIrp:PIRP
invoke DbgPrint, addr string5
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchFunction endp
DriverUnload proc p:PDRIVER_OBJECT
invoke DbgPrint, addr string2
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax,p
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
ret
DriverUnload endp
DriverEntry proc p:PDRIVER_OBJECT, r:PUNICODE_STRING
local pDeviceObject:PDEVICE_OBJECT
invoke DbgPrint, addr string1
invoke IoCreateDevice, p,0,addr
g_usDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,addr pDeviceObject
.if eax == STATUS_SUCCESS
invoke DbgPrint, addr string3
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr
g_usDeviceName
.if eax == STATUS_SUCCESS
invoke DbgPrint, addr string4
mov eax,p
assume eax:ptr DRIVER_OBJECT
mov [eax].MajorFunction[IRP_MJ_CREATE*4], offset DispatchFunction
mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL *4], offset
DispatchIOCTLFunction
mov [eax].DriverUnload, offset DriverUnload
.endif
.endif
mov eax,p
assume eax:nothing
mov eax, 0
ret
DriverEntry endp
end DriverEntry
First If
Second If
DispatchFunction
DispatchIOCTLFunction
Unload
In the second if statement we set the MajorFunction member IRP_MJ_CREATE
like before to the address of a function DispatchFunction. The only difference is that we have to
multiply by 4 the offset as assembler unlike does not do pointer arithmetic for
us.
We also set the IRP_MJ_DEVICE_CONTROL offset of the MajorFunction array
to our function DispatchIOCTLFunction. This is why each time we called the
DeviceIoControl function from user space, the above function gets called. The
Create offset of the MajorFunction array has to be initialized or else
CreateFile returns an error. The code for the moment in the two functions is
identical.
The second parameter is a pointer to a IRP packet, the first to our
device object. We place this value in a register eax and set its type to a
pointer to a Irp structure. We move 0 to the IoStatus member and 0 in the
Information member. We then call the function IofCompleteRequest.
The fastcall keyword calls the function using the fastcall calling
convention which places parameter values in registers and not on the stack.
This makes the function run a nanosecond faster as we do not have the overhead
of creating and tearing down a stack. The function us passed a Irp pointer that
we were passed. No change in the way we did it in C.
The keyword option casemap is used because we have used a typename and
variable name to be the same in the dispatch functions pIrp and PIRP.
P11
r.asm
.386
.model flat, stdcall
option casemap:none
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
.const
CCOUNTED_UNICODE_STRING "\\Device\\vijayd",
g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\DosDevices\\vijayd",
g_usSymbolicLinkName, 4
.data
string1 db "Vijay6",0
string2 db "Unload",0
string3 db "First If",0
string4 db "Second If",0
string5 db "DispatchFunction ",0
string6 db "DispatchIOCTLFunction ",0
string7 db "%d %x",0
.code
DispatchIOCTLFunction proc uses esi edi pdo:PDEVICE_OBJECT, pIrp:PIRP
local pid:DWORD
Invoke DbgPrint, addr string6
mov esi, pIrp
assume esi:ptr _IRP
IoGetCurrentIrpStackLocation esi
mov edi, eax
assume edi:ptr IO_STACK_LOCATION
mov eax,[esi].AssociatedIrp.SystemBuffer
mov ebx, [eax]
mov ecx,[edi].Parameters.DeviceIoControl.IoControlCode
invoke DbgPrint , addr string7 , ebx , ecx
assume edi:nothing
assume esi:ptr _IRP
fastcall IofCompleteRequest, esi, IO_NO_INCREMENT
mov eax, 0
ret
ret
DispatchIOCTLFunction endp
DispatchFunction proc pdo:PDEVICE_OBJECT, pIrp:PIRP
invoke DbgPrint, addr string5
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchFunction endp
DriverUnload proc p:PDRIVER_OBJECT
invoke DbgPrint, addr string2
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax,p
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
ret
DriverUnload endp
DriverEntry proc p:PDRIVER_OBJECT, r:PUNICODE_STRING
local pDeviceObject:PDEVICE_OBJECT
invoke DbgPrint, addr string1
invoke IoCreateDevice, p,0,addr
g_usDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,addr pDeviceObject
.if eax == STATUS_SUCCESS
invoke DbgPrint, addr string3
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr
g_usDeviceName
.if eax == STATUS_SUCCESS
invoke DbgPrint, addr string4
mov eax,p
assume eax:ptr DRIVER_OBJECT
mov [eax].MajorFunction[IRP_MJ_CREATE*4], offset DispatchFunction
mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL *4], offset
DispatchIOCTLFunction
mov [eax].DriverUnload, offset DriverUnload
.endif
.endif
mov eax,p
assume eax:nothing
mov eax, 0
ret
DriverEntry endp
end DriverEntry
y.c
hDevice = CreateFile("\\\\.\\"DRV_NAME, GENERIC_WRITE |
GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
printf("Val=%d hDevice=%x",val,hDevice);
val = 7;
DeviceIoControl(hDevice, 4 << 3 , &val , 4, 0, 0, &b, 0);
Vijay6
First If
Second If
DispatchFunction
DispatchIOCTLFunction
7 20
Unload
We are following somewhat the same structure that we used when we
explained how you could write device drivers in C. Looking at y.c we are
passing the address of the val variable and before we set its value to 7. We
have also set the control code to 0x20. We want to read these values in our
driver.
Coming back to the driver, the only change we make is to the
DispatchIOCTLFunction function. When we call a function good programming
practice says that when the function exits, the registers should have the same
value that they had when we entered the function. Thus every assembler function
starts with pushing the value of registers it is going to change on the stack.
The last lines would then pop the registers off the stack. Saving
registers is only something that is recommended and normally not followed.
Instead of doing this drudgery ourselves, the proc keyword can be optionally followed
by a uses and a list of registers that the function modifies. It is then the
responsibility of the compiler to take add code that will save these registers
on the stack and then restore them.
Even though we change lots of registers in our code, we are only asking
the function to save and restore two esi and edi. We first move the parameter
pIrp into the esi register and then use the assume keyword to set esi as a
pointer to a IRP structure. We then call the function
IoGetCurrentIrpStackLocation passing it a parameter Irp and then move this
value into the edi register.
We cast this value into a IO_STACK_LOCATION pointer. We want to access the SystemBuffer parameter
and store this into the eax register. This SystemBuffer is actually a pointer
to a long so we use the [] to access the four bytes it is pointing to into the
ebx register. As edi is pointer to a stack location we move the IoControlCode
member into the ecx register and display these values.
The same way we did it in C, a little extra hard work in assembler.