Windows Native Applications and Acronis Active Restore Service

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 his 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.

Windows Native Applications and Acronis Active Restore Service

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

  • Start the service much earlier
  • Much earlier to contact the cloud in which the backup is located
  • Much earlier to understand what mode the system is in - normal boot or recovery
  • Much fewer files to restore in advance
  • Let the user get to work even faster.

What is a native app anyway?

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.

Windows Native Applications and Acronis Active Restore Service
Pavel Yosifovich - Windows Kernel Programming (2019)

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 only checks the input arguments and calls the function NtCreateFile (The prefix Nt just indicates that the function is native). This function is declared in the winternl.h header file and implemented in ntdll.dll. It prepares to jump into nuclear space and then makes a system call to create the file. In this case, it turns out that Kernel32 is just a wrapper for Ntdll. One of the reasons why this is done is that Microsoft thus has the ability to change the functions of the native world, but at the same time not touch the standard interfaces. Microsoft does not recommend calling native functions directly 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 much earlier than kernel32. This is logical, because kernel32 requires ntdll to work. As a result, apps that use native features can get up and running much sooner.

Thus, Windows Native Applications are programs that can be launched at an early stage of Windows boot. They ONLY use functions from ntdll. An example of such an application: autochk who performs chkdisk utility to check the disk for errors even 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 (for example, Windows 7 x64)
  • Not required, but downloadable header files can help. here

What's in the code?

Let's practice a little and, for example, write a small application that:

  1. Displays a message on the screen
  2. Allocate some memory
  3. Waiting for keyboard input
  4. Frees up occupied memory

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

Let's start by displaying a message on the screen. For 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 this small function:

//usage: WriteLn(L"Here is my textn");
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 doesn't exist yet (because it comes from the too-high-level C++ world), nor does the 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 it on the heap (i.e. heap). Therefore, let's create a heap for ourselves and take memory from it when we need it.

For this task, the function RtlCreateHeap. Next, using RtlAllocateHeap and RtlFreeHeap, we will borrow 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"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 is to use NtReadFile on an open device, and wait until the keyboard returns us some kind of press. If the ESC key is pressed, we will continue our work. To open the device, we need to call the NtCreateFile function (we will need to open DeviceKeyboardClass0). We will also call NtCreateEventto initialize the object to wait. We will declare the KEYBOARD_INPUT_DATA structure ourselves, which represents the keyboard data. This will make our job easier.

The work of the native application is completed by calling the function NtTerminateProcessbecause we are just killing 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. True, you will need to connect WinDbg to a virtual machine for kernel debugging. Instructions on how to do this can be found here or just use VirtualKD.

Compiling and building

The easiest way to build a native app is to use DDK (Driver Development Kit). We need the ancient version XNUMX, because later versions have a slightly different approach and work closely with Visual Studio. If we 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 take a closer look at sources. This file specifies your program sources (.c files), build options, and other options.

  • TARGETNAME is the name of the executable file that should be the final result.
  • TARGETTYPE is the type of the executable file, it can be a driver (.sys), then the field value must be DRIVER, if it is a 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 working in windowed mode. But we need to specify nt to get the native app.
  • 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 SDK_LIB_PATH variable, do not worry that you do not have such a system variable declared, at the moment when we run checked build from the DDK, this variable will be declared and will point to the necessary libraries.
  • SOURCES - a list of sources for your program.
  • INCLUDES - header files that are needed for the build. This usually indicates the path to the files that come with the DDK, but you can specify any others in addition.
  • TARGETLIBS is a list of libraries to be linked.
  • USE_NTDLL is a required field that needs to 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 run the Build command. The result in the screenshot shows that we have one executable file.

Windows Native Applications and Acronis Active Restore Service

This file cannot be launched so easily, the system swears and sends us to think about our behavior with the following error:

Windows Native Applications and Acronis Active Restore Service

How to run native app?

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

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

The session manager executes programs from this list in turn. The session manager looks for the executable files themselves 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 in the usual ASCII, so the key above will be in 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 title, you can use an online service, for example, this.

Windows Native Applications and Acronis Active Restore Service
It turns out that in order to run a native application, we need:

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

For convenience, here is 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,76,65,00,00

After installation and reboot, before the user selection screen appears, we will get the following picture:

Windows Native Applications and Acronis Active Restore Service

Сonclusion

On the example of such a small application, we made sure that it is quite possible to run an application at the Windows Native level. Next, the guys from Innopolis University and I will continue to build a service that will initiate the process of interaction 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 on another component of the Active Restore service, namely the UEFI driver. Subscribe to our blog so you don't miss the next post.

Source: habr.com

Add a comment