Windows Native Applications a služba Acronis Active Restore

Dnes pokračujeme v příběhu o tom, jak společně s kluky z Innopolis University vyvíjíme technologii Active Restore, která umožní uživateli začít pracovat na svém stroji co nejdříve po selhání. Budeme mluvit o nativních aplikacích pro Windows, včetně funkcí jejich tvorby a spouštění. Pod sestřihem je něco málo o našem projektu a také praktický návod, jak psát nativní aplikace.

Windows Native Applications a služba Acronis Active Restore

V předchozích příspěvcích jsme již mluvili o tom, co to je Aktivní obnovenía jak se vyvíjejí studenti z Innopolis služba. Dnes se chci zaměřit na nativní aplikace, do jejichž úrovně chceme „pohřbít“ naši službu aktivní obnovy. Pokud vše klapne, pak budeme schopni:

  • Spusťte samotnou službu mnohem dříve
  • Kontaktujte cloud, kde se záloha nachází, mnohem dříve
  • Mnohem dříve, abyste pochopili, v jakém režimu je systém - normální spuštění nebo obnovení
  • Mnohem méně souborů k obnovení předem
  • Umožněte uživateli začít ještě rychleji.

Co je vlastně nativní aplikace?

Abychom na tuto otázku odpověděli, podívejme se na posloupnost volání, která systém provede, například pokud se programátor ve své aplikaci pokusí vytvořit soubor.

Windows Native Applications a služba Acronis Active Restore
Pavel Yosifovich - Windows Kernel Programming (2019)

Programátor používá funkci CreateFile, který je deklarován v záhlaví souboru fileapi.h a implementován v Kernel32.dll. Tato funkce však sama soubor nevytváří, pouze kontroluje vstupní argumenty a volá funkci NtCreateFile (předpona Nt pouze označuje, že funkce je nativní). Tato funkce je deklarována v záhlaví souboru winternl.h a implementována v ntdll.dll. Připraví se na skok do jaderného prostoru, načež provede systémové volání k vytvoření souboru. V tomto případě se ukazuje, že Kernel32 je jen obal pro Ntdll. Jedním z důvodů, proč se tak stalo, je, že Microsoft tak má možnost měnit funkce přirozeného světa, ale nedotýkat se standardních rozhraní. Microsoft nedoporučuje volat přímo nativní funkce a většinu z nich nedokumentuje. Mimochodem, lze najít nezdokumentované funkce zde.

Hlavní výhodou nativních aplikací je, že ntdll se do systému načte mnohem dříve než kernel32. To je logické, protože kernel32 ke svému fungování vyžaduje ntdll. Díky tomu mohou aplikace využívající nativní funkce začít fungovat mnohem dříve.

Nativní aplikace Windows jsou tedy programy, které lze spustit brzy při spouštění systému Windows. Používají POUZE funkce z ntdll. Příklad takové aplikace: autochk kdo vystupuje nástroj chkdisk pro kontrolu chyb na disku před spuštěním hlavních služeb. To je přesně úroveň, kterou chceme, aby naše Active Restore byla.

Co potřebujeme?

  • DDK (Driver Development Kit), nyní také známý jako WDK 7 (Windows Driver Kit).
  • Virtuální počítač (například Windows 7 x64)
  • Není to nutné, ale mohou pomoci soubory záhlaví, které lze stáhnout zde

Co je v kódu?

Pojďme si trochu zacvičit a třeba napsat malou aplikaci, která:

  1. Zobrazí zprávu na obrazovce
  2. Přiděluje nějakou paměť
  3. Čeká na vstup z klávesnice
  4. Uvolňuje použitou paměť

V nativních aplikacích není vstupním bodem main nebo winmain, ale funkce NtProcessStartup, protože vlastně přímo spouštíme nové procesy v systému.

Začněme zobrazením zprávy na obrazovce. K tomu máme nativní funkci NtDisplayString, který bere jako argument ukazatel na objekt struktury UNICODE_STRING. RtlInitUnicodeString nám pomůže jej inicializovat. Výsledkem je, že pro zobrazení textu na obrazovce můžeme napsat tuto malou funkci:

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

Vzhledem k tomu, že jsou nám dostupné pouze funkce z ntdll a další knihovny v paměti zatím prostě nejsou, budeme mít určitě problémy s alokací paměti. Nový operátor ještě neexistuje (protože pochází z příliš vysokého světa C++) a neexistuje žádná funkce malloc (vyžaduje runtime C knihovny). Samozřejmě můžete použít pouze zásobník. Pokud ale potřebujeme dynamicky alokovat paměť, budeme to muset udělat na haldě (tedy haldě). Vytvořme si tedy hromadu pro sebe a vezměme si z ní paměť, kdykoli ji budeme potřebovat.

Funkce je pro tento úkol vhodná RtlCreateHeap. Dále pomocí RtlAllocateHeap a RtlFreeHeap zabereme a uvolníme paměť, když ji budeme potřebovat.

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

Přejděme k čekání na vstup z klávesnice.

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

Vše, co potřebujeme, je použít NtReadFile na otevřeném zařízení a počkejte, až nám klávesnice vrátí jakékoli stisknutí. Pokud stisknete klávesu ESC, pokračujeme v práci. Pro otevření zařízení budeme muset zavolat funkci NtCreateFile (budeme muset otevřít DeviceKeyboardClass0). Také zavoláme NtCreateEventk inicializaci objektu čekání. Sami si deklarujeme strukturu KEYBOARD_INPUT_DATA, která reprezentuje data klávesnice. To nám usnadní práci.

Nativní aplikace končí voláním funkce NtTerminateProcessprotože prostě zabíjíme svůj vlastní proces.

Celý kód pro naši malou aplikaci:

#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: K zastavení v ladicím programu můžeme snadno použít funkci DbgBreakPoint() v našem kódu. Je pravda, že pro ladění jádra budete muset připojit WinDbg k virtuálnímu počítači. Návod jak na to najdete zde nebo jen použít VirtualKD.

Kompilace a montáž

Nejjednodušší způsob, jak vytvořit nativní aplikaci, je použít DDK (Sada pro vývoj ovladačů). Potřebujeme starou sedmou verzi, protože pozdější verze mají trochu jiný přístup a úzce spolupracují se sadou Visual Studio. Pokud použijeme DDK, pak náš projekt potřebuje pouze Makefile a zdroje.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

Zdroje:

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

Váš Makefile bude úplně stejný, ale podívejme se na zdroje trochu podrobněji. Tento soubor určuje zdroje vašeho programu (soubory .c), možnosti sestavení a další parametry.

  • TARGETNAME – název spustitelného souboru, který by měl být nakonec vytvořen.
  • TARGETTYPE – typ spustitelného souboru, může to být ovladač (.sys), pak hodnota pole by měla být DRIVER, pokud knihovna (.lib), pak hodnota je LIBRARY. V našem případě potřebujeme spustitelný soubor (.exe), proto nastavíme hodnotu PROGRAM.
  • UMTYPE – možné hodnoty pro toto pole: konzola pro konzolovou aplikaci, okna pro práci v režimu okna. Ale musíme zadat nt, abychom získali nativní aplikaci.
  • BUFFER_OVERFLOW_CHECKS – kontrola zásobníku na přetečení bufferu, bohužel ne náš případ, vypneme.
  • MINWIN_SDK_LIB_PATH – tato hodnota odkazuje na proměnnou SDK_LIB_PATH, nebojte se, že takovou systémovou proměnnou deklarovanou nemáte, když spustíme kontrolované sestavení z DDK, tato proměnná bude deklarována a bude ukazovat na potřebné knihovny.
  • SOURCES – seznam zdrojů pro váš program.
  • OBSAHUJE – hlavičkové soubory, které jsou nutné pro sestavení. Zde obvykle označují cestu k souborům dodaným s DDK, ale můžete dodatečně zadat jakékoli další.
  • TARGETLIBS – seznam knihoven, které je třeba propojit.
  • USE_NTDLL je povinné pole, které musí být ze zřejmých důvodů nastaveno na 1.
  • USER_C_FLAGS – všechny příznaky, které můžete použít v direktivách preprocesoru při přípravě kódu aplikace.

Abychom tedy mohli sestavit, musíme spustit x86 (nebo x64) Checked Build, změnit pracovní adresář na složku projektu a spustit příkaz Build. Výsledek na snímku obrazovky ukazuje, že máme jeden spustitelný soubor.

Windows Native Applications a služba Acronis Active Restore

Tento soubor nelze tak snadno spustit, systém nadává a posílá nás k zamyšlení nad jeho chováním s následující chybou:

Windows Native Applications a služba Acronis Active Restore

Jak spustit nativní aplikaci?

Když se spustí autochk, spouštěcí sekvence programů je určena hodnotou klíče registru:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Správce relací spouští programy z tohoto seznamu jeden po druhém. Správce relace hledá samotné spustitelné soubory v adresáři system32. Formát hodnoty klíče registru je následující:

autocheck autochk *MyNative

Hodnota musí být v hexadecimálním formátu, nikoli v obvyklém ASCII, takže výše uvedený klíč bude ve formátu:

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

Chcete-li převést titul, můžete použít online službu, např. tento.

Windows Native Applications a služba Acronis Active Restore
Ukazuje se, že ke spuštění nativní aplikace potřebujeme:

  1. Zkopírujte spustitelný soubor do složky system32
  2. Přidejte klíč do registru
  3. Restartujte stroj

Pro usnadnění je zde připravený skript pro instalaci nativní aplikace:

Install.bat

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

přidat.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

Po instalaci a restartu, ještě předtím, než se objeví obrazovka výběru uživatele, dostaneme následující obrázek:

Windows Native Applications a služba Acronis Active Restore

Celkový

Na příkladu takto malé aplikace jsme se přesvědčili, že je docela dobře možné provozovat aplikaci na úrovni Windows Native. Dále budeme s kluky z Innopolis University pokračovat v budování služby, která zahájí proces interakce s řidičem mnohem dříve než v předchozí verzi našeho projektu. A s příchodem shellu win32 by bylo logické přenést ovládání na plnohodnotnou službu, která již byla vyvinuta (více o tom zde).

V příštím článku se dotkneme další součásti služby Active Restore, a to ovladače UEFI. Přihlaste se k odběru našeho blogu, aby vám neunikl další příspěvek.

Zdroj: www.habr.com

Přidat komentář