Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Three Steps Down the Stairs: From Win32 User-Land through Native API to Kernel

4.85/5 (10 votes)
1 Apr 2009CPOL10 min read 56.1K   1.6K  
This project application will travel through the Windows system to finally reach the kernel, from ring 3 to ring 0.

Image

Introduction

As the title says, three steps down the stairs, our project application will "travel" through the Windows system to finally reach the kernel, from ring 3 to ring 0. It will become more and more privileged, more powerful, and more interesting. I will use the most simple code examples here to make it more clear. How will it look like? Take a look at this scheme:

Pure Win32 --> Mixed Win32 & NT --> Pure NT --> Kernel mode driver

In fact, our application is kind of a container for three applications. What are these three apps? The first one is a Win32 user mode program; the next is a pure Windows native app, and the last is a kernel mode driver. Now, the Win32 user-mode program will contain in its code the code for the pure native app, which will be deployed after its execution, and which contains in its code, the code for the kernel mode driver, which will be deployed after the native app execution. Kind of a chain reaction.

What do you need to build everything. First of all, you need a Windows driver development kit, which you can get from the Microsoft website. The next and the last thing, a MinGW package (GCC and stuff). Sure, you can use MS Visual Studio, but I prefer the GCC compiler. Actually, maybe, you will be able to compile the driver with GCC too because it is a very simple driver, but I haven't tried, so I don't know about that.

What is the main goal of this paper? The purpose of this paper is to show, on the most basic example:

  • How a Win32 application could be written with the usage of the Native Windows API
  • How a pure native Windows application looks like
  • A method for including different, independent applications in one application
  • Thread injection with the usage of a Windows Native API example

OK, let's start with the Win32 application.

Using the Code

The first thing I should mention about is a way of packing an executable file into another. To better understand this, let's use a hex editor to see how our executable looks like from the inside.

Image 2

Actually, we need to dump the code into one single char container, which will look like this:

C++
unsigned char my_app [] ={
  0x4d,0x5a,0x90,0x00,0x03,0x00,0x00,0x00
  .............
}

For this, I am using a binary file to source text convertor (v1.01 (c) 1992,93 by Alex Kopylov) which I have included into the package with the source from this paper. But, if you are afraid of unknown applications, the code for dumping the EXE will look like this:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){

    FILE *f;
    int i, c;
    char *arr_name;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s input_file [array_name] [> output_file]\n", argv[0]);
        return 1;
    }
    f = fopen(argv[1], "rb");
    if (f == NULL) {
        fprintf(stderr, "%s: fopen(%s) failed", argv[0], argv[1]);
        return 1;
    }

    if (argc >= 3) arr_name=argv[2]; else arr_name="filedata";
    printf("unsigned char %s[] = {", arr_name);

    for (i=0;;i++) {
        if ((c = fgetc(f)) == EOF) break;
        if (i != 0) printf(",");
        if ((i % 12) == 0) printf("\n\t");
        printf("0x%.2X", (unsigned char)c);
    }

    printf("\n};\n");
    fclose(f);
    return 0;
}
You can easily compile it. Now, you know how to pack an executable into another. There is one more thing you need to know: the size of the packed EXE. Simply check your executable properties like this:

Image 3

Size, that is what you need. Not the size on the disk. As you can see, my EXE has 23490 bytes. Let's keep that in mind ;) OK, we got all that we need for now. The first part of our project is the Win32 application which is responsible for unpacking the native app.

#include "remove.h" // here will be the code for deletion of our win32 exe
#include "NATIVE_CONTAINER.h" // here we will store hex code of our native app
#define ADDTOREG_REG "ADD2REG.REG" // this file will add native app to registry
#define RUN_BAT "run.bat" // this file will execute above file and 
                          // copy native //app to system32 folder
#define NATIVE_NAME "native.exe" // name of native app
#define NATIVE_SIZE 14617 // size of native app

The installation of the native Windows application takes two steps: the executable file should be copied to the system32 folder, and the appropriate Registry value should be added to make our application start before Win32. To achieve this I will use two installation files, written by Mark Russinovich in his example of a Windows native application. The following function will create these two files:

void Create_File(char *filename, char *data){
    FILE *file;
    file=fopen(filename,"w");
    if(!file) return;
    fprintf(file,"%s",data);
    fclose(file);
}

Now, a couple of simple steps to create and install everything that is needed:

// now simple function that will deploy native application

// NATIVE_NAME - is a name of our EXE.
hNat = CreateFile(NATIVE_NAME, GENERIC_WRITE, 0, NULL, 
                  CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL, NULL);

// this will write binary data to created executable file
// native_container - is our char container where binary data is stored
// NATIVE_SIZE - is the exact size of our EXE, if it will not be valid,
//               Native application will fail to start
WriteFile(hNat, native_container, NATIVE_SIZE, &dwWritten, NULL);

// here we are creating file that will add our native application to windows registry,
// native app will be executed after computer reboot
Create_File(ADDTOREG_REG,
  "REGEDIT4\n[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\" 
  "Control\\Session Manager]\n\\"BootExecute\"=hex(7):61,75,74,6f," 
  "63,68,65,63,6b,20,61,75,74,6f,63,68,6b,20,2a,\\\n\00," 
  "6e,61,74,69,76,65,00,00\n"
  // 6e,61,74,69,76,65,00,00 : n, a, t, i, v, e, 0, 0
);

// here we are creating batch file that will install our native app
Create_File(RUN_BAT,
"@echo off\n\
copy native.exe %systemroot%\\system32\\.\n\
regedit /s ADD2REG.REG\n"
);

WinExec(RUN_BAT, SW_SHOW); // executing run.bat
Sleep(6000); // just to make sure everything is done OK
DeleteFile(ADDTOREG_REG); // delete run.bat
DeleteFile(RUN_BAT); // delete add2reg.reg
DeleteFile(NATIVE_NAME); // delete native app from desktop

Sure, I can install my native app using C functions, but I'm just too lazy to write it for this paper :P After all, I want my Win32 app be deleted forever from the machine. I will use for it the thread injection method with the usage of native functions, exported from ntdll.dll. Actually, the code below was originally written probably by Killaloop, I have simply modified it to use the native API. So, we are going to a little bit lower level ;) To use native stuff in our Win32, app we first need to write function prototypes as pointers. Just take a look at the Win32 VirtualAllocEx function prototype (from MSDN):

LPVOID WINAPI VirtualAllocEx(
    __in HANDLE hProcess,
    __in_opt LPVOID lpAddress,
    __in SIZE_T dwSize,
    __in DWORD flAllocationType,
    __in DWORD flProtect
);

WINAPI is a Win32 API function. Now, take a look at a similar native function:

NTSYSAPI
NTSTATUS
NTAPI
NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect );

NTAPI is Native API. To use it in a Win32 executable, we will write it as a pointer:

NTSTATUS
(NTAPI *NtAllocateVirtualMemory)
(HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG ZeroBits,
PULONG RegionSize,
ULONG AllocationType,
ULONG Protect);

Now, we will dynamically link with ntdll.dll and get the function address from it:

BOOL InitializeRoot(){
    HMODULE hNtDLL = GetModuleHandle("ntdll.dll");
    *(FARPROC *)&NtAllocateVirtualMemory = 
       GetProcAddress(hNtDLL, "NtAllocateVirtualMemory");
    return 0;
}

After the above function execution, we use the native function in our Win32 application. Below are the functions that should be initialized before the thread injection routine could be executed:

// this we give us debug privileges
LONG
(__stdcall *RtlAdjustPrivilege)(int,BOOL,BOOL,BOOL *);
// this will do the injection stuff

NTSTATUS
(NTAPI * RtlCreateUserThread)(HANDLE ProcessHandle, 
 PSECURITY_DESCRIPTOR SecurityDescriptor,
 BOOLEAN CreateSuspended,ULONG StackZeroBits,
 PULONG StackReserved,PULONG StackCommit,
 PVOID StartAddress,PVOID StartParameter,
 PHANDLE ThreadHandle,PCLIENT_ID ClientID );

// this will open process for injection
NTSTATUS
(NTAPI *NtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK AccessMask,
POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId );

// this will allocate virtual memory inside target process
NTSTATUS
(NTAPI *NtAllocateVirtualMemory)(HANDLE ProcessHandle, 
 PVOID *BaseAddress,ULONG ZeroBits,
PULONG RegionSize,ULONG AllocationType,ULONG Protect);

// this will write virtual memory into target process
NTSTATUS
(NTAPI *NtWriteVirtualMemory)(IN HANDLE ProcessHandle, 
 IN PVOID BaseAddress, IN PVOID Buffer,
 IN ULONG NumberOfBytesToWrite, 
 OUT PULONG NumberOfBytesWritten OPTIONAL);

// that is simply to close a handle to process
NTSTATUS
(NTAPI *NtClose)(IN HANDLE ObjectHandle);

// that is actually strcpy function, exported from
// ntdll, I know, I know, don't say a word :P
NTSTATUS (NTAPI *NtStrCpy)(char *, char *);

You have probably noticed the new stuff like PCLIENT_ID, POBJECT_ATTRIBUTES, NTSTATUS, etc. These structures should be defined too, like:

#ifndef NTAPI
typedef WINAPI NTAPI;
#endif

typedef LONG NTSTATUS;
typedef LONG KPRIORITY;

typedef struct _CLIENT_ID{
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;
    HANDLE RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG Attributes;
    PVOID SecurityDescriptor; // Points to type SECURITY_DESCRIPTOR
    PVOID SecurityQualityOfService; // Points to type SECURITY_QUALITY_OF_SERVICE
} OBJECT_ATTRIBUTES;

typedef OBJECT_ATTRIBUTES *POBJECT_ATTRIBUTES;

From where does all this come from? There are three main "strategy" objects which provides such information. One is the Windows Driver Development Kit documentation, the next is the http://undocumented.ntinternals.net/ website, and the last is NDK (probably stands for Native Development Kit, I really dunno, you can find it somewhere on the net) which contains the header files arranged for native applications development.

OK, let's head back to the subject. We are going to inject only one thread with one function, that will delete our Win32 executable file. The whole method in this case looks just like you were using the standard way, known for years, and the complete code can be found in the package, so I only present here a few things for those who are unfamiliar with the subject. First, the function declarations which I will use in the remote thread:

typedef DWORD (WINAPI *RAWGETPROCADDRESS)( HMODULE ,LPCSTR);
typedef HMODULE (WINAPI *RAWLOADLIBRARYA)(LPCTSTR );
typedef BOOL (WINAPI *RAWDELETEFILE)(LPCTSTR );
typedef VOID (WINAPI *RAWSLEEP)(DWORD );

In my remote thread, my code will load the needed library, will get the needed procedure address, and execute the loaded function. Next, we are structuring the remote data like:

typedef struct _RemoteStructure {
    char kernel[20];
    char delfile[20];
    char sleep[20];
    char somestring[100];
    DWORD dwGetProcAddress;
    DWORD dwLoadLibraryA;
    DWORD dwFreeLibrary;
    HMODULE hKernel32;
} RemoteStructure;

RemoteStructure my_RemoteStructure,*pmy_RemoteStructure;

Here, I should say that I had a lot of problems with making NtAllocateVirtualMemory to work properly, so I went a little bit across:

LPVOID NTAPI NtVirtualAlloc(IN HANDLE hProcess,IN LPVOID lpAddress, 
       IN SIZE_T dwSize, IN DWORD flAllocationType, IN DWORD flProtect) {
    NTSTATUS Status;
    Status = NtAllocateVirtualMemory(hProcess,(PVOID *)&lpAddress, 
             0,&dwSize,flAllocationType,flProtect);
    if (!NT_SUCCESS(Status))return NULL;
    return lpAddress;
}

Now, to each parameter from our remote structure, we are assigning the needed functions and values like:

my_RemoteStructure.dwGetProcAddress = (DWORD)GetProcAddress(hKernel, "GetProcAddress");

and:

NtStrCpy(my_RemoteStructure.delfile, "DeleteFileA");

Now, we need to open the process for injection; we need the process' PID so we will get it in a standard way with the usage of such functions as CreateToolhelp32Snapshot, Process32First, Process32Next.

#define INJECT_EXE "svchost.exe"
DWORD pID = GetPID(INJECT_EXE);

Now, we have to convert our so called pID to CLIENT_ID and do the injection; here goes the native stuff:

ClientId.UniqueProcess = (HANDLE)pID; // process has its own ID
ClientId.UniqueThread = 0; // and thread has its own ID
InitializeObjectAttributes(&ObjectAttributes, NULL, 0, 
                           NULL, NULL); // in this case its useles
RtlAdjustPrivilege(20, TRUE, AdjustCurrentProcess, &en); // 20 - debugging privileges
Status = NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS , &ObjectAttributes, &ClientId);
if( !NT_SUCCESS( Status ))
    { return FALSE;} //NtOpenProcess will return handle to process
pRemoteThread = NtVirtualAlloc(hProcess, 0, dwThreadSize, MEM_COMMIT |
MEM_RESERVE,PAGE_EXECUTE_READWRITE); // reserve virtual memory size
NtWriteVirtualMemory(hProcess, pRemoteThread, 
                     &RemoteThread, dwThreadSize,0); // --||--
RtlZeroMemory(&my_RemoteStructure,sizeof(RemoteStructure)); //free memory

Here, we are actually assigning the needed functions and values:

pmy_RemoteStructure =(RemoteStructure *)NtVirtualAlloc (hProcess ,
  0,sizeof(RemoteStructure),MEM_COMMIT,PAGE_READWRITE); // allocate remote data memory
  NtWriteVirtualMemory(hProcess,
  pmy_RemoteStructure,&my_RemoteStructure,
  sizeof(my_RemoteStructure),0); // write remote data memory
  RtlCreateUserThread(hProcess, NULL,FALSE, 
  0, NULL, NULL,(PVOID)pRemoteThread,
 (PVOID)pmy_RemoteStructure, 0, 0); // create remote thread
 NtClose(hProcess); // close handle to target process

And now, this is how my remote thread looks like:

DWORD __stdcall RemoteThread(RemoteStructure *Parameter){
    // we will point to first 2 major functions
    // from our remote data structure
    RAWGETPROCADDRESS NtGetProcAddress = 
       (RAWGETPROCADDRESS)Parameter->dwGetProcAddress;
    RAWLOADLIBRARYA NtLoadLibraryA = 
       (RAWLOADLIBRARYA)Parameter->dwLoadLibraryA;
    // all other functions we might need, we will load dynamically
    Parameter->hKernel32 = NtLoadLibraryA(Parameter->kernel);
    RAWDELETEFILE NtDeleteFile = 
       (RAWDELETEFILE)NtGetProcAddress(Parameter->hKernel32,
           Parameter->delfile );
    RAWSLEEP NtSleep = (RAWSLEEP)NtGetProcAddress(
             Parameter->hKernel32, Parameter->sleep );
    while(NtDeleteFile(Parameter->Filename) == 0) {NtSleep(1000);}
        return 0;
}

In this way, you can inject a thread in any Windows process, for example, in svchost.exe on Vista. If you will use such types of injection on Windows XP, make sure your remote thread will terminate itself after its execution, else it will cause high CPU usage by csrss.exe and the host process. Anyway, I have added this part with the thread injection just to show how native APIs usage looks like in Win32 applications. If you are interested in digging inside Windows, check out the React OS project - it is a very cool resource.

Our native application will not do much more than our Win32 app. It will simply deploy a kernel-mode driver, register it, and load before the control over your system will be given to your hands. I've heard that a lot of people got problems with compiling native applications, but it is really an easy task. First of all, do not use MS Visual Studio, use the MinGW package with GCC inside :D. Next, when you have already got your code completed, in the command prompt, type: C:\path\to\gcc\bin\gcc native.c -o native.exe -lntdll -nostdlib -Wl,--subsystem,native,-e,_NtProcessStartup and you'll get your app compiled nicely (of course, if your code contains no errors). Anyway, in the package, I have included a makefile (make.bat) so you will be just asked about your GCC path and the batch script will do the rest. Make sure you have in your MinGW include directory the DDK folder, because our application needs some header files from it. OK, so let us define the most important functions that we will need.

// function to load our driver
NTSTATUS
NTAPI
 
ZwLoadDriver(PUNICODE_STRING DriverServiceName);
 
// initialization of unicode string
VOID
NTAPI
RtlInitUnicodeString(PUNICODE_STRING DestinationString,PCWSTR SourceString);
 
// we need to set privileges before we will load driver
LONG
__stdcall
RtlAdjustPrivilege(int,BOOL,BOOL,BOOL *);
 
// this will create driver file
NTSTATUS
NTAPI
ZwCreateFile(OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, 
IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, 
IN PLARGE_INTEGER AllocationSize OPTIONAL, IN ULONG FileAttributes, 
IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG 
CreateOptions, IN PVOID EaBuffer OPTIONAL, IN ULONG EaLength );

// this will write data to driver file
NTSTATUS
NTAPI
NtWriteFile(IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, 
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, 
OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID Buffer, 
IN ULONG Length, IN PLARGE_INTEGER ByteOffset OPTIONAL, IN PULONG Key OPTIONAL );
 
// create driver registry key
NTSTATUS
NTAPI
NtCreateKey( OUT PHANDLE pKeyHandle, 
IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES
ObjectAttributes, IN ULONG TitleIndex, 
IN PUNICODE_STRING Class OPTIONAL, IN ULONG CreateOptions,
OUT PULONG Disposition OPTIONAL );
 
// set key values
NTSTATUS
NTAPI
NtSetValueKey(IN HANDLE KeyHandle, IN PUNICODE_STRING ValueName, 
IN ULONG TitleIndex OPTIONAL, IN ULONG Type, 
IN PVOID Data, IN ULONG DataSize );
 
// that is like Sleep function
NTSTATUS
NTAPI
NtDelayExecution(IN BOOLEAN Alertable, IN PLARGE_INTEGER DelayInterval );
 
// define some values
#define DRIVER "zzzz.sys"
#define DRIVER_SIZE 2560
#define DRIVER_KEY "zzzz"

Now, the driver binary data will be stored here in the same way like the binary data of this native app inside the Win32 app.

unsigned char DriverData[] = {
    0x4d,0x5a,0x90,0x00,0x03,0x00,0x00,0x00
     .............
}

Now, to make life a little bit easier, here is the function, similar to Sleep, but the time values will be in seconds, not in milliseconds or any other smaller value.

BOOL NtDelayExecutionEx(DWORD dwSeconds){
    LARGE_INTEGER Interval;
    Interval.QuadPart = -(unsigned __int64)dwSeconds * 10000 * 1000;
     NtDelayExecution (FALSE, &Interval);
 
}

Let's take a look at how our native application will register a driver. The first thing here which should be clear is that the native apps uses Unicode strings, so our key will look like this:

// registry path in system format
WCHAR DrvPath[] = L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\";
 
WCHAR DrvName[] = L"zzzz";

And now, we need to convert it to a Unicode string like this:

// some constants ...
HANDLE mainDrvHandle, drvKeyHandle, EnumKeyHandle;
UNICODE_STRING KeyName;
OBJECT_ATTRIBUTES ObjectAttributes, oBj, oa;
 
// start of conversion
KeyName.Buffer = DrvPath;
KeyName.Length = wcslen( DrvPath ) *sizeof(WCHAR);
KeyName.MaximumLength = KeyName.Length + sizeof(WCHAR);
// end of conversion

Now, we need to call the InitializeObjectAttributes function. Actually, it is a macro. From MSDN: The InitializeObjectAttributes macro initializes the opaque OBJECT_ATTRIBUTES structure, which specifies the properties of an object handle to routines that open handles.

InitializeObjectAttributes( &oBj, &KeyName, OBJ_CASE_INSENSITIVE, NULL, NULL );
NtCreateKey( &mainDrvHandle, KEY_ALL_ACCESS, &oBj, 0, NULL, 
             REG_OPTION_NON_VOLATILE,&Disposition );

//In this case NtCreateKey will open our DrvPath and return handle
//to it. Now we we will use that handle to create our
//driver registry key.
 
KeyName.Buffer = DrvName;
KeyName.Length = wcslen( DrvName ) *sizeof(WCHAR);
KeyName.MaximumLength = KeyName.Length + sizeof(WCHAR);
InitializeObjectAttributes( &oBj, &KeyName,OBJ_CASE_INSENSITIVE, mainDrvHandle, NULL );
NtCreateKey(&drvKeyHandle, KEY_ALL_ACCESS, &oBj, 0, NULL,
REG_OPTION_NON_VOLATILE,&Disposition );

Now, look at how the most important key value is created.

WCHAR DrvImagePath[]= L"ImagePath";
WCHAR ImagePathValue[]= L"\\??\\C:\\WINDOWS\\system32\\zzzz.sys";
ValueNameDrv.Buffer = DrvImagePath;
ValueNameDrv.Length = wcslen( DrvImagePath ) *sizeof(WCHAR);
ValueNameDrv.MaximumLength = ValueNameDrv.Length + sizeof(WCHAR);
NtSetValueKey(drvKeyHandle, &ValueNameDrv, 0, REG_EXPAND_SZ, 
  ImagePathValue, wcslen( ImagePathValue) * sizeof(WCHAR)+ sizeof(WCHAR) );

I will not show here all the code from the project, but other values (your application should at least create two values: ImagePath - where it should be looking for a driver, and Type - if you wish to add more values like DisplayName, ErrorControl, Start etc.; check how other services have registered in your system) are created without "+ sizeof(WCHAR)". Our value type is REG_EXPAND_SZ, so it needs extra space for terminating zeros (most likely 2, like this: 00 00). If you create this value without "+ sizeof(WCHAR)", the application will fail to load the driver; it will simply return ((NTSTATUS)0xC0000034L), which means STATUS_OBJECT_NAME_NOT_FOUND.

OK, time to create the driver. As you can see, the initialization of the Unicode string is the same:

FileName.Buffer = L"\\??\\C:\\WINDOWS\\system32\\zzzz.sys";
FileName.Length = wcslen( FileName.Buffer ) *sizeof(WCHAR);
FileName.MaximumLength = FileName.Length + sizeof(WCHAR);
 
//Below is manual initialization of object attributes
ObjectAttributes.Length =sizeof(ObjectAttributes);
ObjectAttributes.RootDirectory = NULL;
ObjectAttributes.ObjectName = &FileName;
ObjectAttributes.Attributes = OBJ_CASE_INSENSITIVE;
ObjectAttributes.SecurityDescriptor = NULL;
ObjectAttributes.SecurityQualityOfService = NULL;
 
//So finally lets create our driver file
if(ZwCreateFile(&outFileHandle, GENERIC_WRITE | SYNCHRONIZE, 
    &ObjectAttributes, &ioStatusBlock,
    &fileSize, FILE_ATTRIBUTE_NORMAL, 0, FILE_SUPERSEDE,
    FILE_SEQUENTIAL_ONLY|FILE_SYNCHRONOUS_IO_NONALERT, 
    NULL, 0 ) != STATUS_SUCCESS){
 
    // set memory for debug message
    RtlZeroMemory(&dbgMessage, sizeof(dbgMessage));
    // init Unicode string
    RtlInitUnicodeString(&dbgMessage, L"ZwCreateFile error!\n");
    NtDisplayString( &dbgMessage ); // display it on blue screen
    NtDelayExecutionEx(2); // sleep for 2 seconds
    NtTerminateProcess( NtCurrentProcess(), 0 );
    // terminate own process
}

And write the driver data to the driver file....

C++
if( NtWriteFile(outFileHandle,NULL, NULL, NULL, &ioStatusBlock,DriverData, 
    DRIVER_SIZE, NULL, NULL) ! = STATUS_SUCCESS){

    RtlZeroMemory(&dbgMessage, sizeof(dbgMessage));
    RtlInitUnicodeString(&dbgMessage, L"NtWriteFile error!\n");
    NtDisplayString( &dbgMessage );
    NtDelayExecutionEx(2);
    NtTerminateProcess( NtCurrentProcess(), 0 );
}

NtClose(outFileHandle); // close handle to file

I think you have already noticed that writing native application code is very similar to writing a driver. You can replace, for example, NtWriteFile with ZwWriteFile, and you get the same functions at this moment. And the last thing, load the driver:

C++
if (ZwLoadDriver(&DriverPath) != STATUS_SUCCESS){
    //if loading driver failed
    RtlInitUnicodeString(&dbgMessage, L"\nDriver load error!\n");
    NtDisplayString( &dbgMessage );
    NtDelayExecutionEx(2);
    NtTerminateProcess( NtCurrentProcess(), 0 );

} else { // if success

    RtlInitUnicodeString(&dbgMessage, L"\nDriver loaded successfully!\n");
    NtDisplayString( &dbgMessage );
}

//At the end our application must terminate itself:
NtTerminateProcess( NtCurrentProcess(), 0 );

At this point, we have two parts of our project explained. Let's take a look at the last part. My driver included in this project has only two silly functions: create a text file in C:\, and write to this file the text OWNED. Here goes the code:

C++
typedef unsigned long DWORD;
 
// main function
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath){
    HANDLE fHandle; // handle to file
    OBJECT_ATTRIBUTES ObjectAttributes; // object attributes
    LARGE_INTEGER Interval; // integer, we will use it for sleep function
    UNICODE_STRING FileName; // file name
    IO_STATUS_BLOCK ioStatusBlock; // IO status block
    DWORD dwSeconds = 10; // time to sleep
    char myString[] = {"OWNED!!!"}; // string to print to file
    Interval.QuadPart = -(unsigned __int64)dwSeconds * 10000 * 1000;
    // initialize our time interval
 
    if(KeGetCurrentIrql() != PASSIVE_LEVEL) return STATUS_INVALID_DEVICE_STATE;
    RtlInitUnicodeString(&FileName, L"\\DosDevices\\C:\\0WN3ED.TXT");
    // our file name -> unicode string

    InitializeObjectAttributes( &ObjectAttributes, &FileName, 
           OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL );
    ZwCreateFile(&fHandle, GENERIC_WRITE, &ObjectAttributes, 
        &ioStatusBlock, NULL, //create file
        FILE_ATTRIBUTE_NORMAL, 0, FILE_OVERWRITE_IF, 
        FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
     ZwWriteFile(fHandle, NULL, NULL, NULL, &ioStatusBlock, myString, 
                strlen(myString), NULL, NULL); // write data to file
    ZwClose(fHandle); // close handle to file
    KeDelayExecutionThread(KernelMode,FALSE,&Interval );
    // sleep for some time to let user read our
    //message from native application
 
    return STATUS_SUCCESS;
}
 
//--------------------------------------------------------------------------------------

And here we go - our three parts ready. You need to compile the driver now. Select from the main Windows menu Driver Build Environment like you see on the picture below:

Image 4

You'll get this command prompt:

Image 5

Now, simply navigate to a folder with the driver source and type: build -c. Next, you need to dump the driver binary data, and you can use the tool I have included: the binary file to source text convertor. Or, compile the source from the very beginning of this paper and use it. Then, include the dumped data into a native application source and compile it with the makefile I have provided and dump it in the same way. And, in the end, include the dumped native app in a Win32 application code, then compile it in the standard way. After all that, just launch an executable and enjoy the trip from user-mode to kernel-mode :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)