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.
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.
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:
Üzenetet jelenít meg a képernyőn
Lefoglal egy kis memóriát
Vár a billentyűzet bevitelére
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);
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.
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:
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:
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:
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:
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.