Windows Native Applications és Acronis Active Restore szolgáltatás

Ma folytatjuk a történetet arról, hogy az Innopolis Egyetem srácaival együtt fejlesztjük az Active Restore technológiát, hogy a felhasználó a lehető leghamarabb elkezdhesse dolgozni a gépén egy meghibásodás után. Beszélni fogunk a natív Windows-alkalmazásokról, beleértve azok létrehozásának és elindításának jellemzőit. A vágás alatt egy kicsit a projektünkről, valamint egy gyakorlati útmutató a natív alkalmazások írásához.

Windows Native Applications és Acronis Active Restore szolgáltatás

Korábbi bejegyzéseinkben már beszéltünk arról, hogy mi ez Aktív visszaállítás, és hogyan fejlődnek az innopolisi diákok szolgáltatás. Ma a natív alkalmazásokra szeretnék koncentrálni, amelyek szintjére szeretnénk „temetni” az aktív helyreállítási szolgáltatásunkat. Ha minden sikerül, akkor képesek leszünk:

  • Indítsa el magát a szolgáltatást sokkal korábban
  • Sokkal korábban lépjen kapcsolatba a felhővel, ahol a biztonsági mentés található
  • Sokkal korábban megérteni, hogy a rendszer milyen módban van - normál rendszerindítás vagy helyreállítás
  • Sokkal kevesebb fájlt kell előre visszaállítani
  • Lehetővé teszi a felhasználó számára, hogy még gyorsabban kezdje el.

Egyáltalán mi az a natív alkalmazás?

A kérdés megválaszolásához nézzük meg a rendszer által végrehajtott hívások sorrendjét, ha például egy programozó az alkalmazásában megpróbál fájlt létrehozni.

Windows Native Applications és Acronis Active Restore szolgáltatás
Pavel Yosifovich – Windows kernel programozás (2019)

A programozó használja a függvényt CreateFile, amely a fileapi.h fejlécfájlban van deklarálva és a Kernel32.dll-ben van megvalósítva. Ez a függvény azonban maga nem hozza létre a fájlt, csak ellenőrzi a bemeneti argumentumokat és meghívja a függvényt NtCreateFile (az Nt előtag csak azt jelzi, hogy a függvény natív). Ez a függvény a winternl.h fejlécfájlban van deklarálva, és az ntdll.dll fájlban van megvalósítva. Felkészül a nukleáris térbe való ugrásra, majd rendszerhívást indít, hogy létrehozzon egy fájlt. Ebben az esetben kiderül, hogy a Kernel32 csak az Ntdll csomagolóanyaga. Ennek egyik oka az, hogy a Microsoft így képes megváltoztatni a natív világ funkcióit, de nem érinti a szabványos felületeket. A Microsoft nem javasolja a natív függvények közvetlen meghívását, és legtöbbjüket nem dokumentálja. Egyébként nem dokumentált függvények is megtalálhatók itt.

A natív alkalmazások fő előnye, hogy az ntdll sokkal korábban töltődik be a rendszerbe, mint a kernel32. Ez logikus, mert a kernel32 működéséhez ntdll szükséges. Ennek eredményeként a natív funkciókat használó alkalmazások sokkal korábban elkezdhetnek működni.

Így a Windows natív alkalmazások olyan programok, amelyek a Windows rendszerindításakor korán elindulhatnak. CSAK az ntdll függvényeit használják. Példa egy ilyen alkalmazásra: autochk aki előadja chkdisk segédprogram hogy a fő szolgáltatások elindítása előtt ellenőrizze a lemezen található hibákat. Pontosan ezt a szintet szeretnénk elérni az Active Restore-nkkal.

Mire van szükségünk?

  • DDK (Driver Development Kit), ma WDK 7 (Windows Driver Kit) néven is ismert.
  • Virtuális gép (például Windows 7 x64)
  • Nem szükséges, de a letölthető fejlécfájlok segíthetnek itt

Mi van a kódban?

Gyakoroljunk egy kicsit, és például írjunk egy kis alkalmazást, amely:

  1. Üzenetet jelenít meg a képernyőn
  2. Lefoglal egy kis memóriát
  3. Vár a billentyűzet bevitelére
  4. Felszabadítja a használt memóriát

A natív alkalmazásokban a belépési pont nem a main vagy a winmain, hanem az NtProcessStartup függvény, mivel tulajdonképpen közvetlenül indítunk el új folyamatokat a rendszerben.

Kezdjük egy üzenet megjelenítésével a képernyőn. Ehhez van egy natív funkciónk NtDisplayString, amely argumentumként egy mutatót vesz egy UNICODE_STRING szerkezeti objektumra. Az RtlInitUnicodeString segít inicializálni. Ennek eredményeként a szöveg képernyőn való megjelenítéséhez ezt a kis függvényt írhatjuk:

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

Mivel csak az ntdll-ből származó függvények állnak rendelkezésünkre, más könyvtárak pedig egyszerűen még nincsenek a memóriában, biztosan lesz gondunk a memóriafoglalással. Az új operátor még nem létezik (mert a C++ túl magas szintű világából származik), és nincs malloc függvény sem (runtime C könyvtárakat igényel). Természetesen csak verem használható. De ha dinamikusan kell lefoglalnunk a memóriát, akkor azt a kupacon (vagyis kupon) kell megtennünk. Tehát hozzunk létre egy kupacot magunknak, és vegyünk belőle emléket, amikor csak szükségünk van rá.

A funkció alkalmas erre a feladatra RtlCreateHeap. Ezután az RtlAllocateHeap és az RtlFreeHeap használatával lefoglaljuk és felszabadítjuk a memóriát, amikor szükségünk van rá.

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

Térjünk át a billentyűzet bevitelére várva.

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

Csak használnunk kell NtReadFile nyitott eszközön, és várja meg, amíg a billentyűzet bármilyen megnyomást visszaad nekünk. Ha megnyomja az ESC billentyűt, folytatjuk a munkát. Az eszköz megnyitásához meg kell hívnunk az NtCreateFile függvényt (meg kell nyitnunk a DeviceKeyboardClass0-t). Hívni is fogunk NtCreateEventa várakozás objektum inicializálásához. Mi magunk deklaráljuk a KEYBOARD_INPUT_DATA struktúrát, amely a billentyűzet adatait reprezentálja. Ez megkönnyíti a munkánkat.

A natív alkalmazás függvényhívással zárul NtTerminateProcessmert egyszerűen megöljük a saját folyamatunkat.

Kis alkalmazásunk összes kódja:

#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: A kódunkban található DbgBreakPoint() függvény segítségével egyszerűen leállíthatjuk a hibakeresőben. Igaz, a kernel hibakereséséhez a WinDbg-t egy virtuális géphez kell csatlakoztatnia. Ennek végrehajtására vonatkozó utasítások megtalálhatók itt vagy csak használja VirtualKD.

Összeállítás és összeállítás

A natív alkalmazás létrehozásának legegyszerűbb módja a használata DDK (Driver Development Kit). Szükségünk van az ősi hetedik verzióra, mivel a későbbi verziók kissé eltérő megközelítést alkalmaznak, és szorosan együttműködnek a Visual Studióval. Ha a DDK-t használjuk, akkor a projektünknek csak Makefile-ra és forrásokra van szüksége.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

Forrás:

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

A Makefile pontosan ugyanaz lesz, de nézzük meg kicsit részletesebben a forrásokat. Ez a fájl határozza meg a program forrásait (.c fájlok), összeállítási beállításokat és egyéb paramétereket.

  • TARGETNAME – a végrehajtható fájl neve, amelyet a végén elő kell állítani.
  • TARGETTYPE – a végrehajtható fájl típusa, lehet driver (.sys), akkor a mező értéke DRIVER, ha könyvtár (.lib), akkor LIBRARY. Esetünkben szükségünk van egy futtatható fájlra (.exe), ezért az értéket PROGRAM-ra állítjuk.
  • UMTYPE – a mező lehetséges értékei: konzol konzolalkalmazáshoz, ablakok ablakos módban történő munkavégzéshez. De meg kell adnunk az nt-t, hogy natív alkalmazást kapjunk.
  • BUFFER_OVERFLOW_CHECKS – puffer túlcsordulás ellenőrzése a veremben, sajnos nem esetünkben, kikapcsoljuk.
  • MINWIN_SDK_LIB_PATH – ez az érték az SDK_LIB_PATH változóra vonatkozik, ne aggódj, hogy nincs ilyen rendszerváltozód deklarálva, amikor az ellenőrzött buildet futtatjuk a DDK-ból, ez a változó deklarálva lesz, és a szükséges könyvtárakra mutat.
  • FORRÁSOK – a program forrásainak listája.
  • INCLUDES – az összeállításhoz szükséges fejlécfájlok. Itt általában jelzik a DDK-val szállított fájlok elérési útját, de megadhat másokat is.
  • TARGETLIBS – a csatolandó könyvtárak listája.
  • A USE_NTDLL egy kötelező mező, amelyet nyilvánvaló okokból 1-re kell állítani.
  • USER_C_FLAGS – minden olyan jelző, amelyet az előfeldolgozó direktívákban használhat az alkalmazáskód előkészítésekor.

Tehát a felépítéshez le kell futtatnunk az x86 (vagy x64) Checked Build programot, módosítanunk kell a munkakönyvtárat a projekt mappára, és futtassa a Build parancsot. A képernyőképen látható eredmény azt mutatja, hogy van egy végrehajtható fájlunk.

Windows Native Applications és Acronis Active Restore szolgáltatás

Ezt a fájlt nem lehet olyan egyszerűen elindítani, a rendszer átkozódik, és a következő hibával küldi el, hogy gondoljuk végig a viselkedését:

Windows Native Applications és Acronis Active Restore szolgáltatás

Hogyan indítsunk el egy natív alkalmazást?

Amikor az autochk elindul, a programok indítási sorrendjét a rendszerleíró kulcs értéke határozza meg:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

A munkamenet-kezelő egyenként hajtja végre a programokat ebből a listából. A munkamenet-kezelő magukat a végrehajtható fájlokat keresi a system32 könyvtárban. A rendszerleíró kulcs értékének formátuma a következő:

autocheck autochk *MyNative

Az értéknek hexadecimális formátumban kell lennie, nem a szokásos ASCII-ben, ezért a fent látható kulcs a következő formátumban lesz:

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

A cím konvertálásához használhat egy online szolgáltatást, például ezt.

Windows Native Applications és Acronis Active Restore szolgáltatás
Kiderült, hogy egy natív alkalmazás elindításához szükségünk van:

  1. Másolja a végrehajtható fájlt a system32 mappába
  2. Adjon hozzá egy kulcsot a rendszerleíró adatbázishoz
  3. Indítsa újra a gépet

A kényelem kedvéért itt van egy kész szkript a natív alkalmazás telepítéséhez:

install.bat

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

Telepítés és újraindítás után, még a felhasználóválasztó képernyő megjelenése előtt a következő képet kapjuk:

Windows Native Applications és Acronis Active Restore szolgáltatás

Teljes

Egy ilyen kis alkalmazás példáján megbizonyosodtunk arról, hogy az alkalmazás Windows Native szinten is futtatható. Ezután az Innopolis Egyetem srácaival folytatjuk egy olyan szolgáltatás felépítését, amely sokkal korábban elindítja az interakciót a sofőrrel, mint a projektünk előző verziójában. A win32 shell megjelenésével pedig logikus lenne az irányítást egy már kifejlesztett teljes körű szolgáltatásra átvinni (erről bővebben itt).

A következő cikkben az Active Restore szolgáltatás egy másik összetevőjét érintjük, nevezetesen az UEFI illesztőprogramot. Iratkozz fel blogunkra, hogy ne maradj le a következő bejegyzésről.

Forrás: will.com

Hozzászólás