Ma ma még folytatjuk a beszámolót arról, hogyan dolgozunk együtt az Innopolis Egyetem srácokkal az Active Restore technológián, hogy a felhasználók a lehető leghamarabb dolgozhassanak a gépükön hiba esetén. A téma a Windows natív alkalmazásokról szól, beleértve azok létrehozásának és indításának sajátosságait. A cikk alatt kicsit bővebben bemutatjuk a projektünket, valamint egy gyakorlati útmutatót arról, hogyan kell natív alkalmazásokat írni.

Az előző bejegyzésekben már írtunk arról, hogy mi az , és hogy az Innopolis diákjai hogyan dolgozzák ki ezt a megoldást. Ma szeretnék a natív alkalmazásokra összpontosítani, amelyek szintjére szeretnénk „elásni” az aktív helyreállítási szolgáltatásunkat. Ha minden jól megy, akkor képesek leszünk: Sokkal korábban elindítani magát a szolgáltatást
- Sokkal korábban kapcsolatba lépni a felhővel, ahol a biztonsági másolat található
- Sokkal korábban megérteni, hogy a rendszer normál indítási vagy helyreállítási módban van-e
- Sokkal kevesebb fájlt előre helyreállítani
- Lehetővé tenni a felhasználó számára, hogy még gyorsabban kezdje el a munkát.
- Mi is az a natív alkalmazás?
Hogy megválaszolhassuk ezt a kérdést, nézzük meg a hívások sorozatát, amelyet a rendszer végrehajt, például ha egy programozó a saját alkalmazásában fájlt próbál létrehozni.
Pavel Yosifovich — Windows Kernel Programming (2019)

A programozó a következő funkciót használja:
CreateFile NtCreateFile (приставка Nt как раз свидетельствует о том, что функция нативная). Данная функция объявлена в заголовочном файле winternl.h и реализована в ntdll.dll. Она производит подготовку к прыжку в ядерное пространство, после чего совершает системный вызов для создания файла. В данном случае получается, что Kernel32 – всего лишь обертка для Ntdll. Одна из причин для чего это сделано, Microsoft таким образом имеет возможность изменять функции нативного мира, но при этом не трогать стандартные интерфейсы. Microsoft не рекомендует напрямую вызывать нативные функции и не документирует большую часть из них. Кстати, недокументированные функции можно найти .
A natív alkalmazások fő előnye abban rejlik, hogy az ntdll sokkal korábban kerül betöltésre a rendszerbe, mint a kernel32. Ez logikus, hiszen a kernel32 működéséhez szüksége van az ntdll-re. Ennek következtében azok az alkalmazások, amelyek natív funkciókat használnak, lényegesen korábban elindulhatnak.
Így a Windows Native Applications olyan programokat jelentenek, amelyek a Windows betöltésének korai szakaszában képesek elindulni. Ezek kizárólag az ntdll funkcióit használják. Egy példa egy ilyen alkalmazásra: amely végrehajtja a lemezhibák ellenőrzésére még az alapvető szolgáltatások elindítása előtt. pontosan ilyen szinten szeretnénk látni az Active Restore-t.
Mire lesz szükségünk?
- (Driver Development Kit), ma már WDK 7 (Windows Driver Kit) néven is ismert.
- Virtuális gép (például Windows 7 x64)
- Nem kötelező, de segíthetnek a fejléc fájlok, amelyeket letölthetünk.
Mit is kell a kódban?
Gyakoroljuk egy kicsit, és példaként írjunk egy egyszerű alkalmazást, amely:
- Megjelenít egy üzenetet a képernyőn.
- Egy kis memóriát allokál.
- Várja a billentyűzet bemeneti adatát.
- Frees the allocated memory
A natív alkalmazások belépési pontja nem a main vagy winmain, hanem az NtProcessStartup, mivel valójában közvetlenül indítunk új folyamatot a rendszerben.
Kezdjük az üzenet megjelenítésével a képernyőn. Ehhez van egy natív funkciónk: , amely egy UNICODE_STRING struktúra objektumra mutató mutatót fogad el argumentumként. Ezt az RtlInitUnicodeString segít inicializálni. Ennek eredményeként, a szöveg képernyőre írásához egy ilyen egyszerű funkciót írhatunk:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Mivel csak az ntdll funkciók állnak rendelkezésünkre, és más könyvtárak a memóriában egyszerűen még nincsenek, feltétlenül problémáink lesznek a memória allokálásával. A new operátor még nem létezik (mivel ez a C++ túl magas szintű világából származik), és a malloc funkció sem érhető el (ezekhez C futási könyvtárakra van szükség). Természetesen használhatjuk csak a veremben. De ha dinamikusan kell memóriát allokálnunk, ezt a halmazban (azaz heapben) kell tennünk. Ezért létre kell hoznunk egy halmazt, és abból kell memóriát foglalnunk, amikor szükségünk van rá.
Ehhez a feladathoz megfelelő a . Ezután az RtlAllocateHeap és RtlFreeHeap segítségével fogunk memóriát foglalni és felszabadítani, amikor szükségünk lesz rá.
PVOID memória = NULL;
PVOID puffer = NULL;
ULONG pufferméret = 42;
// halmaz létrehozása a memória későbbi lefoglalásához
memória = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// puffer lefoglalása a pufferméret nagyságára
puffer = RtlAllocateHeap(
memória,
HEAP_ZERO_MEMORY,
pufferméret
);
// puffer felszabadítása (valójában nem szükséges, mivel a halmazt a következő lépésben megsemmisítjük)
RtlFreeHeap(memória, 0, puffer);
RtlDestroyHeap(memória);Lépjünk tovább, és várjunk a billentyűzet bemenetére.
// 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;
}
}Minden, amire szükségünk van, az az, hogy használjuk a nyitott eszközön, és várjunk, amíg a billentyűzet vissza nem ad valamelyik megnyomott billentyűt. Ha megnyomják az ESC billentyűt, folytatjuk a munkát. Az eszköz megnyitásához fel kell hívnunk a NtCreateFile függvényt (a DeviceKeyboardClass0-t kell megnyitnunk). Emellett fel fogjuk hívni , hogy inicializáljuk a várakozási objektumot. Szándékosan deklarálni fogjuk a KEYBOARD_INPUT_DATA struktúrát, amely a billentyűzet adatait képviseli. Ez megkönnyíti a munkánkat.
A natív alkalmazás működése a , mert egyszerűen megöljük a saját folyamatunkat.
A kis alkalmazásunk teljes 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: Nyugodtan használhatjuk a DbgBreakPoint() függvényt a kódunkban, hogy megálljunk a hibakeresőben. Igaz, hogy a WinDbg-t csatlakoztatni kell a virtuális géphez a kernelhibakereséshez. Az utasítást egyszerűen megtalálhatjuk vagy egyszerűen használhatjuk a .
Fordítás és összeállítás
A legegyszerűbb módja a natív alkalmazás összeállításának az, hogy használjuk a (Driver Development Kit). Nekünk pontosan a régi hetedik verzió szükséges, mivel a későbbi verziók kissé eltérő megközelítéssel bírnak és szorosan együttműködnek a Visual Studioval. Ha DDK-t használunk, akkor a projektünkhöz mindössze Makefile-ra és forrásokra van szükség.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defforrások:
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 = 1A Makefile-ed pontosan ilyen lesz, a forrásoknál pedig álljunk meg egy kicsit. Ebben a fájlban adjuk meg a programunk forrásait (a .c fájlokat), a fordítási opciókat és egyéb paramétereket.
- TARGETNAME – a végrehajtható fájl neve, ami végül létrejön.
- TARGETTYPE – a végrehajtható fájl típusa, ez lehet driverek (.sys), akkor a mező értéke DRIVER kell legyen, ha a könyvtár (.lib), akkor a LIBRARY érték. Ebben az esetben végrehajtható fájlra (.exe) van szükségünk, ezért PROGRAM értéket állítunk be.
- UMTYPE – a mezők lehetséges értékei: console a konzolos alkalmazáshoz, windows az ablakos üzemmódhoz. Nekünk azonban nt-t kell megadnunk, hogy natív alkalmazást kapjunk.
- BUFFER_OVERFLOW_CHECKS – a verem ellenőrzése a puffer túlcsordulás szempontjából, sajnos nem a mi esetünk, kapcsold ki.
- MINWIN_SDK_LIB_PATH – ez az érték az SDK_LIB_PATH változóra mutat, ne aggódj, ha ilyen rendszer változót nem deklaráltál, a DDK-ból indított checked build során ez a változó létrejön és az szükséges könyvtárakra fog mutatni.
- SOURCES – a programod forrásainak listája.
- INCLUDES – a fejlécek, amelyek szükségesek a fordításhoz. Itt általában a DDK-hoz mellékelt fájlok elérési útját adják meg, de további fájlokat is megadhatsz.
- TARGETLIBS – a linkelendő könyvtárak listája.
- USE_NTDLL – kötelező mező, amelyet 1 értékre kell állítani. Nyilvánvaló okokból.
- USER_C_FLAGS – bármilyen zászló, amelyet használhatsz a pre-processzor direktívákban az alkalmazás kódjának előkészítésekor.
Tehát a fordításhoz el kell indítanunk az x86 (vagy x64) Checked Build-et, meg kell változtatnunk a munkakönyvtárat a projekt mappájára, és végre kell hajtanunk a Build parancsot. Az eredmény a képernyőképen azt mutatja, hogy egy végrehajtható fájlunk készült el.

Ezt a fájlt nem lehet egyszerűen csak elindítani, a rendszer hibát jelez és arra kényszerít, hogy a következő hibaüzenetet átgondold:

Hogyan indítsunk natív alkalmazást?
A program indításakor az autochk megfelelő sorrendjét a következő regisztri kulcs értéke határozza meg:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteA session manager sorban hajtja végre az ebben a listában szereplő programokat. Az végrehajtható fájlokat a session manager a system32 könyvtárban keresi. A regisztri kulcs értékének formátuma a következő:
autocheck autochk *MyNativeAz értéknek hexadecimális formátumban kell lennie, nem pedig a megszokott ASCII formában, így a fent bemutatott kulcs a következő formátumot fogja kapni:
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,00A név konvertálásához használhatsz online szolgáltatást, például, .

Tehát, ahhoz hogy natív alkalmazást indítsunk, a következőkre van szükség:
- Másold a végrehajtható fájlt a system32 mappába
- Adj hozzá egy kulcsot a regiszterhez
- Indítsd újra a gépet
A kényelem érdekében itt van egy kész script 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
pauseadd.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,00A telepítés és újraindítás után, még a felhasználói választó képernyő megjelenése előtt, a következő képet kapjuk:

Result
Egy ilyen kis alkalmazás példáján láttuk, hogy Windows Native szinten alkalmazást indítani teljesen lehetséges. Továbbra is a Kazan-i Innopolis Egyetem fiúival fogunk dolgozni egy olyan szolgáltatás kiépítésén, amely a vezérlőmeghajtóval való interakciót jóval korábban indítja el, mint a projektünk korábbi verziója. A win32 felület megjelenésével logikus lesz átadni a vezérlést egy teljes értékű szolgáltatásnak, amely már kidolgozásra került (erről részletesebben ).
A következő cikkben a szolgáltatás egyik másik elemével, az UEFI meghajtóval foglalkozunk. Iratkozz fel blogunkra, hogy ne maradj le a következő bejegyzésről.
Forrás: habr.com
