Aplicații native Windows și serviciul Acronis Active Restore

Astăzi continuăm povestea despre cum, împreună cu colegii de la Universitatea Innopolis, dezvoltăm tehnologia Active Restore pentru a permite utilizatorului să înceapă să lucreze cât mai repede posibil pe mașina sa după o defecțiune. Vom discuta despre aplicațiile native Windows, inclusiv despre caracteristicile creării și lansării acestora. Sub acest articol – puțin despre proiectul nostru, dar și un ghid practic despre cum să scrii aplicații native.

Aplicații native Windows și serviciul Acronis Active Restore

În postările anterioare, am discutat deja despre ce este Active Restore, și cum studenții de la Innopolis dezvoltă serviciul. Astăzi vreau să mă opresc asupra aplicațiilor native, la nivelul cărora dorim să „scufundăm” serviciul nostru de recuperare activă. Dacă totul merge bine, atunci vom putea:

  • Să lansăm serviciul mult mai devreme
  • Să ne conectăm mult mai repede la cloudul în care se află backup-ul
  • Să înțelegem mult mai repede în ce mod se află sistemul – în modul de încărcare normală sau de recuperare
  • Să restaurăm cu mult mai puține fișiere în avans
  • Să permitem utilizatorului să înceapă să lucreze și mai repede.

Ce este, de fapt, o aplicație nativă?

Pentru a răspunde la această întrebare, haideți să ne uităm la secvența de apeluri pe care o face sistemul, de exemplu, dacă un programator în aplicația sa încearcă să creeze un fișier.

Aplicații native Windows și serviciul Acronis Active Restore
Pavel Yosifovich – Programare cu kernel Windows (2019)

Programatorul folosește funcția CreateFile, care este declarată în fișierul de antet fileapi.h și implementată în Kernel32.dll. Totuși, această funcție nu se ocupă de crearea fișierului, ci doar verifică argumentele de intrare și apelează funcția NtCreateFile (prefixul Nt indică că funcția este nativă). Această funcție este declarată în fișierul de antet winternl.h și implementată în ntdll.dll. Ea pregătește saltul în spațiul kernel-ului, după care efectuează un apel de sistem pentru a crea fișierul. Așadar, în acest caz, Kernel32 este doar un wrapper pentru Ntdll. Unul dintre motivele pentru care este făcut acest lucru este că Microsoft are posibilitatea să schimbe funcțiile lumii native fără a afecta interfețele standard. Microsoft nu recomandă apelarea directă a funcțiilor native și nu documentează cea mai mare parte dintre ele. Apropo, funcțiile nedocumentate pot fi găsite aici.

Principala avantaj al aplicațiilor native este că ntdll este încărcat în sistem mult mai devreme decât kernel32. Este logic, având în vedere că kernel32 necesită ntdll pentru a funcționa. Ca rezultat, aplicațiile care utilizează funcții native pot începe să funcționeze mult mai devreme.

Astfel, Aplicațiile Native Windows sunt programe capabile să pornească într-o fază timpurie a procesului de încărcare a Windows. Ele folosesc NUMAI funcții din ntdll. Un exemplu de astfel de aplicație: autochk care execută utilitarul chkdisk pentru a verifica discul pentru erori înainte de a porni serviciile principale. Exact la acest nivel vrem să vedem Active Restore.

Ce ne va trebui?

  • DDK (Driver Development Kit), cunoscut acum și sub numele de WDK 7 (Windows Driver Kit).
  • O mașină virtuală (de exemplu, Windows 7 x64)
  • Nu este obligatorie, dar pot ajuta fișierele header care pot fi descărcate aici

Ce este în cod?

Să facem puțin exercițiu și, de exemplu, să scriem o mică aplicație care:

  1. Afișează un mesaj pe ecran
  2. Alocă puțină memorie
  3. Așteaptă introducerea de la tastatură
  4. Eliberează memoria ocupată

În aplicațiile native, punctul de intrare nu este main sau winmain, ci funcția NtProcessStartup, deoarece practic pornim direct un nou proces în sistem.

Să începem cu afișarea unui mesaj pe ecran. Pentru aceasta, avem funcția nativă NtDisplayString, care ca argument primește un pointer la un obiect al structurii UNICODE_STRING. Îl putem initializa cu ajutorul lui RtlInitUnicodeString. Ca urmare, pentru a afișa textul pe ecran, putem scrie o mică funcție astfel:

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

Deoarece avem acces doar la funcții din ntdll și alte biblioteci nu sunt încă în memorie, vom avea neapărat probleme cu alocarea memoriei. Operatorul new nu există încă (deoarece provine dintr-o lume mult mai înaltă C++), de asemenea nu există funcția malloc (deoarece pentru ea sunt necesare biblioteci runtime C). Putem folosi doar stiva. Dar dacă trebuie să alocăm dinamic memorie, va trebui să facem acest lucru în heap. Așadar, să creăm pentru noi un heap și să luăm din el memorie atunci când avem nevoie.

Pentru această sarcină, funcția RtlCreateHeapeste potrivită. Apoi, folosind RtlAllocateHeap și RtlFreeHeap, vom ocupa și elibera memorie atunci când avem nevoie.

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

// creează heap pentru a aloca memorie mai târziu
memory = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// alocă buffer de dimensiune bufferSize
buffer = RtlAllocateHeap(
  memory, 
  HEAP_ZERO_MEMORY, 
  bufferSize
);

// eliberează bufferul (de fapt, nu este necesar deoarece distrugem heapul în pasul următor)
RtlFreeHeap(memory, 0, buffer);

RtlDestroyHeap(memory);

Să trecem la așteptarea intrării de la tastatură.

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

Tot ce trebuie să facem este să folosim NtReadFile pe dispozitivul deschis și să așteptăm până când tastatura ne va returna o apăsare. În cazul în care este apăsată tasta ESC, vom continua lucrul. Pentru a deschide dispozitivul, va trebui să apelăm funcția NtCreateFile (va trebui să deschidem DeviceKeyboardClass0). De asemenea, vom apela NtCreateEvent, pentru a inițializa un obiect pentru așteptare. Vom declara singuri structura KEYBOARD_INPUT_DATA, care reprezintă datele tastaturii. Aceasta ne va facilita munca.

Funcția aplicației native se încheie prin apelarea funcției NtTerminateProcess, pentru că practic ne ucidem propriul proces.

Întregul cod al aplicației noastre mici:

#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: Putem folosi cu ușurință în cod funcția DbgBreakPoint() pentru a ne opri în debugger. Cu toate acestea, va trebui să conectăm WinDbg la mașina virtuală pentru depanarea kernelului. Instrucțiunile despre cum să facem asta le putem găsi aici sau pur și simplu să folosim VirtualKD.

Compilarea și construcția

Cel mai simplu mod de a construi o aplicație nativă este să folosim DDK (Driver Development Kit). Avem nevoie exact de vechea versiune a șaptea, deoarece versiunile mai recente au o abordare ușor diferită și colaborează strâns cu Visual Studio. Dacă folosim DDK, atunci proiectul nostru are nevoie doar de Makefile și surse.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

surse:

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-ul tău va fi exact la fel, iar la surse să ne oprim puțin mai în detaliu. În acest fișier se specifică sursele programului tău (fișiere .c), opțiunile de construcție și alte parametere.

  • TARGETNAME – numele fișierului executabil care ar trebui să rezulte la final.
  • TARGETTYPE – tipul fișierului executabil, poate fi un driver (.sys), în acest caz valoarea câmpului trebuie să fie DRIVER, dacă este o bibliotecă (.lib), atunci valoarea LIBRARY. În cazul nostru, avem nevoie de un fișier executabil (.exe), așa că setăm valoarea PROGRAM.
  • UMTYPE – valorile posibile pentru acest câmp sunt: console pentru aplicații de consolă, windows pentru aplicații în modul fereastră. Totuși, trebuie să specificăm nt pentru a obține o aplicație nativă.
  • BUFFER_OVERFLOW_CHECKS – verificarea stivei pentru suprasarcină de buffer, din păcate nu este cazul nostru, deci dezactivăm.
  • MINWIN_SDK_LIB_PATH – această valoare se referă la variabila SDK_LIB_PATH, nu este o problemă dacă această variabilă de sistem nu este declarată, atunci când vom rula build-ul verificat din DDK, această variabilă va fi declarată și va indica bibliotecile necesare.
  • SOURCES – lista surselor programului tău.
  • INCLUDES – fișierele de antet necesare pentru compilare. De obicei, aici se specifică calea către fișierele incluse cu DDK, dar poți adăuga și alte fișiere dacă este necesar.
  • TARGETLIBS – lista bibliotecilor care trebuie legate.
  • USE_NTDLL – câmp obligatoriu care trebuie setat pe 1. Din motive destul de evidente.
  • USER_C_FLAGS – orice flage pe care le poți folosi în directivele preprocesorului în pregătirea codului aplicației.

Așadar, pentru a compila, trebuie să lansăm x86 (sau x64) Checked Build, să schimbăm directorul de lucru în folderul proiectului și să executăm comanda Build. Rezultatul de pe captură arată că am obținut un fișier executabil.

Aplicații native Windows și serviciul Acronis Active Restore

Acest fișier nu poate fi lansat cu ușurință, sistemul dă o eroare și ne trimite să ne gândim la comportamentul său cu următoarea eroare:

Aplicații native Windows și serviciul Acronis Active Restore

Cum se lansează o aplicație nativă?

La începutul autochk, secvența de lansare a programelor este determinată de valoarea cheia de registru:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Managerul sesiunii execută programele din această listă pe rând. Executabilele sunt căutate de managerul sesiunii în directorul system32. Formatul valorii cheii de registru este următorul:

autocheck autochk *MyNative

Valoarea trebuie să fie în format hexazecimal, nu în ASCII obișnuit, prin urmare cheia prezentată mai sus va avea formatul:

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

Pentru a converti numele, poți folosi un serviciu online, de exemplu, aceasta.

Aplicații native Windows și serviciul Acronis Active Restore
Prin urmare, pentru a lansa o aplicație nativă, trebuie să:

  1. Copiați fișierul executabil în folderul system32
  2. Adăugați o cheie în registru
  3. Reporniți mașina

Pentru comoditate, iată un script pregătit pentru instalarea aplicației native:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Exemplu Native Instalate
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

După instalare și repornire, înainte de a apărea ecranul de selecție a utilizatorilor, vom obține următoarea imagine:

Aplicații native Windows și serviciul Acronis Active Restore

Rezultatul

Pe exemplul acestei aplicații mici, am demonstrat că este posibil să rulezi aplicația la nivel Windows Native. În continuare, împreună cu colegii de la Universitatea Innopolis, vom continua să construim un serviciu care va iniția procesul de interacțiune cu driverul mult mai devreme decât în versiunea anterioară a proiectului nostru. Odată cu apariția interfeței win32, va fi logic să transferăm controlul către un serviciu complet dezvoltat (despre care vom discuta mai în detaliu) aici).

În următorul articol, ne vom ocupa de un alt component al serviciului Active Restore, și anume driverul UEFI. Abonați-vă la blogul nostru pentru a nu pierde următoarea postare.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster