Windows Native Applications i Acronis Active Restore servis

Danes nadaljujemo pripoved o tem, kako skupaj s fanti z Univerze Innopolis razvijamo tehnologijo Active Restore, da bi uporabniku omogočili, da čim prej začne delati na svojem računalniku po okvari. Govorili bomo o nativnih aplikacijah Windows, vključno z značilnostmi njihovega ustvarjanja in zagona. Podrobnosti o našem projektu in praktično vodilo o tem, kako pisati nativne aplikacije, najdete spodaj.

Windows Native Applications i Acronis Active Restore servis

V prejšnjih objavah smo že govorili o tem, kaj je Active Restore, in kako študenti iz Innopolisa razvijajo servis. Danes se želim osredotočiti na nativne aplikacije, do ravni katerih želimo 'poglobiti' naš servis aktivnega okrevanja. Če bo vse šlo po načrtih, bomo lahko:

  • Zagnali sam servis precej prej
  • Se povezali z oblakom, kjer je varnostna kopija, precej prej
  • Zelo hitro razumeli, v katerem načinu deluje sistem - v načinu normalnega zagona ali okrevanja
  • Zelo malo datotek predhodno obnovili
  • Omogočili uporabniku, da se loti dela še hitreje.

Kaj sploh je nativna aplikacija?

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

Windows Native Applications i Acronis Active Restore servis
Pavel Yosifovich — Windows Kernel Programming (2019)

Programer uporablja funkcijo CreateFile, ki je razglašena v glavi fileapi.h in implementirana v Kernel32.dll. Vendar sama ta funkcija ne ustvarja datoteke, ampak le preverja vhodne argumente in kliče funkcijo NtCreateFile (predpona Nt prav tako potrjuje, da je funkcija nativna). Ta funkcija je razglašena v glavi winternl.h in implementirana v ntdll.dll. Opravi priprave na skok v jedrni prostor, nato pa izvede sistemski klic za ustvarjanje datoteke. V tem primeru imamo torej Kernel32 kot le ovitek za Ntdll. Eden od razlogov za to je, da ima Microsoft tako možnost spreminjati funkcije nativnega sveta, ne da bi se dotikal standardnih vmesnikov. Microsoft ne priporoča neposrednega klica nativnim funkcijam in večino od njih ne dokumentira. Mimogrede, nedokumentirane funkcije lahko najdete. tukaj.

Glavna prednost nativnih aplikacija je u tome što se ntdll učitava u sistem mnogo ranije od kernel32. To je logično, jer kernel32 zahteva prisustvo ntdll za rad. Kao posledica, aplikacije koje koriste nativne funkcije mogu započeti rad značajno ranije.

Dakle, Windows Native Applications su programi koji mogu da se pokrenu u ranoj fazi podizanja Windows-a. Oni koriste SAMO funkcije iz ntdll. Primer takve aplikacije: autochk koji izvršava chkdisk utility za proveru diska na greške još pre pokretanja osnovnih servisa. Upravo na tom nivou želimo da vidimo naš Active Restore.

Šta će nam biti potrebno?

  • DDK (Driver Development Kit), danas poznat i kao WDK 7 (Windows Driver Kit).
  • Virtuelna mašina (na primer, Windows 7 x64)
  • Nije obavezno, ali mogu pomoći zaglavlja koja se mogu preuzeti tukaj

Šta je u kodu?

Hajde da se malo vežbamo i kao primer napišemo malu aplikaciju koja:

  1. Ispisuje poruku na ekran
  2. Alocira malo memorije
  3. Čeka unos sa tastature
  4. Oslobađa zauzetu memoriju

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

Počnimo sa ispisom poruke na ekran. Za to imamo nativnu funkciju NtDisplayString, koja kao argument prima pokazivač na objekat strukture UNICODE_STRING. Inicijalizovati ćemo ga pomoću RtlInitUnicodeString. Kao rezultat, za ispis teksta na ekran možemo napisati ovakvu malu funkciju:

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

Pošto su nam dostupne samo funkcije iz ntdll, i drugih biblioteka u memoriji jednostavno još nema, definitivno ćemo imati problema sa alokacijom memorije. Operator new još ne postoji (jer pripada previše visokom svetu C++), takođe nema funkcije malloc (za nju su potrebne C runtime biblioteke). Možda bismo mogli da koristimo samo stek. Ali ako nam je potrebna dinamička alokacija memorije, to ćemo morati da uradimo u hrpi (tj. heap). Zato hajde da stvorimo hrpu za sebe i uzimamo iz nje memoriju kad nam zatreba.

Za ovu svrhu odgovara funkcija RtlCreateHeap. Zatim, koristeći RtlAllocateHeap i RtlFreeHeap, zauzimaćemo i oslobađaćemo memoriju kada nam to bude potrebno.

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

// ustvarimo kup za dodelitev pomnilnika kasneje
memory = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// dodelimo pomnilnik velikosti bufferSize
buffer = RtlAllocateHeap(
  memory, 
  HEAP_ZERO_MEMORY, 
  bufferSize
);

// sprostimo pomnilnik (v resnici ni potrebno, ker bomo uničili kup v naslednjem koraku)
RtlFreeHeap(memory, 0, buffer);

RtlDestroyHeap(memory);

Preidimo na čakanje na vnose 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;
	}
}

Vse kar potrebujemo – je uporabiti NtReadFile na odprtem napravi in čakati, da tipkovnica vrne kakršno koli pritiskanje tipke. V primeru, da je pritisnjena tipka ESC, bomo nadaljevali z delom. Da bi odprli napravo, bomo morali poklicati funkcijo NtCreateFile (odpreti bo treba DeviceKeyboardClass0). Prav tako bomo poklicali NtCreateEvent, da inicializiramo objekt za čakanje. Sami bomo razglasili strukturo KEYBOARD_INPUT_DATA, ki predstavlja podatke tipkovnice. To nam bo olajšalo delo.

Delo nativne aplikacije se konča s klicem funkcije NtTerminateProcess, ker preprosto ubijemo svoj proces.

Vse kode naše majhne aplikacije:

#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: Lahko brez problema uporabimo v kodi funkcijo DbgBreakPoint() za ustavitev v debuggerju. Vendar bo treba povezati WinDbg z virtualno napravo za jedrno razhlevanje. Navodila, kako to storiti, lahko najdete tukaj ali pa preprosto uporabite VirtualKD.

Kompilacija in sestava

Najlažji način za sestavo nativne aplikacije je uporaba DDK (Driver Development Kit). Potrebujemo prav staro sedmo različico, saj imajo novejše različice nekoliko drugačen pristop in tesno delujejo z Visual Studio. Če uporabimo DDK, potrebujemo za naš projekt le Makefile in 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

Vaš Makefile bo popolnoma enak, na sources pa se ustavimo malo podrobneje. V tem datoteki so navedeni izvorni kodne datoteke (.c), možnosti sestave in druge nastavitve.

  • TARGETNAME – ime izvršne datoteke, ki naj bi nastala na koncu.
  • TARGETTYPE – tip izvršne datoteke, lahko je to gonilnik (.sys), takrat mora biti vrednost polja DRIVER, če je knjižnica (.lib), pa mora biti vrednost LIBRARY. V našem primeru potrebujemo izvršno datoteko (.exe), zato nastavimo vrednost PROGRAM.
  • UMTYPE – možne vrednosti tega polja so: console za konzolno aplikacijo, windows za delo v okenskem načinu. Vendar pa moramo navesti nt, da dobimo nativno aplikacijo.
  • BUFFER_OVERFLOW_CHECKS – preverjanje sklada na prelivanje pomnilnika, žal to ni naš primer, izklopimo.
  • MINWIN_SDK_LIB_PATH – ta vrednost se nanaša na spremenljivko SDK_LIB_PATH, ni vam treba biti zaskrbljeni, da nimate take sistemske spremenljivke, ko bomo zagnali checked build iz DDK, ta spremenljivka bo razglašena in bo kazala na potrebne knjižnice.
  • SOURCES – seznam izvorov vaše aplikacije.
  • INCLUDES – datoteke z glavo, ki so potrebne za sestavo. Tukaj običajno navedemo pot do datotek, ki so priložene DDK, vendar lahko dodatno navedete tudi druge.
  • TARGETLIBS – seznam knjižnic, ki jih je treba povezati.
  • USE_NTDLL – obvezno polje, ki ga je treba nastaviti na vrednost 1. Zelo očitno.
  • USER_C_FLAGS – vsi zastavice, ki jih lahko uporabljate v predprocesorskih direktivah pri pripravi kode aplikacije.

Torej za sestavo moramo zagnati x86 (ali x64) Checked Build, spremeniti delovni imenik na mapo s projektom in izvesti ukaz Build. Rezultat na posnetku zaslona kaže, da smo sestavili eno izvršljivo datoteko.

Windows Native Applications i Acronis Active Restore servis

Te datoteke ne bo enostavno zagnati, sistem opozarja in nas pošilja, da razmislimo o svojem vedenju s sledečo napako:

Windows Native Applications i Acronis Active Restore servis

Kako zagnati nativno aplikacijo?

Ob zagonu autochk zaporedje zagon programov določa vrednost registra:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Upravitelj seje zaporedoma izvaja programe s tega seznama. Izvršljive datoteke upravitelj seje najde v direktoriju system32. Oblika vrednosti registra je naslednja:

autocheck autochk *MyNative

Vrednost mora biti v šestnajstiškem formatu in ne v običajni ASCII, zato bo ključ, predstavljen zgoraj, imel obliko:

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 imena lahko uporabite spletno storitev, na primer, ta.

Windows Native Applications i Acronis Active Restore servis
Torej, da zagnamo nativno aplikacijo, moramo:

  1. Kopirati izvršljivo datoteko v mapo system32
  2. Dodati ključ v register
  3. Ponovno zagnati računalnik

Za lažjo uporabo vam predstavljam pripravljeno skripto za namestitev nativne aplikacije:

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 namestitvi in ponovnem zagonu, še preden se prikaže zaslon za izbiro uporabnikov, bomo videli naslednjo sliko:

Windows Native Applications i Acronis Active Restore servis

Zaključek

Na primeru te majhne aplikacije smo se prepričali, da je zagon aplikacije na ravni Windows Native povsem mogoč. Nadalje bomo s kolegi z Univerze Innopolis nadaljevali z razvojem storitve, ki bo sprožila postopek interakcije z gonilnikom precej prezgodaj v primerjavi s prejšnjo različico našega projekta. S prihodom vmesnika win32 bo smiselno prenesti upravljanje celoviti storitvi, ki je bila že razvita (o tem podrobneje ovde).

V naslednjem članku se bomo dotaknili še ene komponente storitve Active Restore, in sicer gonilnika UEFI. Prijavite se na naš blog, da ne zamudite naslednjega prispevka.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster