Windows Native Applications in storitev Acronis Active Restore

Danes nadaljujemo zgodbo o tem, kako skupaj s fanti z Univerze Innopolis razvijamo tehnologijo Active Restore, ki uporabniku omogoča, da začne delati na svojem računalniku čim prej po okvari. Govorili bomo o izvornih aplikacijah Windows, vključno s funkcijami njihovega ustvarjanja in zagona. Spodaj je nekaj o našem projektu, pa tudi praktičen vodnik o pisanju izvornih aplikacij.

Windows Native Applications in storitev Acronis Active Restore

V prejšnjih objavah smo že govorili o tem, kaj je Aktivna obnovitevin kako se razvijajo učenci iz Innopolisa storitev. Danes se želim osredotočiti na izvorne aplikacije, na raven katerih želimo »pokopati« našo storitev aktivne obnovitve. Če bo vse delovalo, bomo lahko:

  • Zaženite samo storitev veliko prej
  • Veliko prej se obrnite na oblak, kjer se nahaja varnostna kopija
  • Veliko prej, da bi razumeli, v kakšnem načinu je sistem - običajni zagon ali obnovitev
  • Veliko manj datotek za vnaprejšnjo obnovitev
  • Omogočite uporabniku, da začne še hitreje.

Kaj sploh je izvorna aplikacija?

Da bi odgovorili na to vprašanje, poglejmo zaporedje klicev, ki jih sistem opravi, na primer, če programer v svoji aplikaciji poskuša ustvariti datoteko.

Windows Native Applications in storitev Acronis Active Restore
Pavel Yosifovich - Programiranje jedra Windows (2019)

Programer uporablja funkcijo CreateFile, ki je deklariran v datoteki glave fileapi.h in implementiran v Kernel32.dll. Vendar ta funkcija sama ne ustvari datoteke, ampak samo preveri vhodne argumente in pokliče funkcijo NtCreateFile (predpona Nt samo označuje, da je funkcija izvorna). Ta funkcija je deklarirana v datoteki glave winternl.h in implementirana v ntdll.dll. Pripravi se na skok v jedrski prostor, nato pa izvede sistemski klic za ustvarjanje datoteke. V tem primeru se izkaže, da je Kernel32 samo ovoj za Ntdll. Eden od razlogov, zakaj je bilo to storjeno, je, da ima Microsoft tako možnost spreminjanja funkcij domačega sveta, vendar se ne dotika standardnih vmesnikov. Microsoft ne priporoča neposrednega klicanja izvornih funkcij in večine od njih ne dokumentira. Mimogrede, najdete nedokumentirane funkcije tukaj.

Glavna prednost izvornih aplikacij je, da se ntdll naloži v sistem veliko prej kot kernel32. To je logično, ker kernel32 za delovanje potrebuje ntdll. Posledično lahko aplikacije, ki uporabljajo izvorne funkcije, začnejo delovati veliko prej.

Tako so domače aplikacije Windows programi, ki se lahko zaženejo zgodaj ob zagonu sistema Windows. Uporabljajo SAMO funkcije iz ntdll. Primer takšne aplikacije: autochk ki opravlja pripomoček chkdisk da na disku preverite napake pred zagonom glavnih storitev. To je točno raven, za katero želimo, da je naša aktivna obnovitev.

Kaj potrebujemo?

  • DDK (Driver Development Kit), zdaj znan tudi kot WDK 7 (Windows Driver Kit).
  • Virtualni stroj (na primer Windows 7 x64)
  • Ni potrebno, vendar lahko pomagajo datoteke glave, ki jih je mogoče prenesti tukaj

Kaj je v kodi?

Malo vadimo in na primer napišemo majhno aplikacijo, ki:

  1. Prikaže sporočilo na zaslonu
  2. Dodeli nekaj pomnilnika
  3. Čaka na vnos s tipkovnice
  4. Sprosti uporabljeni pomnilnik

V izvornih aplikacijah vstopna točka ni main ali winmain, temveč funkcija NtProcessStartup, saj pravzaprav neposredno zaženemo nove procese v sistemu.

Začnimo s prikazom sporočila na zaslonu. Za to imamo domačo funkcijo NtDisplayString, ki kot argument sprejme kazalec na objekt strukture UNICODE_STRING. RtlInitUnicodeString nam bo pomagal pri inicializaciji. Posledično lahko za prikaz besedila na zaslonu napišemo to majhno funkcijo:

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

Ker so nam na voljo samo funkcije iz ntdll, drugih knjižnic pa preprosto še ni v pomnilniku, bomo zagotovo imeli težave z dodeljevanjem pomnilnika. Novi operater še ne obstaja (ker prihaja iz previsokonivojskega sveta C++) in ni funkcije malloc (zahteva izvajalne knjižnice C). Seveda lahko uporabite le kup. Če pa moramo dinamično dodeliti pomnilnik, bomo morali to storiti na kupu (tj. kopici). Ustvarimo si torej kup zase in iz njega vzemimo spomin, kadar koli ga potrebujemo.

Funkcija je primerna za to nalogo RtlCreateHeap. Nato bomo z uporabo RtlAllocateHeap in RtlFreeHeap zasedli in sprostili pomnilnik, ko ga bomo potrebovali.

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

Preidimo na čakanje na vnos s tipkovnico.

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

Vse, kar potrebujemo, je uporaba NtReadFile na odprti napravi in ​​počakajte, da nam tipkovnica vrne pritisk. Če pritisnemo tipko ESC, nadaljujemo z delom. Za odpiranje naprave bomo morali poklicati funkcijo NtCreateFile (odpreti bomo morali DeviceKeyboardClass0). Bomo tudi poklicali NtCreateEventza inicializacijo čakajočega objekta. Sami bomo deklarirali strukturo KEYBOARD_INPUT_DATA, ki predstavlja podatke tipkovnice. To nam bo olajšalo delo.

Izvorna aplikacija se konča s klicem funkcije NtTerminateProcessker preprosto ubijamo svoj proces.

Vsa koda za našo majhno aplikacijo:

#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: Z lahkoto lahko uporabimo funkcijo DbgBreakPoint() v naši kodi, da jo zaustavimo v razhroščevalniku. Res je, da boste morali WinDbg povezati z virtualnim strojem za razhroščevanje jedra. Navodila, kako to storiti, najdete tukaj ali samo uporabite VirtualKD.

Sestavljanje in sestavljanje

Najlažji način za izdelavo domače aplikacije je uporaba DDK (Komplet za razvoj gonilnikov). Potrebujemo starodavno sedmo različico, saj imajo kasnejše različice nekoliko drugačen pristop in tesno sodelujejo z Visual Studio. Če uporabljamo DDK, potrebuje naš projekt samo Makefile in vire.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

Viri:

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 bo popolnoma enak, vendar si poglejmo vire malo bolj podrobno. Ta datoteka določa izvorne kode vašega programa (.c datoteke), možnosti gradnje in druge parametre.

  • TARGETNAME – ime izvršljive datoteke, ki naj bi bila na koncu izdelana.
  • TARGETTYPE – tip izvršljive datoteke, lahko je gonilnik (.sys), potem naj bo vrednost polja GONILNIK, če je knjižnica (.lib), potem je vrednost KNJIŽNICA. V našem primeru potrebujemo izvršljivo datoteko (.exe), zato nastavimo vrednost na PROGRAM.
  • UMTYPE – možne vrednosti za to polje: konzola za konzolno aplikacijo, okna za delo v okenskem načinu. Vendar moramo določiti nt, da dobimo izvorno aplikacijo.
  • BUFFER_OVERFLOW_CHECKS – preverjanje sklada za prepolnitev medpomnilnika, žal ne v našem primeru, izklopimo ga.
  • MINWIN_SDK_LIB_PATH – ta vrednost se nanaša na spremenljivko SDK_LIB_PATH, ne skrbite, da nimate navedene sistemske spremenljivke, ko zaženemo preverjeno gradnjo iz DDK, bo ta spremenljivka deklarirana in bo kazala na potrebne knjižnice.
  • VIRI – seznam virov za vaš program.
  • VKLJUČUJE – datoteke glave, ki so potrebne za sestavljanje. Tukaj običajno navedejo pot do datotek, ki so priložene DDK, lahko pa dodatno določite tudi druge.
  • TARGETLIBS – seznam knjižnic, ki jih je treba povezati.
  • USE_NTDLL je obvezno polje, ki mora biti iz očitnih razlogov nastavljeno na 1.
  • USER_C_FLAGS – vse zastavice, ki jih lahko uporabite v direktivah predprocesorja pri pripravi kode aplikacije.

Za gradnjo moramo torej zagnati x86 (ali x64) Checked Build, spremeniti delovni imenik v mapo projekta in zagnati ukaz Build. Rezultat na posnetku zaslona kaže, da imamo eno izvršljivo datoteko.

Windows Native Applications in storitev Acronis Active Restore

Te datoteke ni mogoče zagnati tako enostavno, sistem preklinja in nas pošilja v razmislek o njenem vedenju z naslednjo napako:

Windows Native Applications in storitev Acronis Active Restore

Kako zagnati izvorno aplikacijo?

Ko se autochk zažene, je zagonsko zaporedje programov določeno z vrednostjo registrskega ključa:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Upravljalnik sej izvaja programe s tega seznama enega za drugim. Upravljalnik sej poišče same izvršljive datoteke v imeniku system32. Format vrednosti registrskega ključa je naslednji:

autocheck autochk *MyNative

Vrednost mora biti v šestnajstiški obliki, ne v običajnem ASCII, zato bo zgoraj prikazani ključ v obliki:

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

Za pretvorbo naslova lahko uporabite spletno storitev, npr. ta.

Windows Native Applications in storitev Acronis Active Restore
Izkazalo se je, da za zagon izvorne aplikacije potrebujemo:

  1. Kopirajte izvršljivo datoteko v mapo system32
  2. Dodajte ključ v register
  3. Ponovno zaženite stroj

Za udobje je tukaj že pripravljen skript za namestitev izvorne aplikacije:

install.bat

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

dop.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 namestitvi in ​​ponovnem zagonu, še preden se prikaže zaslon za izbiro uporabnika, bomo dobili naslednjo sliko:

Windows Native Applications in storitev Acronis Active Restore

Skupaj

Na primeru tako majhne aplikacije smo se prepričali, da je povsem možno pognati aplikacijo na ravni Windows Native. Nato bomo s fanti z univerze Innopolis nadaljevali z izgradnjo storitve, ki bo sprožila proces interakcije z voznikom veliko prej kot v prejšnji različici našega projekta. In s prihodom lupine win32 bi bilo logično prenesti nadzor na polnopravno storitev, ki je že bila razvita (več o tem tukaj).

V naslednjem članku se bomo dotaknili še ene komponente storitve Active Restore, in sicer gonilnika UEFI. Naročite se na naš blog, da ne zamudite naslednje objave.

Vir: www.habr.com

Dodaj komentar