Windows Native Applications and Acronis Active Restore

Today, we continue the story of how we, together with the guys from Innopolis University, are developing Active Restore technology to allow the user to start working on their machine as soon as possible after a failure. We will talk about native Windows applications, including the features of their creation and launch. Under the cut – a little about our project, as well as a practical guide on how to write native applications.

In previous posts, we already talked about what Active Restore is and how students from Innopolis develop the service. Today I want to dwell on native applications, to the level of which we want to “bury” our active recovery service. If everything works out, then we can:

  • Much earlier to start the service itself
  • Much earlier to contact the cloud in which the backup lies
  • It’s much earlier to understand what mode the system is in – normal boot or recovery
  • To restore much less files in advance
  • Allow the user to get started even faster.

What is a native application in general?

To answer this question, let's look at the sequence of calls that the system makes, for example, if a programmer in his application tries to create a file.


Pavel Yosifovich – Windows Kernel Programming (2019)

The programmer uses the CreateFile function, which is declared in the fileapi.h header file and is implemented in Kernel32.dll. However, this function itself does not create a file, it only checks the arguments at the input and calls the NtCreateFile function (the Nt prefix just indicates that the function is native). This function is declared in the winternl.h header file and is implemented in ntdll.dll. She prepares to jump into nuclear space, after which she makes a system call to create a file. In this case, it turns out that Kernel32 is just a wrapper for Ntdll. One of the reasons why this is done, Microsoft thus has the ability to change the functions of the native world, but not to touch the standard interfaces. Microsoft does not recommend calling native functions directly and does not document most of them. By the way, undocumented features can be found here.

The main advantage of native applications is that ntdll is loaded into the system much earlier than kernel32. This is logical, because kernel32 requires ntdll to work. As a result, applications that use native functions can start working much earlier.

Thus, Windows Native Applications are programs that can run at an early stage in booting Windows. They use ONLY functions from ntdll. An example of such an application: autochk which executes chkdisk utility to check the disk for errors before starting the main services. It is at this level that we want to see our Active Restore.

What do we need?

  • DDK (Driver Development Kit), now also known as WDK 7 (Windows Driver Kit).
  • Virtual machine (e.g. windows 7 x64)
  • Not necessarily, but header files can be downloaded here.

What is in the code?

Let's practice a little and for an example we will write a small application which:

  1. Displays a message on the screen.
  2. Allocates a little memory
  3. Waiting for keyboard input
  4. Frees busy memory

In native applications, the entry point is not the main or winmain, but the NtProcessStartup function, since we actually directly start the new process in the system.

Let's start by displaying the message on the screen. To do this, we have a native function NtDisplayString, which takes as an argument a pointer to an object of the UNICODE_STRING structure. RtlInitUnicodeString will help us initialize it. As a result, to display text on the screen, we can write such a small function:

// usage: WriteLn (L "Here is my text  n");
void WriteLn (LPWSTR Message)
{
    UNICODE_STRING string;
    RtlInitUnicodeString (& string, Message);
    NtDisplayString (& string);
}

Since only functions from ntdll are available to us, and there are simply no other libraries in memory yet, we will definitely have problems with how to allocate memory. The new operator does not exist yet (because it comes from a too high-level C ++ world), there is also no malloc function (it needs runtime C libraries). You can of course use only the stack. But if we need to dynamically allocate memory, we will have to do this on the heap (i.e. heap). Therefore, let's create a bunch for ourselves and we will take memory from it when we need it.

The function RtlCreateHeap is suitable for this task. Further, using RtlAllocateHeap and RtlFreeHeap, we will occupy and free memory when we need it.

PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;

// create heap in order to allocate memory later
memory = RtlCreateHeap (
  HEAP_GROWABLE,
  NULL
  1000
  0, null, null
);

// allocate buffer of size bufferSize
buffer = RtlAllocateHeap (
  memory
  HEAP_ZERO_MEMORY,
  bufferSize
);

// free buffer (actually not needed because we destroy heap in next step)
RtlFreeHeap (memory, 0, buffer);

RtlDestroyHeap (memory);

Let's move on to waiting for keyboard input.

// https://docs.microsoft.com/en-us/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_input_data
typedef struct _KEYBOARD_INPUT_DATA {
  USHORT UnitId;
  USHORT MakeCode;
  USHORT Flags;
  USHORT Reserved;
  ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, * PKEYBOARD_INPUT_DATA;

// ...

HANDLE hKeyBoard, hEvent;
UNICODE_STRING skull, keyboard;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK Iosb;
LARGE_INTEGER ByteOffset;
KEYBOARD_INPUT_DATA kbData;

// inialize variables
RtlInitUnicodeString (& keyboard, L "\ Device \ KeyboardClass0");
InitializeObjectAttributes (& ObjectAttributes, & keyboard, OBJ_CASE_INSENSITIVE, NULL, NULL);

// open keyboard device
NtCreateFile (& hKeyBoard,
SYNCHRONIZE | GENERIC_READ | FILE_READ_ATTRIBUTES,
& ObjectAttributes,
& Iosb,
NULL
FILE_ATTRIBUTE_NORMAL,
0
FILE_OPEN, FILE_DIRECTORY_FILE,
NULL, 0);

// create event to wait on
InitializeObjectAttributes (& ObjectAttributes, NULL, 0, NULL, NULL);
NtCreateEvent (& hEvent, EVENT_ALL_ACCESS, & ObjectAttributes, 1, 0);

while (TRUE)
{
NtReadFile (hKeyBoard, hEvent, NULL, NULL, & Iosb, & kbData, sizeof (KEYBOARD_INPUT_DATA), & ByteOffset, NULL);
NtWaitForSingleObject (hEvent, TRUE, NULL);

if (kbData.MakeCode == 0x01) // if ESC pressed
{
break;
}
}

All we need to do is use NtReadFile on an open device, and wait until the keyboard returns a click to us. In case the ESC key is pressed, we will continue to work. To open the device, we need to call the NtCreateFile function (you will need to open Device KeyboardClass0). We will also call NtCreateEvent to initialize the object to wait. We will independently declare a KEYBOARD_INPUT_DATA structure that represents keyboard data. This will facilitate our work.

The native application ends with a call to the NtTerminateProcess function, because we just kill our own process.

All code of our small application:

#include "ntifs.h" //  WinDDK  7600.16385.1  inc  ddk
#include "ntdef.h"

// ------------------------------------
// Following function definitions can be found in native development kit
// but I am too lazy to include `em so I declare it here
// ------------------------------------

NTSYSAPI
NTSTATUS
NTAPI
NtTerminateProcess (
  IN HANDLE ProcessHandle OPTIONAL,
  IN NTSTATUS ExitStatus
);

NTSYSAPI
NTSTATUS
NTAPI
NtDisplayString (
IN PUNICODE_STRING String
);

NTSTATUS
NtWaitForSingleObject (
  IN HANDLE Handle,
  IN BOOLEAN Alertable,
  IN PLARGE_INTEGER Timeout
);

NTSYSAPI
NTSTATUS
NTAPI
NtCreateEvent (
    OUT PHANDLE EventHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN EVENT_TYPE EventType,
    IN BOOLEAN InitialState
);



// https://docs.microsoft.com/en-us/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_input_data
typedef struct _KEYBOARD_INPUT_DATA {
  USHORT UnitId;
  USHORT MakeCode;
  USHORT Flags;
  USHORT Reserved;
  ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, * PKEYBOARD_INPUT_DATA;

// ------------------------------------------------ ----------
// Our code goes here
// ------------------------------------------------ ----------

// usage: WriteLn (L "Hello Native World!  n");
void WriteLn (LPWSTR Message)
{
    UNICODE_STRING string;
    RtlInitUnicodeString (& string, Message);
    NtDisplayString (& string);
}

void NtProcessStartup (void * StartupArgument)
{
// it is important to declare all variables at the beginning
HANDLE hKeyBoard, hEvent;
UNICODE_STRING skull, keyboard;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK Iosb;
LARGE_INTEGER ByteOffset;
KEYBOARD_INPUT_DATA kbData;

PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;

// use it if debugger connected to break
// DbgBreakPoint ();

WriteLn (L "Hello Native World!  N");

// inialize variables
RtlInitUnicodeString (& keyboard, L "\ Device \ KeyboardClass0");
InitializeObjectAttributes (& ObjectAttributes, & keyboard, OBJ_CASE_INSENSITIVE, NULL, NULL);

// open keyboard device
NtCreateFile (& hKeyBoard,
SYNCHRONIZE | GENERIC_READ | FILE_READ_ATTRIBUTES,
& ObjectAttributes,
& Iosb,
NULL
FILE_ATTRIBUTE_NORMAL,
0
FILE_OPEN, FILE_DIRECTORY_FILE,
NULL, 0);

// create event to wait on
InitializeObjectAttributes (& ObjectAttributes, NULL, 0, NULL, NULL);
NtCreateEvent (& hEvent, EVENT_ALL_ACCESS, & ObjectAttributes, 1, 0);

WriteLn (L "Keyboard ready  n");

// create heap in order to allocate memory later
memory = RtlCreateHeap (
HEAP_GROWABLE,
NULL
1000
0, NULL, NULL
);

WriteLn (L "Heap ready  n");

// allocate buffer of size bufferSize
buffer = RtlAllocateHeap (
memory
HEAP_ZERO_MEMORY,
bufferSize
);

WriteLn (L "Buffer allocated  n");

// free buffer (actually not needed because we destroy heap in next step)
RtlFreeHeap (memory, 0, buffer);

RtlDestroyHeap (memory);

WriteLn (L "Heap destroyed  n");

WriteLn (L "Press ESC to continue ...  n");

while (TRUE)
{
NtReadFile (hKeyBoard, hEvent, NULL, NULL, & Iosb, & kbData, sizeof (KEYBOARD_INPUT_DATA), & ByteOffset, NULL);
NtWaitForSingleObject (hEvent, TRUE, NULL);

if (kbData.MakeCode == 0x01) // if ESC pressed
{
break;
}
}

NtTerminateProcess (NtCurrentProcess (), 0);
}

PS: We can easily use the DbgBreakPoint () function in the code to stop in the debugger. True, you will need to connect WinDbg to the virtual machine for kernel debugging. Instructions on how to do this can be found here or just use VirtualKD.

Compilation and assembly

The easiest way to build a native application is to use DDK (Driver Development Kit). We need exactly the ancient seventh version, since later versions have a slightly different approach and work closely with Visual Studio. If we use DDK, then our project needs only Makefile and sources.

Makefile

! INCLUDE $ (NTMAKEENV)  makefile.def

sources:

TARGETNAME = MyNative
TARGETTYPE = PROGRAM
UMTYPE = nt
BUFFER_OVERFLOW_CHECKS = 0
MINWIN_SDK_LIB_PATH = $ (SDK_LIB_PATH)
SOURCES = source.c

INCLUDES = $ (DDK_INC_PATH); 
C:  WinDDK  7600.16385.1  ndk;

TARGETLIBS = $ (DDK_LIB_PATH)  ntdll.lib 
$ (DDK_LIB_PATH)  nt.lib

USE_NTDLL = 1

Your Makefile will be exactly the same, but let’s dwell on the sources in more detail. This file contains the sources of your program (.c files), build options and other parameters.

  • TARGETNAME – the name of the executable file, which should be the result.
  • TARGETTYPE – type of executable file, it can be a driver (.sys), then the field value should be DRIVER, if the library (.lib), then the value is LIBRARY. In our case, we need an executable file (.exe), so we set the value to PROGRAM.
  • UMTYPE – possible values ​​for this field: console for a console application, windows for operating in windowed mode. But we need to specify nt to get the native application.
  • BUFFER_OVERFLOW_CHECKS – checking the stack for buffer overflow, unfortunately not our case, turn it off.
  • MINWIN_SDK_LIB_PATH – this value refers to the variable SDK_LIB_PATH, do not worry that you have not declared such a system variable, the moment we run the checked build from DDK, this variable will be declared and will point to the necessary libraries.
  • SOURCES – a list of the sources of your program.
  • INCLUDES – header files that are needed for assembly. They usually indicate the path to the files that come with the DDK, but you can optionally specify any others.
  • TARGETLIBS – a list of libraries that need to be linked.
  • USE_NTDLL is a required field that must be set to position 1. For obvious reasons.
  • USER_C_FLAGS – any flags that you can use in preprocessor directives when preparing application code.

So to build, we need to run x86 (or x64) Checked Build, change the working directory to the project folder and execute the Build command. The result in the screenshot shows that we have gathered one executable file.

Build

This file cannot be run so simply, the system swears and sends us to think about its behavior with the following error:

Error

How to run a native application?

At the start of autochk, the startup sequence of programs is determined by the registry key value:

HKLM  System  CurrentControlSet  Control  Session Manager  BootExecute

The session manager executes the programs from this list one by one. The session manager itself looks for executable files in the system32 directory. The format of the registry key value is as follows:

autocheck autochk * MyNative

The value should be in hexadecimal format, and not in the usual ASCII, therefore the key presented above will have the format:

61.75.74.6f, 63.68.65.63.6b, 20.61.75.74.6f, 63.68.6b, 20.2a, 00.4d, 79.4e, 61.74, 69.76.65.00.00

To convert the name, you can use an online service, for example, this one.

It turns out that to run the native application, we need:

  1. Copy executable file to system32 folder
  2. Add a key to the registry
  3. Reboot machine

For convenience, here's a ready-made script for installing a native application:

install.bat

@echo off
copy MyNative.exe% systemroot%  system32 .
regedit / s add.reg
echo Native Example Installed
pause

add.reg

REGEDIT4

[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession Manager]
"BootExecute" = hex (7): 61.75.74.6f, 63.68.65.63.6b, 20.61.75.74.6f, 63.68.6b, 20.2a, 00.4d , 79.4e, 61.74.69, 79.66.65.00.00

After installation and rebooting, even before the user selection screen appears, we get the following picture:

result

Total

Using the example of such a small application, we were convinced that it is quite possible to run the application at the Windows Native level. Further, the guys from Innopolis University will continue to build a service that will initiate the process of interacting with the driver much earlier than in the previous version of our project. And with the advent of the win32 shell, it will be logical to transfer control to a full-fledged service that has already been developed (more on this here).

In the next article, we will touch upon another component of the Active Restore service, namely the UEFI driver. Subscribe to our blog not to miss the next post.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *