Windows Native Applications і сэрвіс Acronis Active Restore

Сёння працягваем аповяд аб тым, як мы разам з рабятамі з Універсітэта Іннаполіс распрацоўваем тэхналогію Active Restore, каб дазволіць карыстачу як мага раней пачаць працу на сваёй машыне пасля збою. Гаворка пойдзе аб натыўных прыкладаннях Windows, у тым ліку асаблівасці іх стварэння і запуску. Пад катам – крыху пра наш праект, а таксама практычнае кіраўніцтва як пісаць натыўныя прыкладанні.

Windows Native Applications і сэрвіс Acronis Active Restore

У мінулых пастах мы ўжо расказвалі пра тое, што такое. Active Restore, і як студэнты з Інаполіса распрацоўваюць сэрвіс. Сёння я хачу спыніцца на натыўных дадатках, да ўзроўню якіх мы хочам "закапаць" наш сэрвіс актыўнага аднаўлення. Калі ўсё атрымаецца, то мы зможам:

  • Нашмат раней запусціць сам сэрвіс
  • Нашмат раней звязацца з воблакам, у якім ляжыць бэкап
  • Нашмат раней зразумець, у якім рэжыме знаходзіцца сістэма - нармальнай загрузкі або аднаўлення
  • Нашмат менш файлаў аднаўляць загадзя
  • Дазволіць карыстачу прыступіць да працы яшчэ хутчэй.

Што наогул такое натыўнае дадатак?

Каб адказаць на гэтае пытанне, давайце зірнем на паслядоўнасць выклікаў, якія здзяйсняе сістэма, напрыклад, калі праграміст у сваім дадатку спрабуе стварыць файл.

Windows Native Applications і сэрвіс Acronis Active Restore
Pavel Yosifovich - Windows Kernel Programming (2019)

Праграміст выкарыстоўвае функцыю Стварыць файл, якая абвешчаная ў загалоўкавых файлаў fileapi.h і рэалізаваная ў Kernel32.dll. Аднак сама гэтая функцыя не займаецца стварэннем файла, яна толькі правярае аргументы на ўваходзе і выклікае функцыю. NtCreateFile (Прыстаўка Nt як раз сведчыць аб тым, што функцыя натыўная). Дадзеная функцыя абвешчаная ў загалоўкавых файлаў winternl.h і рэалізаваная ў ntdll.dll. Яна вырабляе падрыхтоўку да скачка ў ядзерную прастору, пасля чаго здзяйсняе сістэмны выклік для стварэння файла. У дадзеным выпадку атрымліваецца, што Kernel32 - усяго толькі абгортка для Ntdll. Адна з прычын для чаго гэта зроблена, Microsoft такім чынам мае магчымасць змяняць функцыі натыўнага свету, але пры гэтым не чапаць стандартныя інтэрфейсы. Microsoft не рэкамендуе напрамую выклікаць натыўныя функцыі і не дакументуе большую частку з іх. Дарэчы, недакументаваныя функцыі можна знайсці тут.

Асноўная перавага натыўных прыкладанняў заключаецца ў тым, што ntdll загружаецца ў сістэму значна раней за kernel32. Гэта лагічна, бо kernel32 патрабуе наяўнасці ntdll для працы. Як следства, прыкладанні, якія выкарыстоўваюць натыўныя функцыі, могуць пачаць працу значна раней.

Такім чынам, Windows Native Applications - гэта праграмы, здольныя запускацца на раннім этапе загрузкі Windows. Яны выкарыстоўваюць ТОЛЬКІ функцыі з ntdll. Прыклад такога прыкладання: autochk які выконвае chkdisk utility для праверкі дыска на памылкі яшчэ да запуску асноўных сервісаў. Менавіта на такім узроўні мы і жадаем бачыць наш Active Restore.

Што нам спатрэбіцца?

  • ДДК (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, і іншых бібліятэк у памяці проста яшчэ няма, у нас абавязкова ўзнікнуць праблемы з тым як алакаваць памяць. Аператара new яшчэ не існуе (таму што ён родам з занадта высокаўзроўневага свету C++), таксама няма функцыі malloc (для яе патрэбны бібліятэкі runtime C). Можна вядома карыстацца толькі стэкам. Але калі нам трэба дынамічна алакаваць памяць, рабіць гэта давядзецца ў кучы (г.зн. heap). Таму давайце створым для сябе кучу і будзем браць з яе памяць калі нам запатрабуецца.

Для гэтай задачы падыдзе функцыя 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.

Кампіляцыя і зборка

Самы просты спосаб сабраць натыўнае дадатак - гэта выкарыстоўваць ДДК (Driver Development Kit). Нам патрэбна менавіта старажытная сёмая версія, бо пазнейшыя версіі маюць некалькі іншы падыход і цесна працуюць з Visual Studio. Калі ж выкарыстоўваць DDK, то нашаму праекту патрэбны ўсяго толькі Makefile і sources.

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 будзе сапраўды такім жа, на sources жа давайце спынімся крыху падрабязней. У дадзеным файле паказваюцца зыходнікі вашай праграмы (файлы .c), опцыі зборкі і іншыя параметры.

  • TARGETNAME - імя выкананага файла, які павінен атрымацца ў выніку.
  • TARGETTYPE - тып выкананага файла, гэта можа быць драйвер (.sys), тады значэнне поля павінна быць DRIVER, калі бібліятэка (.lib), то значэнне LIBRARY. У нашым выпадку патрэбен выкананы файл (.exe), таму мы ўсталёўваны значэнне PROGRAM.
  • UMTYPE - магчымыя значэнні гэтага поля: console для кансольнага прыкладання, windows для працы ў аконным рэжыме. Але нам неабходна пазначыць nt, каб атрымаць натыўны дадатак.
  • BUFFER_OVERFLOW_CHECKS - праверка стэка на перапаўненне буфера, нажаль не наш выпадак, выключаны.
  • MINWIN_SDK_LIB_PATH – дадзенае значэнне спасылаецца на зменную SDK_LIB_PATH, не варта перажываць што ў вас не абвешчана падобная сістэмная пераменная, у момант калі мы запусцім checked build з DDK, дадзеная пераменная будзе абвешчана і будзе паказваць на неабходныя бібліятэкі.
  • SOURCES - спіс зыходнікаў вашай праграмы.
  • INCLUDES - загалоўкавыя файлы, якія неабходныя для зборкі. Тут звычайна паказваюць шлях да файлаў, якія ідуць у камплекце з DDK, але вы можаце пазначыць дадаткова любыя іншыя.
  • TARGETLIBS – спіс бібліятэк, якія неабходна лінкаваць.
  • USE_NTDLL - абавязковае поле, якое неабходна ўсталяваць у становішча 1. Па цалкам відавочным прычынах.
  • USER_C_FLAGS - любыя сцягі, якія вы зможаце выкарыстоўваць у прэпрацэсарных дырэктывах пры падрыхтоўцы кода прыкладання.

Такім чынам для зборкі нам неабходна запусціць x86 (ці x64) Checked Build, змяніць працоўны каталог на тэчку з праектам і выканаць каманду Build. Вынік на скрыншоце паказвае што ў нас сабраўся адзін выкананы файл.

Windows Native Applications і сэрвіс Acronis Active Restore

Дадзены файл не атрымаецца так проста запусціць, сістэма лаецца і адпраўляе нас думаць аб сваіх паводзінах з наступнай памылкай:

Windows Native Applications і сэрвіс 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 Applications і сэрвіс 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

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

Пасля ўсталёўкі і перазагрузкі яшчэ да з'яўлення экрана выбару карыстачоў мы атрымаем наступную карціну:

Windows Native Applications і сэрвіс Acronis Active Restore

Вынік

На прыкладзе вось такога маленькага прыкладання мы пераканаліся, што запусціць прыкладанне на ўзроўні Windows Native суцэль магчыма. Далей мы з хлопцамі з Універсітэта Іннаполіс працягнем будаваць сэрвіс, які будзе ініцыяваць працэс узаемадзеяння з драйверам нашмат раней, чым у папярэдняй версіі нашага праекта. А са з'яўленнем абалонкі win32 лагічна будзе перадаць кіраванне паўнавартаснаму сэрвісу, які ўжо быў распрацаваны (пра гэта падрабязней тут).

У чарговым артыкуле мы кранем яшчэ аднаго кампанента сэрвісу Active Restore, а менавіта UEFI драйвера. Падпісвайцеся на наш блог, каб не прапусціць наступны пост.

Крыніца: habr.com

Дадаць каментар