Windows Native Applications and the Acronis Active Restore service

Today, we continue our story about how we, along with the team from Innopolis University, are developing the Active Restore technology to allow users to start working on their machines as soon as possible after a crash. We will discuss native Windows applications, including the specifics of their creation and launch. Below is a bit about our project and a practical guide on how to write native applications.

Windows Native Applications and the Acronis Active Restore service

In previous posts, we have already talked about what Active Restoreis and how the students from Innopolis are developing it. Today, I want to focus on native applications, to the level at which we want to "embed" our active recovery service. If everything goes well, we will be able to: serviceStart the service much earlier

  • Connect to the cloud where the backup is stored much earlier
  • Understand much earlier what mode the system is in – normal boot or recovery
  • Recover many fewer files in advance
  • Allow the user to get to work even faster.
  • What exactly is a native application?

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

Pavel Yosifovich — Windows Kernel Programming (2019)

Windows Native Applications and the Acronis Active Restore service
The programmer uses the function

CreateFile , which is declared in the header file fileapi.h and implemented in Kernel32.dll. However, this function itself does not create a file; it merely checks the input arguments and calls the functionNtCreateFile (the prefix Nt indicates that the function is native). This function is declared in the header file winternl.h and implemented in ntdll.dll. It prepares for a jump into kernel space and then makes a system call to create the file. In this case, Kernel32 serves merely as a wrapper for Ntdll. One of the reasons this is done is that Microsoft can change the functions of the native world without affecting standard interfaces. Microsoft does not recommend directly calling native functions and does not document most of them. By the way, undocumented functions can be found . here.

The main advantage of native applications is that ntdll is loaded into the system significantly earlier than kernel32. This makes sense, as kernel32 requires ntdll to function. Consequently, applications that use native functions can start operating much earlier.

Thus, Windows Native Applications are programs capable of launching at an early stage of the Windows boot process. They use ONLY functions from ntdll. An example of such an application is: autochk which executes the chkdisk utility to check the disk for errors before the main services start. This is precisely the level at which we want to see our Active Restore.

What do we need?

  • DDK (Driver Development Kit), also known today as WDK 7 (Windows Driver Kit).
  • A virtual machine (for example, Windows 7 x64)
  • Not necessary, but header files that can be downloaded may be helpful. here

What about the code?

Let's practice a bit and write a small application that:

  1. Displays a message on the screen
  2. Allocates some memory
  3. Waits for input from the keyboard
  4. Frees the allocated memory

In native applications, the entry point is not main or winmain, but the function NtProcessStartup, as we are effectively launching new processes directly in the system.

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

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

Since we only have access to functions from ntdll and no other libraries are yet in memory, we will inevitably face problems with how to allocate memory. The new operator does not exist yet (as it belongs to the too high-level world of C++), nor is there a malloc function (which requires C runtime libraries). We can only use the stack. However, if we need to dynamically allocate memory, we will have to do it in the heap. So let's create a heap for ourselves and allocate memory from it when we need it.

For this task, we can use the function RtlCreateHeap. Next, using RtlAllocateHeap and RtlFreeHeap, we will allocate and free memory when needed.

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 input from the keyboard.

// 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"DeviceKeyboardClass0");
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 the opened device and wait for a keypress from the keyboard. In case the ESC key is pressed, we will continue operation. To open the device, we need to call the NtCreateFile function (we'll need to open DeviceKeyboardClass0). We will also call NtCreateEvent, to initialize the waiting object. We will independently declare the KEYBOARD_INPUT_DATA structure, which represents keyboard data. This will make our work easier.

The native application's work ends with the call to the NtTerminateProcess, because we are simply terminating our own process.

The entire code of our small application:

#include "ntifs.h" // WinDDK7600.16385.1incddk
#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"DeviceKeyboardClass0");
	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 readyn");
	
	// create heap in order to allocate memory later
	memory = RtlCreateHeap(
	  HEAP_GROWABLE, 
	  NULL, 
	  1000, 
	  0, NULL, NULL
	);
	
	WriteLn(L"Heap readyn");

	// allocate buffer of size bufferSize
	buffer = RtlAllocateHeap(
	  memory, 
	  HEAP_ZERO_MEMORY, 
	  bufferSize
	);
	
	WriteLn(L"Buffer allocatedn");

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

	RtlDestroyHeap(memory);
	
	WriteLn(L"Heap destroyedn");
	
	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. However, we will need to connect WinDbg to the virtual machine for kernel debugging. Instructions on how to do this can be found here or simply use VirtualKD.

Compilation and assembly

The easiest way to build a native application is to use DDK Driver Development Kit). We specifically need the old seventh version, as later versions have a slightly different approach and are closely integrated with Visual Studio. If you use the DDK, then our project only needs a 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:WinDDK7600.16385.1ndk;

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

USE_NTDLL			= 1

Your Makefile will be exactly the same; let’s stop a little longer on sources. This file specifies the source code of your program (the .c files), build options, and other parameters.

  • TARGETNAME is the name of the executable file that should be generated.
  • TARGETTYPE is the type of executable file; this could be a driver (.sys), in which case the value should be DRIVER, or a library (.lib), then the value 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 console applications, windows for windowed applications. However, we need to specify nt to obtain a native application.
  • BUFFER_OVERFLOW_CHECKS – checking the stack for buffer overflow, unfortunately not our case, we will disable it.
  • MINWIN_SDK_LIB_PATH – this value refers to the SDK_LIB_PATH variable, there's no need to worry if you haven't declared such a system variable; when we run a checked build from the DDK, this variable will be declared and will refer to the necessary libraries.
  • SOURCES – the list of source files for your program.
  • INCLUDES – header files required for the build. Here, you usually specify the path to the files included with the DDK, but you can additionally specify any others.
  • TARGETLIBS – the list of libraries that need to be linked.
  • USE_NTDLL – a mandatory field that must be set to 1. For quite obvious reasons.
  • USER_C_FLAGS – any flags you can use in preprocessor directives when preparing the 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 built one executable file.

Windows Native Applications and the Acronis Active Restore service

This file cannot be easily run; the system complains and sends us to ponder its behavior with the following error:

Windows Native Applications and the Acronis Active Restore service

How to run a native application?

At startup, the program launch sequence is determined by the value of the registry key:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

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

autocheck autochk *MyNative

The value must be in hexadecimal format, not 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.

Windows Native Applications and the Acronis Active Restore service
So, to run the native application, we need to:

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

For convenience, here's a ready-made script for installing the 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_MACHINE\SYSTEM\CurrentControlSet\Control\Session 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,76,65,00,00

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

Windows Native Applications and the Acronis Active Restore service

Summary

Using this small application as an example, we have confirmed that it is indeed possible to launch an application at the Windows Native level. Moving forward, we will continue to collaborate with the team from Innopolis University to build a service that will initiate interaction with the driver much earlier than in the previous version of our project. With the introduction of the win32 shell, it will logically transfer control to a fully developed service that has already been created (more on this later). here).

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

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster