Nativné aplikácie pre Windows a služba Acronis Active Restore

Dnes pokračujeme v rozprávaní o tom, ako spolu s chlapcami z Innopolis University vyvíjame technológiu Active Restore, aby sme používateľovi umožnili čo najskôr sa znova zapojiť do práce na svojom počítači po zlyhaní. Bude reč o natívnych aplikáciách Windows, vrátane vlastností ich vytvorenia a spustenia. Pod katom – trochu o našom projekte, ako aj praktický návod, ako písať natívne aplikácie.

Nativné aplikácie pre Windows a služba Acronis Active Restore

V predchádzajúcich príspevkoch sme už hovorili o tom, čo je to Active Restore, a ako študenti z Innopolisu vyvíjajú služba. Dnes sa chcem zamerať na natívne aplikácie, do úrovne ktorých chceme "zakopávať" našu službu aktívneho obnovenia. Ak sa všetko podarí, budeme môcť:

  • Oveľa skôr spustiť samotnú službu
  • Oveľa skôr sa spojiť s cloudom, v ktorom leží záloha
  • Oveľa skôr pochopiť, v akom režime sa nachádza systém – normálnom spustení alebo obnove
  • Oveľa menej súborov obnovovať vopred
  • Umožniť používateľovi začať pracovať ešte rýchlejšie.

Čo je vlastne natívna aplikácia?

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

Nativné aplikácie pre Windows a služba Acronis Active Restore
Pavel Yosifovich – Programovanie jadra Windows (2019)

Programátor používa funkciu CreateFile, ktorá je deklarovaná v hlavičkovom súbore fileapi.h a implementovaná v Kernel32.dll. Avšak táto funkcia sa neposkytuje pre vytvorenie súboru, len kontroluje argumenty na vstupe a volá funkciu NtCreateFile (predpona Nt svedčí o tom, že funkcia je natívna). Táto funkcia je deklarovaná v hlavičkovom súbore winternl.h a implementovaná v ntdll.dll. Pripravuje sa na skok do jadrového priestoru, po čom uskutočňuje systémové volanie na vytvorenie súboru. V tomto prípade ide o to, že Kernel32 je len obal pre Ntdll. Jedným z dôvodov, prečo je to takto urobené, je, že Microsoft má možnosť meniť funkcie natívneho sveta, ale pritom nezasahovať do štandardných rozhraní. Microsoft neodporúča priamo volať natívne funkcie a väčšinu z nich nedokumentuje. Mimochodom, nedokumentované funkcie je možné nájsť tu.

Hlavnou výhodou natívnych aplikácií je, že ntdll sa načítava do systému podstatne skôr ako kernel32. Je to logické, pretože kernel32 potrebuje ntdll na svoju činnosť. V dôsledku toho môžu aplikácie, ktoré používajú natívne funkcie, začať pracovať podstatne skôr.

Tým pádom, Windows Native Applications sú programy, ktoré sa dokážu spustiť v ranných fázach spúšťania Windows. Používajú IBA funkcie z ntdll. Príklad takejto aplikácie: autochk ktorý vykonáva chkdisk utility na kontrolu disku na chyby ešte pred spustením hlavných služieb. Presne na tejto úrovni chceme vidieť náš Active Restore.

Čo budeme potrebovať?

  • DDK (Driver Development Kit), dnes známy tiež ako WDK 7 (Windows Driver Kit).
  • Virtuálny stroj (napr. Windows 7 x64)
  • Nie je to nutné, ale môžu pomôcť hlavičkové súbory, ktoré si môžeme stiahnuť. tu

Čo sa nachádza v kóde?

Poďme si na chvíľu zacvičiť a ako príklad si napíšme malú aplikáciu, ktorá:

  1. Zobrazí správu na obrazovke
  2. Alokuje trochu pamäte
  3. Čaká na vstup z klávesnice
  4. Uvoľní obsadenú pamäť

V natívnych aplikáciách je bodom vstupu nie main alebo winmain, ale funkcia NtProcessStartup, pretože v podstate priamo spúšťame nové procesy v systéme.

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

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

Keďže máme prístup len k funkciám z ntdll a v pamäti ešte nie sú žiadne iné knižnice, určite sa nám vyskytnú problémy s alokovaním pamäte. Operátor new ešte neexistuje (pretože pochádza z príliš vysokoúrovňového sveta C++), rovnako chýba funkcia malloc (pre ňu sú potrebné knižnice runtime C). Môžeme síce používať len zásobník. Ale ak potrebujeme dynamicky alokovať pamäť, musíme to urobiť v hromade (t.j. heap). Preto si poďme vytvoriť hromadu a budeme si z nej brať pamäť, keď ju budeme potrebovať.

Na tento úkol nám vyhovuje funkcia RtlCreateHeap. Následne, pomocou RtlAllocateHeap a RtlFreeHeap, budeme zaberať a uvoľňovať pamäť, keď to budeme potrebovať.

PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;

// vytvorenie haldy na priradenie pamäte neskôr
memory = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// priradenie buffera s veľkosťou bufferSize
buffer = RtlAllocateHeap(
  memory, 
  HEAP_ZERO_MEMORY, 
  bufferSize
);

// uvoľnenie buffera (v skutočnosti nie je potrebné, pretože zničíme haldu v nasledujúcom kroku)
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 čakať, kým klávesnica nevráti akékoľvek stlačenie. V prípade, že bola stlačená klávesa ESC, budeme pokračovať v práci. Na otvorenie zariadenia budeme musieť zavolať funkciu NtCreateFile (otvoriť budeme musieť DeviceKeyboardClass0). Takisto zavoláme NtCreateEvent, aby sme inicializovali objekt na čakanie. Samostatne deklarujeme štruktúru KEYBOARD_INPUT_DATA, ktorá predstavuje dáta z klávesnice. To nám uľahčí prácu.

Práca natívnej aplikácie končí zavolaním funkcie NtTerminateProcess, pretože jednoducho zabíjame vlastný proces.

Celý kód našej malej aplikácie:

#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: Môžeme bez problémov použiť v kóde funkciu DbgBreakPoint() na zastavenie v debuggery. Pravda, budeme musieť pripojiť WinDbg k virtuálnemu stroju pre kernel debugging. Inštrukciu, ako to urobiť, možno nájsť tu alebo jednoducho použiť VirtualKD.

Kompilácia a zostavenie

Najjednoduchší spôsob zostaviť natívnu aplikáciu je použiť DDK (Driver Development Kit). Potrebujeme presne starú siedmu verziu, pretože novšie verzie majú trochu iný prístup a úzko spolupracujú s Visual Studio. Ak použijeme DDK, náš projekt potrebuje iba Makefile a 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

Váš Makefile bude presne taký istý, na sources sa však zastavme trochu podrobnejšie. V tomto súbore sa uvádzajú zdrojové súbory vašej aplikácie (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), v takom prípade by hodnota poľa mala byť DRIVER, ak knižnica (.lib), tak hodnota LIBRARY. V našom prípade potrebujeme spustiteľný súbor (.exe), preto nastavujeme hodnotu PROGRAM.
  • UMTYPE – možné hodnoty tohoto pole: console pro konzolovou aplikaci, windows pro provoz v režimu oken. Musíme však uvést nt, abychom získali nativní aplikaci.
  • BUFFER_OVERFLOW_CHECKS – kontrola zásobníku na přetečení, bohužel to není náš případ, vypínáme.
  • MINWIN_SDK_LIB_PATH – tato hodnota odkazuje na proměnnou SDK_LIB_PATH, nemusíte se obávat, že nemáte takovou systémovou proměnnou deklarovanou, v okamžiku, kdy spustíme checked build z DDK, bude tato proměnná deklarována a bude ukazovat na potřebné knihovny.
  • SOURCES – seznam zdrojů vaší aplikace.
  • INCLUDES – hlavičkové soubory, které jsou potřebné pro sestavení. Obvykle zde uvádějí cestu k souborům, které přicházejí s DDK, ale můžete uvést i jakékoliv další.
  • TARGETLIBS – seznam knihoven, které je třeba linkovat.
  • USE_NTDLL – povinné pole, které je třeba nastavit na hodnotu 1. Z očividných důvodů.
  • USER_C_FLAGS – jakékoliv příznaky, které můžete použít v preprocesorových direktivách při přípravě kódu aplikace.

Takže pro sestavení musíme spustit x86 (nebo x64) Checked Build, změnit pracovní adresář na složku s projektem a provést příkaz Build. Výsledek na snímku obrazovky ukazuje, že jsme vytvořili jeden spustitelný soubor.

Nativné aplikácie pre Windows a služba Acronis Active Restore

Tento soubor nelze tak snadno spustit, systém si stěžuje a posílá nás přemýšlet o svém chování s následující chybou:

Nativné aplikácie pre Windows a služba Acronis Active Restore

Jak spustit nativní aplikaci?

Při spuštění autochk určuje sekvenci spuštění programů hodnota klíče registru:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Správce relace postupně vykonává programy z tohoto seznamu. Samotné spustitelné soubory správce relace hledá v adresáři system32. Formát hodnoty klíče registru je následující:

autocheck autochk *MyNative

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

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

Pro převod názvu můžete použít online službu, například: web.

Nativné aplikácie pre Windows a služba Acronis Active Restore
Takže, abychom spustili nativní aplikaci, musíme:

  1. Zkopírovat spustitelný soubor do složky system32
  2. Přidat do registru klíč
  3. Restartovat stroj

Pro pohodlí zde máte 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

add.reg

REGEDIT4

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session 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 na výber používateľov, získame nasledujúci pohľad:

Nativné aplikácie pre Windows a služba Acronis Active Restore

Záver

Na príklade tejto malej aplikácie sme sa presvedčili, že spustiť aplikáciu na úrovni Windows Native je úplne možné. Pokračujeme so spolupracovníkmi z Innopolis University v budovaní služby, ktorá spustí proces interakcie s ovládačom oveľa skôr, než to bola v predchádzajúcej verzii nášho projektu. S príchodom shellwin32 bude logické odovzdať správu plnohodnotnej službe, ktorá už bola vyvinutá (o tom podrobnejšie tu).

V ďalšom článku sa budeme zaoberať ďalšou súčasťou služby Active Restore, konkrétne UEFI ovládačom. Sledujte náš blog, aby ste nezmeškali ďalší príspevok.

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster