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.

 

Back to the main page