Windows izvorne aplikacije i usluga Acronis Active Restore

Danas nastavljamo priču o tome kako mi, zajedno s dečkima sa Sveučilišta Innopolis, razvijamo Active Restore tehnologiju kako bismo omogućili korisniku da počne raditi na svom stroju što je prije moguće nakon kvara. Razgovarat ćemo o izvornim Windows aplikacijama, uključujući značajke njihovog stvaranja i pokretanja. Ispod presjeka nalazi se nešto o našem projektu, kao i praktični vodič o tome kako pisati izvorne aplikacije.

Windows izvorne aplikacije i usluga Acronis Active Restore

U prethodnim postovima smo već govorili o tome što je to Aktivno vraćanje, i kako se razvijaju studenti iz Innopolisa usluga. Danas se želim usredotočiti na izvorne aplikacije, na čiju razinu želimo "zakopati" našu aktivnu uslugu oporavka. Ako sve uspije, tada ćemo moći:

  • Puno ranije pokrenite sam servis
  • Kontaktirajte oblak u kojem se sigurnosna kopija nalazi mnogo ranije
  • Mnogo ranije da biste razumjeli u kojem je načinu rada sustav - normalno pokretanje ili oporavak
  • Puno manje datoteka za oporavak unaprijed
  • Omogućite korisniku da počne još brže.

Što je uopće izvorna aplikacija?

Da bismo odgovorili na ovo pitanje, pogledajmo redoslijed poziva koje sustav upućuje, na primjer, ako programer u svojoj aplikaciji pokuša stvoriti datoteku.

Windows izvorne aplikacije i usluga Acronis Active Restore
Pavel Yosifovich - Windows Kernel Programming (2019)

Programer koristi funkciju CreateFile, koji je deklariran u datoteci zaglavlja fileapi.h i implementiran u Kernel32.dll. Međutim, sama ova funkcija ne stvara datoteku, ona samo provjerava ulazne argumente i poziva funkciju NtCreateFile (prefiks Nt samo označava da je funkcija izvorna). Ova je funkcija deklarirana u datoteci zaglavlja winternl.h i implementirana u ntdll.dll. Priprema se za skok u nuklearni prostor, nakon čega upućuje sistemski poziv za stvaranje datoteke. U ovom slučaju ispada da je Kernel32 samo omotač za Ntdll. Jedan od razloga zašto je to učinjeno je taj što Microsoft na taj način ima mogućnost mijenjati funkcije matičnog svijeta, ali ne dirati standardna sučelja. Microsoft ne preporučuje izravno pozivanje izvornih funkcija i ne dokumentira većinu njih. Usput, mogu se pronaći nedokumentirane funkcije ovdje.

Glavna prednost izvornih aplikacija je da se ntdll učitava u sustav mnogo ranije nego kernel32. To je logično, jer kernel32 zahtijeva ntdll za rad. Kao rezultat toga, aplikacije koje koriste izvorne funkcije mogu početi raditi mnogo ranije.

Dakle, izvorne Windows aplikacije su programi koji se mogu pokrenuti rano tijekom pokretanja sustava Windows. Koriste SAMO funkcije iz ntdll. Primjer takve aplikacije: autochk koji izvodi uslužni program chkdisk za provjeru grešaka na disku prije pokretanja glavnih usluga. To je upravo ona razina koju želimo da bude naš Active Restore.

Što nam je potrebno?

  • DDK (Driver Development Kit), sada poznat i kao WDK 7 (Windows Driver Kit).
  • Virtualni stroj (na primjer, Windows 7 x64)
  • Nije potrebno, ali datoteke zaglavlja koje se mogu preuzeti mogu pomoći ovdje

Što je u šifri?

Hajdemo malo vježbati i, na primjer, napisati malu aplikaciju koja:

  1. Prikazuje poruku na ekranu
  2. Dodjeljuje dio memorije
  3. Čeka unos s tipkovnice
  4. Oslobađa korištenu memoriju

U nativnim aplikacijama ulazna točka nije main ili winmain, već funkcija NtProcessStartup, budući da zapravo izravno pokrećemo nove procese u sustavu.

Počnimo prikazivanjem poruke na ekranu. Za to imamo izvornu funkciju NtDisplayString, koji kao argument uzima pokazivač na objekt strukture UNICODE_STRING. RtlInitUnicodeString će nam pomoći da ga inicijaliziramo. Kao rezultat, za prikaz teksta na ekranu možemo napisati ovu malu funkciju:

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

Budući da su nam dostupne samo funkcije iz ntdll-a, a jednostavno još nema drugih biblioteka u memoriji, sigurno ćemo imati problema s alociranjem memorije. Novi operator još ne postoji (jer dolazi iz svijeta previsoke razine C++), i ne postoji funkcija malloc (zahtijeva runtime C biblioteke). Naravno, možete koristiti samo stog. Ali ako trebamo dinamički dodijeliti memoriju, morat ćemo to učiniti na hrpi (tj. gomili). Zato stvorimo gomilu za sebe i uzimajmo iz nje memoriju kad god nam zatreba.

Funkcija je prikladna za ovaj zadatak RtlCreateHeap. Dalje, koristeći RtlAllocateHeap i RtlFreeHeap, zauzimat ćemo i oslobađati memoriju kada nam je potrebna.

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

Prijeđimo na čekanje unosa s tipkovnice.

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

Sve što trebamo je koristiti NtReadFile na otvorenom uređaju i pričekajte dok nam tipkovnica ne uzvrati bilo kakav pritisak. Ako pritisnemo tipku ESC, nastavljamo s radom. Da bismo otvorili uređaj, morat ćemo pozvati funkciju NtCreateFile (morat ćemo otvoriti DeviceKeyboardClass0). Također ćemo nazvati NtCreateEventza inicijaliziranje objekta čekanja. Sami ćemo deklarirati strukturu KEYBOARD_INPUT_DATA, koja predstavlja podatke tipkovnice. To će nam olakšati rad.

Izvorna aplikacija završava pozivom funkcije NtTerminateProcessjer jednostavno ubijamo vlastiti proces.

Sav kod za našu malu aplikaciju:

#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: Možemo jednostavno upotrijebiti funkciju DbgBreakPoint() u našem kodu da ga zaustavimo u programu za ispravljanje pogrešaka. Istina, morat ćete povezati WinDbg s virtualnim strojem za otklanjanje pogrešaka u jezgri. Upute kako to učiniti možete pronaći ovdje ili samo koristiti VirtualKD.

Kompilacija i montaža

Najlakši način za izradu izvorne aplikacije je korištenje DDK (Komplet za razvoj upravljačkih programa). Trebamo drevnu sedmu verziju, budući da kasnije verzije imaju nešto drugačiji pristup i blisko surađuju s Visual Studiom. Ako koristimo DDK, onda naš projekt treba samo Makefile i izvore.

makefile

!INCLUDE $(NTMAKEENV)makefile.def

izvori:

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

Vaš Makefile bit će potpuno isti, ali pogledajmo izvore malo detaljnije. Ova datoteka navodi izvore vašeg programa (.c datoteke), opcije izgradnje i druge parametre.

  • TARGETNAME – naziv izvršne datoteke koja se na kraju treba proizvesti.
  • TARGETTYPE – vrsta izvršne datoteke, može biti upravljački program (.sys), tada bi vrijednost polja trebala biti DRIVER, ako je biblioteka (.lib), tada je vrijednost LIBRARY. U našem slučaju, potrebna nam je izvršna datoteka (.exe), pa vrijednost postavljamo na PROGRAM.
  • UMTYPE – moguće vrijednosti za ovo polje: konzola za konzolnu aplikaciju, prozori za rad u prozorskom načinu rada. Ali moramo navesti nt da bismo dobili izvornu aplikaciju.
  • BUFFER_OVERFLOW_CHECKS – provjera steka za prekoračenje međuspremnika, nažalost nije naš slučaj, mi ga isključujemo.
  • MINWIN_SDK_LIB_PATH – ova se vrijednost odnosi na varijablu SDK_LIB_PATH, ne brinite da nemate deklariranu takvu sistemsku varijablu, kada pokrenemo provjerenu izgradnju iz DDK-a, ova varijabla će biti deklarirana i pokazat će na potrebne biblioteke.
  • IZVORI – popis izvora za vaš program.
  • UKLJUČUJE – datoteke zaglavlja koje su potrebne za sastavljanje. Ovdje obično označavaju put do datoteka koje dolaze s DDK-om, ali možete dodatno navesti bilo koji drugi.
  • TARGETLIBS – popis knjižnica koje je potrebno povezati.
  • USE_NTDLL je obavezno polje koje mora biti postavljeno na 1 iz očitih razloga.
  • USER_C_FLAGS – sve oznake koje možete koristiti u direktivama pretprocesora prilikom pripreme koda aplikacije.

Dakle, da bismo izgradili, moramo pokrenuti x86 (ili x64) Checked Build, promijeniti radni direktorij u mapu projekta i pokrenuti naredbu Build. Rezultat na snimci zaslona pokazuje da imamo jednu izvršnu datoteku.

Windows izvorne aplikacije i usluga Acronis Active Restore

Ova datoteka se ne može tako lako pokrenuti, sustav psuje i šalje nas da razmislimo o njenom ponašanju sljedećom greškom:

Windows izvorne aplikacije i usluga Acronis Active Restore

Kako pokrenuti nativnu aplikaciju?

Kada se pokrene autochk, redoslijed pokretanja programa određen je vrijednošću ključa registra:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Upravitelj sesije izvršava programe s ovog popisa jedan po jedan. Upravitelj sesije traži same izvršne datoteke u direktoriju system32. Format vrijednosti ključa registra je sljedeći:

autocheck autochk *MyNative

Vrijednost mora biti u heksadecimalnom formatu, a ne u uobičajenom ASCII formatu, tako da će ključ prikazan gore biti u formatu:

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

Da biste pretvorili naslov, možete koristiti internetsku uslugu, na primjer, ovo.

Windows izvorne aplikacije i usluga Acronis Active Restore
Ispada da nam je za pokretanje izvorne aplikacije potrebno:

  1. Kopirajte izvršnu datoteku u mapu system32
  2. Dodajte ključ u registar
  3. Ponovno pokrenite stroj

Radi praktičnosti, ovdje je gotova skripta za instaliranje izvorne aplikacije:

install.bat

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

dodati.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

Nakon instalacije i ponovnog pokretanja, čak i prije nego što se pojavi ekran za odabir korisnika, dobit ćemo sljedeću sliku:

Windows izvorne aplikacije i usluga Acronis Active Restore

Ukupan

Na primjeru tako male aplikacije uvjerili smo se da je sasvim moguće pokrenuti aplikaciju na Windows Native razini. Zatim ćemo dečki sa Sveučilišta Innopolis i ja nastaviti graditi uslugu koja će pokrenuti proces interakcije s vozačem mnogo ranije nego u prethodnoj verziji našeg projekta. A s pojavom ljuske win32 bilo bi logično prenijeti kontrolu na cjelovitu uslugu koja je već razvijena (više o tome здесь).

U sljedećem ćemo se članku dotaknuti još jedne komponente usluge Active Restore, naime UEFI drivera. Pretplatite se na naš blog kako ne biste propustili sljedeći post.

Izvor: www.habr.com

Dodajte komentar