Windows Native приложения и услуга Acronis Active Restore

Днес продължаваме историята за това как ние, заедно с момчетата от Innopolis University, разработваме технологията Active Restore, за да позволим на потребителя да започне работа на своята машина възможно най-скоро след повреда. Ще говорим за собствените приложения на Windows, включително характеристиките на тяхното създаване и стартиране. По-долу е малко за нашия проект, както и практическо ръководство за това как да пишете собствени приложения.

Windows Native приложения и услуга Acronis Active Restore

В предишни публикации вече говорихме какво представлява Активно възстановяване, и как се развиват учениците от Innopolis услуга. Днес искам да се съсредоточа върху собствените приложения, до нивото на което искаме да „погребем“ нашата активна услуга за възстановяване. Ако всичко работи, тогава ще можем:

  • Стартирайте самата услуга много по-рано
  • Свържете се с облака, където се намира резервното копие, много по-рано
  • Много по-рано, за да разберете в какъв режим е системата - нормално зареждане или възстановяване
  • Много по-малко файлове за предварително възстановяване
  • Позволете на потребителя да започне още по-бързо.

Какво все пак е естествено приложение?

За да отговорим на този въпрос, нека разгледаме последователността от извиквания, които системата прави, например, ако програмист в своето приложение се опита да създаде файл.

Windows Native приложения и услуга Acronis Active Restore
Павел Йосифович - Програмиране на ядрото на Windows (2019)

Програмистът използва функцията Създаване на файл, който е деклариран в заглавния файл fileapi.h и имплементиран в Kernel32.dll. Самата тази функция обаче не създава файла, тя само проверява входните аргументи и извиква функцията NtCreateFile (префиксът Nt просто показва, че функцията е естествена). Тази функция е декларирана в заглавния файл winternl.h и е внедрена в ntdll.dll. Той се подготвя да скочи в ядреното пространство, след което прави системно извикване за създаване на файл. В този случай се оказва, че Kernel32 е просто обвивка за Ntdll. Една от причините, поради които беше направено това, е, че по този начин Microsoft има способността да променя функциите на родния свят, но не и да докосва стандартните интерфейси. Microsoft не препоръчва директно извикване на естествени функции и не документира повечето от тях. Между другото, могат да бъдат намерени недокументирани функции тук.

Основното предимство на родните приложения е, че ntdll се зарежда в системата много по-рано от kernel32. Това е логично, защото kernel32 изисква ntdll, за да работи. В резултат на това приложенията, които използват естествени функции, могат да започнат да работят много по-рано.

По този начин собствените приложения на Windows са програми, които могат да стартират рано при зареждане на Windows. Те използват САМО функции от ntdll. Пример за такова приложение: autochk който изпълнява помощна програма chkdisk за проверка на диска за грешки преди стартиране на основните услуги. Това е точно нивото, което искаме да бъде нашето активно възстановяване.

От какво се нуждаем?

  • DDK (Driver Development Kit), сега известен още като WDK 7 (Windows Driver Kit).
  • Виртуална машина (например Windows 7 x64)
  • Не е необходимо, но заглавните файлове, които могат да бъдат изтеглени, могат да помогнат тук

Какво има в кода?

Нека се упражняваме малко и например да напишем малко приложение, което:

  1. Показва съобщение на екрана
  2. Разпределя малко памет
  3. Изчаква въвеждане от клавиатурата
  4. Освобождава използваната памет

В собствените приложения входната точка не е main или winmain, а функцията NtProcessStartup, тъй като ние всъщност директно стартираме нови процеси в системата.

Нека започнем с показване на съобщение на екрана. За това имаме естествена функция NtDisplayString, който приема като аргумент указател към структурен обект UNICODE_STRING. RtlInitUnicodeString ще ни помогне да го инициализираме. В резултат на това, за да покажем текст на екрана, можем да напишем тази малка функция:

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

Тъй като за нас са достъпни само функции от ntdll и просто все още няма други библиотеки в паметта, определено ще имаме проблеми с това как да разпределим паметта. Новият оператор все още не съществува (тъй като идва от света на твърде високо ниво на C++) и няма функция malloc (тя изисква C библиотеки по време на изпълнение). Разбира се, можете да използвате само стек. Но ако трябва да разпределим динамично памет, ще трябва да го направим в купчината (т.е. купчината). Така че нека създадем купчина за себе си и да вземем памет от нея, когато имаме нужда от нея.

Функцията е подходяща за тази задача RtlCreateHeap. След това, използвайки RtlAllocateHeap и RtlFreeHeap, ние ще заемаме и освобождаваме памет, когато имаме нужда от нея.

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);

Нека да преминем към изчакване на въвеждане от клавиатурата.

// 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;
	}
}

Всичко, от което се нуждаем, е да използваме NtReadFile на отворено устройство и изчакайте, докато клавиатурата ни върне всяко натискане. Ако се натисне клавишът ESC, ще продължим да работим. За да отворим устройството, ще трябва да извикаме функцията NtCreateFile (ще трябва да отворим DeviceKeyboardClass0). Ние също ще се обадим NtCreateEventза инициализиране на чакащия обект. Ние сами ще декларираме структурата KEYBOARD_INPUT_DATA, която представлява данните от клавиатурата. Това ще улесни работата ни.

Родното приложение завършва с извикване на функция NtTerminateProcessзащото ние просто убиваме собствения си процес.

Целият код за нашето малко приложение:

#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: Можем лесно да използваме функцията DbgBreakPoint() в нашия код, за да го спрем в дебъгера. Вярно е, че ще трябва да свържете WinDbg към виртуална машина за отстраняване на грешки в ядрото. Можете да намерите инструкции как да направите това тук или просто използвайте VirtualKD.

Компилация и асемблиране

Най-лесният начин да създадете собствено приложение е да използвате DDK (Комплект за разработка на драйвери). Имаме нужда от древната седма версия, тъй като по-късните версии имат малко по-различен подход и работят в тясно сътрудничество с Visual Studio. Ако използваме DDK, тогава нашият проект се нуждае само от Makefile и източници.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

източници:

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

Вашият Makefile ще бъде абсолютно същият, но нека разгледаме източниците малко по-подробно. Този файл определя изходния код на вашата програма (.c файлове), опциите за компилация и други параметри.

  • TARGETNAME – името на изпълнимия файл, който трябва да бъде произведен в крайна сметка.
  • TARGETTYPE – тип изпълним файл, може да бъде драйвер (.sys), тогава стойността на полето трябва да е DRIVER, ако е библиотека (.lib), тогава стойността е LIBRARY. В нашия случай се нуждаем от изпълним файл (.exe), така че задаваме стойност на PROGRAM.
  • UMTYPE – възможни стойности за това поле: конзола за конзолно приложение, прозорци за работа в прозоречен режим. Но трябва да посочим nt, за да получим собствено приложение.
  • BUFFER_OVERFLOW_CHECKS – проверка на стека за препълване на буфера, за съжаление не е нашият случай, ние го изключваме.
  • MINWIN_SDK_LIB_PATH – тази стойност се отнася за променливата SDK_LIB_PATH, не се притеснявайте, че нямате декларирана такава системна променлива, когато стартираме проверена компилация от DDK, тази променлива ще бъде декларирана и ще сочи към необходимите библиотеки.
  • ИЗТОЧНИЦИ – списък с източници за вашата програма.
  • ВКЛЮЧВА – заглавни файлове, които са необходими за сглобяване. Тук те обикновено показват пътя до файловете, които идват с DDK, но можете допълнително да посочите други.
  • TARGETLIBS – списък с библиотеки, които трябва да бъдат свързани.
  • USE_NTDLL е задължително поле, което трябва да бъде зададено на 1 по очевидни причини.
  • USER_C_FLAGS – всички флагове, които можете да използвате в директивите на препроцесора, когато подготвяте кода на приложението.

Така че, за да изградим, трябва да стартираме x86 (или x64) Checked Build, да променим работната директория на папката на проекта и да изпълним командата Build. Резултатът в екранната снимка показва, че имаме един изпълним файл.

Windows Native приложения и услуга Acronis Active Restore

Този файл не може да се стартира толкова лесно, системата ругае и ни кара да мислим за поведението му със следната грешка:

Windows Native приложения и услуга Acronis Active Restore

Как да стартирате собствено приложение?

Когато autochk стартира, стартовата последователност на програмите се определя от стойността на ключа на системния регистър:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Мениджърът на сесии изпълнява програми от този списък една по една. Мениджърът на сесии търси самите изпълними файлове в директорията system32. Форматът на стойността на ключа на системния регистър е както следва:

autocheck autochk *MyNative

Стойността трябва да е в шестнадесетичен формат, а не в обичайния ASCII, така че показаният по-горе ключ ще бъде във формат:

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

За да конвертирате заглавието, можете да използвате онлайн услуга, напр. това.

Windows Native приложения и услуга Acronis Active Restore
Оказва се, че за да стартираме собствено приложение, имаме нужда от:

  1. Копирайте изпълнимия файл в папката system32
  2. Добавете ключ към системния регистър
  3. Рестартирайте машината

За удобство, ето готов скрипт за инсталиране на собствено приложение:

install.bat

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

доп.рег

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

След инсталиране и рестартиране, дори преди да се появи екранът за избор на потребител, ще получим следната картина:

Windows Native приложения и услуга Acronis Active Restore

Общо

Използвайки примера на такова малко приложение, бяхме убедени, че е напълно възможно да стартирате приложението на ниво Windows Native. След това момчетата от университета Innopolis и аз ще продължим да изграждаме услуга, която ще инициира процеса на взаимодействие с водача много по-рано, отколкото в предишната версия на нашия проект. И с появата на обвивката win32 би било логично контролът да се прехвърли на пълноценна услуга, която вече е разработена (повече за това тук).

В следващата статия ще разгледаме още един компонент на услугата Active Restore, а именно UEFI драйвера. Абонирайте се за нашия блог, за да не пропуснете следващата публикация.

Източник: www.habr.com

Добавяне на нов коментар