Windows Native Applications a služba Acronis Active Restore

Dnes pokračujeme v príbehu o tom, ako spolu s chalanmi z Innopolis University vyvíjame technológiu Active Restore, aby používateľ mohol začať pracovať na svojom stroji čo najskôr po zlyhaní. Budeme hovoriť o natívnych aplikáciách pre Windows vrátane funkcií ich vytvárania a spúšťania. Pod zostrihom je niečo málo o našom projekte, ako aj praktický návod, ako písať natívne aplikácie.

Windows Native Applications a služba Acronis Active Restore

V predchádzajúcich príspevkoch sme už hovorili o tom, čo to je Aktívne obnoveniea ako sa rozvíjajú študenti z Innopolisu služba. Dnes sa chcem venovať natívnym aplikáciám, na úroveň ktorých chceme „pochovať“ našu službu aktívneho obnovenia. Ak všetko funguje, potom budeme môcť:

  • Samotnú službu spustite oveľa skôr
  • Oveľa skôr kontaktujte cloud, kde sa záloha nachádza
  • Oveľa skôr, aby ste pochopili, v akom režime je systém - normálne spustenie alebo obnovenie
  • Oveľa menej súborov na obnovenie vopred
  • Umožnite používateľovi začať ešte rýchlejšie.

Čo je vlastne natívna aplikácia?

Aby sme odpovedali na túto otázku, pozrime sa na postupnosť volaní, ktoré systém vykoná, napríklad ak sa programátor vo svojej aplikácii pokúsi vytvoriť súbor.

Windows Native Applications a služba Acronis Active Restore
Pavel Yosifovich - programovanie jadra systému Windows (2019)

Programátor používa funkciu CreateFile, ktorý je deklarovaný v hlavičkovom súbore fileapi.h a implementovaný v Kernel32.dll. Táto funkcia však sama o sebe súbor nevytvára, iba kontroluje vstupné argumenty a volá funkciu NtCreateFile (predpona Nt len ​​označuje, že funkcia je natívna). Táto funkcia je deklarovaná v hlavičkovom súbore winternl.h a implementovaná v ntdll.dll. Pripraví sa na skok do jadrového priestoru, po ktorom vykoná systémové volanie na vytvorenie súboru. V tomto prípade sa ukazuje, že Kernel32 je len obal pre Ntdll. Jedným z dôvodov, prečo sa tak stalo, je, že Microsoft tak má možnosť meniť funkcie pôvodného sveta, ale nedotýkať sa štandardných rozhraní. Microsoft neodporúča volať priamo natívne funkcie a väčšinu z nich nedokumentuje. Mimochodom, možno nájsť nezdokumentované funkcie tu.

Hlavnou výhodou natívnych aplikácií je, že ntdll sa načíta do systému oveľa skôr ako kernel32. Je to logické, pretože kernel32 vyžaduje na fungovanie ntdll. Vďaka tomu môžu aplikácie využívajúce natívne funkcie začať fungovať oveľa skôr.

Natívne aplikácie Windows sú teda programy, ktoré sa môžu spustiť skoro pri zavádzaní systému Windows. Používajú LEN funkcie z ntdll. Príklad takejto aplikácie: autochk kto vystupuje nástroj chkdisk na kontrolu chýb na disku pred spustením hlavných služieb. To je presne úroveň, na ktorej chceme, aby bola naša funkcia Active Restore.

Čo potrebujeme?

  • DDK (Driver Development Kit), teraz tiež známy ako WDK 7 (Windows Driver Kit).
  • Virtuálny počítač (napríklad Windows 7 x64)
  • Nie je to potrebné, ale môžu pomôcť súbory hlavičiek, ktoré je možné stiahnuť tu

Čo je v kóde?

Poďme si trochu zacvičiť a napríklad napísať malú aplikáciu, ktorá:

  1. Zobrazí správu na obrazovke
  2. Prideľuje určitú pamäť
  3. Čaká na vstup z klávesnice
  4. Uvoľňuje použitú pamäť

V natívnych aplikáciách nie je vstupným bodom main alebo winmain, ale funkcia NtProcessStartup, keďže vlastne priamo spúšťame nové procesy v systéme.

Začnime zobrazením správy na obrazovke. Na to máme natívnu funkciu NtDisplayString, ktorý berie ako argument ukazovateľ na objekt štruktúry UNICODE_STRING. RtlInitUnicodeString nám ho pomôže inicializovať. Výsledkom je, že na zobrazenie textu na obrazovke môžeme napísať túto malú funkciu:

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

Keďže sú nám dostupné len funkcie z ntdll a iné knižnice jednoducho v pamäti zatiaľ nie sú, určite budeme mať problémy s alokáciou pamäte. Nový operátor ešte neexistuje (pretože pochádza z príliš vysokého sveta C++) a neexistuje žiadna funkcia malloc (vyžaduje runtime knižnice C). Samozrejme, môžete použiť iba zásobník. Ale ak potrebujeme dynamicky alokovať pamäť, budeme to musieť urobiť na halde (t.j. halde). Vytvorme si teda kôpku pre seba a vezmime si z nej pamäť vždy, keď ju budeme potrebovať.

Funkcia je vhodná pre túto úlohu RtlCreateHeap. Ďalej pomocou RtlAllocateHeap a RtlFreeHeap zaberieme a uvoľníme pamäť, keď ju budeme potrebovať.

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

Prejdime k čakaniu 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šetko, čo potrebujeme, je použiť NtReadFile na otvorenom zariadení a počkajte, kým nám klávesnica nevráti žiadne stlačenie. Ak stlačíte kláves ESC, pokračujeme v práci. Na otvorenie zariadenia budeme musieť zavolať funkciu NtCreateFile (budeme musieť otvoriť DeviceKeyboardClass0). Zavoláme aj my NtCreateEventna inicializáciu objektu čakania. Sami si deklarujeme štruktúru KEYBOARD_INPUT_DATA, ktorá reprezentuje dáta klávesnice. To nám uľahčí prácu.

Natívna aplikácia končí volaním funkcie NtTerminateProcesspretože jednoducho zabíjame svoj vlastný proces.

Celý kód pre našu malú aplikáciu:

#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: Na zastavenie v debuggeri môžeme jednoducho použiť funkciu DbgBreakPoint() v našom kóde. Je pravda, že na ladenie jadra budete musieť pripojiť WinDbg k virtuálnemu stroju. Návod ako na to nájdete tu alebo len použiť VirtualKD.

Kompilácia a montáž

Najjednoduchší spôsob, ako vytvoriť natívnu aplikáciu, je použiť DDK (Súprava na vývoj ovládačov). Potrebujeme starú siedmu verziu, pretože neskoršie verzie majú trochu iný prístup a úzko spolupracujú s Visual Studio. Ak použijeme DDK, potom náš projekt potrebuje iba 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 úplne rovnaký, ale pozrime sa na zdroje trochu podrobnejšie. Tento súbor špecifikuje zdroje vášho programu (súbory .c), možnosti zostavenia a ďalšie parametre.

  • TARGETNAME – názov spustiteľného súboru, ktorý by mal byť nakoniec vytvorený.
  • TARGETTYPE – typ spustiteľného súboru, môže to byť ovládač (.sys), potom hodnota poľa by mala byť DRIVER, ak knižnica (.lib), potom hodnota je LIBRARY. V našom prípade potrebujeme spustiteľný súbor (.exe), preto nastavíme hodnotu PROGRAM.
  • UMTYPE – možné hodnoty pre toto pole: konzola pre konzolovú aplikáciu, okná pre prácu v režime okna. Ale musíme zadať nt, aby sme získali natívnu aplikáciu.
  • BUFFER_OVERFLOW_CHECKS – kontrola zásobníka na pretečenie vyrovnávacej pamäte, bohužiaľ nie náš prípad, vypneme.
  • MINWIN_SDK_LIB_PATH – táto hodnota sa vzťahuje na premennú SDK_LIB_PATH, nebojte sa, že takúto systémovú premennú deklarovanú nemáte, keď spustíme kontrolované zostavenie z DDK, táto premenná bude deklarovaná a bude ukazovať na potrebné knižnice.
  • SOURCES – zoznam zdrojov pre váš program.
  • OBSAHUJE – hlavičkové súbory, ktoré sú potrebné na zostavenie. Tu zvyčajne označujú cestu k súborom, ktoré sa dodávajú s DDK, ale môžete dodatočne zadať akékoľvek iné.
  • TARGETLIBS – zoznam knižníc, ktoré je potrebné prepojiť.
  • USE_NTDLL je povinné pole, ktoré musí byť zo zrejmých dôvodov nastavené na 1.
  • USER_C_FLAGS – akékoľvek príznaky, ktoré môžete použiť v direktívach preprocesora pri príprave kódu aplikácie.

Aby sme mohli zostaviť, musíme spustiť x86 (alebo x64) Checked Build, zmeniť pracovný adresár na priečinok projektu a spustiť príkaz Build. Výsledok na snímke obrazovky ukazuje, že máme jeden spustiteľný súbor.

Windows Native Applications a služba Acronis Active Restore

Tento súbor sa nedá tak ľahko spustiť, systém nadáva a posiela nás zamyslieť sa nad jeho správaním s nasledujúcou chybou:

Windows Native Applications a služba Acronis Active Restore

Ako spustiť natívnu aplikáciu?

Keď sa spustí autochk, postupnosť spúšťania programov je určená hodnotou kľúča databázy Registry:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Správca relácie spúšťa programy z tohto zoznamu jeden po druhom. Správca relácie hľadá samotné spustiteľné súbory v adresári system32. Formát hodnoty kľúča databázy Registry je nasledujúci:

autocheck autochk *MyNative

Hodnota musí byť v hexadecimálnom formáte, nie v bežnom ASCII, takže kľúč zobrazený vyššie bude vo formáte:

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

Na konverziu titulu môžete použiť online službu, napr. toto.

Windows Native Applications a služba Acronis Active Restore
Ukazuje sa, že na spustenie natívnej aplikácie potrebujeme:

  1. Skopírujte spustiteľný súbor do priečinka system32
  2. Pridajte kľúč do registra
  3. Reštartujte stroj

Pre pohodlie je tu pripravený skript na inštaláciu natívnej aplikácie:

install.bat

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

pridať.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 inštalácii a reštarte, ešte pred zobrazením obrazovky výberu používateľa, dostaneme nasledujúci obrázok:

Windows Native Applications a služba Acronis Active Restore

Celkový

Na príklade takejto malej aplikácie sme sa presvedčili, že je celkom možné spustiť aplikáciu na úrovni Windows Native. Ďalej budeme s chalanmi z Innopolis University pokračovať v budovaní služby, ktorá spustí proces interakcie s vodičom oveľa skôr ako v predchádzajúcej verzii nášho projektu. A s príchodom shellu win32 by bolo logické preniesť riadenie na plnohodnotnú službu, ktorá už bola vyvinutá (viac o tom tu).

V ďalšom článku sa dotkneme ďalšej súčasti služby Active Restore, a to ovládača UEFI. Prihláste sa na odber nášho blogu, aby vám neušiel ďalší príspevok.

Zdroj: hab.com

Pridať komentár