Windowsi algrakendused ja Acronis Active Restore'i teenus

Täna jätkame lugu sellest, kuidas me koos Innopolise ülikooli kuttidega arendame Active Restore tehnoloogiat, et kasutaja saaks pärast riket võimalikult kiiresti oma masinaga tööle asuda. Räägime Windowsi algrakendustest, sealhulgas nende loomise ja käivitamise funktsioonidest. Lõike all on veidi meie projektist, samuti praktiline juhend, kuidas kirjutada kohalikke rakendusi.

Windowsi algrakendused ja Acronis Active Restore'i teenus

Eelmistes postitustes oleme juba rääkinud, mis see on Aktiivne taastamine, ja kuidas Innopolise õpilased arenevad teenus. Täna tahan keskenduda natiivsetele rakendustele, mille tasemele tahame oma aktiivse taastamise teenuse "matta". Kui kõik õnnestub, saame:

  • Käivitage teenus ise palju varem
  • Võtke palju varem ühendust pilvega, kus varukoopia asub
  • Palju varem, et mõista, mis režiimis süsteem on - tavaline alglaadimine või taastamine
  • Palju vähem faile, mida eelnevalt taastada
  • Võimaldab kasutajal veelgi kiiremini alustada.

Mis on ikkagi omarakendus?

Sellele küsimusele vastamiseks vaatame kõnede jada, mida süsteem teeb, näiteks kui programmeerija oma rakenduses proovib faili luua.

Windowsi algrakendused ja Acronis Active Restore'i teenus
Pavel Yosifovich – Windowsi tuuma programmeerimine (2019)

Programmeerija kasutab funktsiooni Loo fail, mis on deklareeritud päisefailis fileapi.h ja rakendatud failis Kernel32.dll. See funktsioon ise aga faili ei loo, vaid kontrollib ainult sisendargumente ja kutsub funktsiooni välja NtCreateFile (eesliide Nt näitab lihtsalt, et funktsioon on loomulik). See funktsioon on deklareeritud päisefailis winternl.h ja rakendatud failis ntdll.dll. See valmistub hüppama tuumaruumi, misjärel teeb süsteemikutse faili loomiseks. Sel juhul selgub, et Kernel32 on lihtsalt Ntdll-i ümbris. Üks põhjusi, miks seda tehti, on see, et Microsoftil on seega võimalus muuta põhimaailma funktsioone, kuid mitte puudutada standardseid liideseid. Microsoft ei soovita algfunktsioone otse kutsuda ega dokumenteeri enamikku neist. Muide, dokumenteerimata funktsioone võib leida siin.

Natiivsete rakenduste peamine eelis on see, et ntdll laaditakse süsteemi palju varem kui kernel32. See on loogiline, sest kernel32 vajab töötamiseks ntdll-i. Tänu sellele saavad algfunktsioone kasutavad rakendused tööle hakata palju varem.

Seega on Windowsi algrakendused programmid, mis võivad käivituda Windowsi alglaadimisel. Nad kasutavad AINULT ntdll-i funktsioone. Sellise rakenduse näide: autochk kes esineb chkdisk utiliit ketta vigade kontrollimiseks enne põhiteenuste käivitamist. Just sellisel tasemel tahame, et meie aktiivne taastamine oleks.

Mida me vajame?

  • DDK (Driver Development Kit), nüüd tuntud ka kui WDK 7 (Windows Driver Kit).
  • Virtuaalne masin (näiteks Windows 7 x64)
  • Pole vajalik, kuid päisefailid, mida saab alla laadida, võivad aidata siin

Mis on koodis?

Harjutame veidi ja kirjutame näiteks väikese rakenduse, mis:

  1. Kuvab ekraanil teate
  2. Eraldab natuke mälu
  3. Ootab klaviatuuri sisestust
  4. Vabastab kasutatud mälu

Natiivsetes rakendustes ei ole sisenemispunkt main või winmain, vaid funktsioon NtProcessStartup, kuna tegelikult käivitame süsteemis otse uued protsessid.

Alustuseks kuvame ekraanil teate. Selleks on meil natiivne funktsioon NtDisplayString, mis võtab argumendina kursorit struktuuriobjektile UNICODE_STRING. RtlInitUnicodeString aitab meil seda lähtestada. Selle tulemusena saame teksti ekraanil kuvamiseks kirjutada selle väikese funktsiooni:

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

Kuna meie käsutuses on ainult ntdll-i funktsioonid ja muid teeke mälus veel lihtsalt pole, on meil kindlasti probleeme mälu eraldamisega. Uut operaatorit pole veel olemas (kuna see pärineb liiga kõrgetasemelisest C++ maailmast) ja puudub malloc funktsioon (see nõuab käitusaegseid C teeke). Loomulikult saate kasutada ainult virna. Kuid kui meil on vaja mälu dünaamiliselt eraldada, peame seda tegema kuhjas (st kuhjas). Nii et loome endale hunniku ja võtame sellest mälestust alati, kui seda vajame.

Funktsioon sobib selle ülesande jaoks RtlCreateHeap. Järgmiseks, kasutades RtlAllocateHeapi ja RtlFreeHeapi, hõivame ja vabastame mälu, kui seda vajame.

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

Liigume edasi klaviatuurisisendi ootamise juurde.

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

Kõik, mida vajame, on kasutada NtReadFile avatud seadmes ja oodake, kuni klaviatuur meile vajutuse tagastab. Kui vajutada ESC-klahvi, jätkame tööd. Seadme avamiseks peame kutsuma funktsiooni NtCreateFile (peame avama DeviceKeyboardClass0). Helistame ka NtCreateEventooteobjekti lähtestamiseks. Me deklareerime ise struktuuri KEYBOARD_INPUT_DATA, mis esindab klaviatuuri andmeid. See muudab meie töö lihtsamaks.

Omarakendus lõpeb funktsioonikutsega NtTerminateProcesssest me lihtsalt tapame omaenda protsessi.

Kogu meie väikese rakenduse kood:

#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: Saame hõlpsasti kasutada oma koodis funktsiooni DbgBreakPoint(), et see siluris peatada. Tõsi, kerneli silumiseks peate ühendama WinDbg virtuaalse masinaga. Juhised, kuidas seda teha, leiate siin või lihtsalt kasutada VirtualKD.

Koostamine ja kokkupanek

Lihtsaim viis omarakenduse loomiseks on kasutada DDK (Draiveri arenduskomplekt). Vajame iidset seitsmendat versiooni, kuna hilisematel versioonidel on pisut erinev lähenemine ja need töötavad tihedalt Visual Studioga. Kui kasutame DDK-d, vajab meie projekt ainult Makefile'i ja allikaid.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

allikad:

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

Teie Makefile on täpselt sama, kuid vaatame allikaid veidi üksikasjalikumalt. See fail määrab teie programmi allikad (.c-failid), järgu valikud ja muud parameetrid.

  • TARGETNAME – käivitatava faili nimi, mis tuleks lõpuks luua.
  • TARGETTYPE – käivitatava faili tüüp, see võib olla draiver (.sys), siis välja väärtus peaks olema DRIVER, kui teek (.lib), siis väärtuseks LIBRARY. Meie puhul vajame käivitatavat faili (.exe), seega määrame väärtuseks PROGRAM.
  • UMTYPE – selle välja võimalikud väärtused: konsool konsoolirakenduse jaoks, aknad aknarežiimis töötamiseks. Kuid natiivse rakenduse saamiseks peame määrama nt.
  • BUFFER_OVERFLOW_CHECKS – virna kontrollimine puhvri ületäitumise suhtes, kahjuks mitte meie puhul, lülitame selle välja.
  • MINWIN_SDK_LIB_PATH – see väärtus viitab muutujale SDK_LIB_PATH, ärge muretsege, et teil pole sellist süsteemimuutujat deklareeritud, kui käivitame DDK-st kontrollitud ehituse, deklareeritakse see muutuja ja see osutab vajalikele teekidele.
  • ALLIKAD – teie programmi allikate loend.
  • KAASA – kokkupanekuks vajalikud päisefailid. Siin näitavad nad tavaliselt DDK-ga kaasas olevate failide teed, kuid saate lisaks määrata ka muid.
  • TARGETLIBS – linkimist vajavate raamatukogude loend.
  • USE_NTDLL on kohustuslik väli, mis tuleb arusaadavatel põhjustel määrata väärtusele 1.
  • USER_C_FLAGS – kõik lipud, mida saate rakenduse koodi ettevalmistamisel eeltöötlejate käskkirjades kasutada.

Nii et ehitamiseks peame käivitama x86 (või x64) Checked Build, muutma töökataloogi projekti kaustaks ja käivitama käsu Build. Ekraanipildi tulemus näitab, et meil on üks käivitatav fail.

Windowsi algrakendused ja Acronis Active Restore'i teenus

Seda faili ei saa nii lihtsalt käivitada, süsteem kirub ja saadab meid selle käitumise üle järele mõtlema järgmise veaga:

Windowsi algrakendused ja Acronis Active Restore'i teenus

Kuidas käivitada omarakendus?

Kui autochk käivitub, määrab programmide käivitusjada registrivõtme väärtuse järgi:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Seansihaldur käivitab selle loendi programme ükshaaval. Seansihaldur otsib käivitatavad failid ise system32 kataloogist. Registrivõtme väärtuse vorming on järgmine:

autocheck autochk *MyNative

Väärtus peab olema kuueteistkümnendsüsteemis, mitte tavalises ASCII-vormingus, nii et ülaltoodud võti on vormingus:

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

Pealkirja teisendamiseks võite kasutada võrguteenust, näiteks see.

Windowsi algrakendused ja Acronis Active Restore'i teenus
Selgub, et omarakenduse käivitamiseks vajame:

  1. Kopeerige käivitatav fail kausta system32
  2. Lisage registrisse võti
  3. Taaskäivitage masin

Mugavuse huvides on siin valmis skript omarakenduse installimiseks:

install.bat

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

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

Pärast installimist ja taaskäivitamist, isegi enne kasutaja valimise ekraani ilmumist, saame järgmise pildi:

Windowsi algrakendused ja Acronis Active Restore'i teenus

Summaarne

Sellise väikese rakenduse näitel veendusime, et rakendust on täiesti võimalik käivitada Windows Native tasemel. Järgmisena jätkame Innopolise ülikooli poistega teenuse ehitamist, mis käivitab juhiga suhtlemise protsessi palju varem kui meie projekti eelmises versioonis. Ja win32 kesta tulekuga oleks loogiline viia juhtimine üle täisväärtuslikule teenusele, mis on juba välja töötatud (sellest lähemalt siin).

Järgmises artiklis käsitleme veel ühte Active Restore teenuse komponenti, nimelt UEFI draiverit. Liituge meie ajaveebiga, et te ei jääks järgmisest postitusest ilma.

Allikas: www.habr.com

Lisa kommentaar