Dnes pokračujeme v našem vyprávění o tom, jak společně s kluky z Univerzity Innopolis vyvíjíme technologii Active Restore, aby uživateli umožnili začít pracovat na své mašině co nejdříve po selhání. Budeme se zabývat nativními aplikacemi Windows, včetně zvláštností jejich vytváření a spouštění. Pod katem – něco o našem projektu a také praktický průvodce, jak psát nativní aplikace.

V předchozích příspěvcích jsme již mluvili o tom, co je to , a jak studenti z Innopolis vyvíjejí . Dnes se chci zaměřit na nativní aplikace, na úroveň, na kterou chceme "zahrabat" naši službu aktivního obnovení. Pokud to všechno vyjde, budeme moci:
- Spustit službu mnohem dříve
- Mnohem dříve se spojit s cloudem, kde je záloha
- Mnohem dříve pochopit, v jakém režimu se systém nachází – normální start nebo obnova
- Mnohem méně souborů předem obnovit
- Umožnit uživateli začít pracovat ještě rychleji.
Co vlastně je nativní aplikace?
Abychom na tuto otázku odpověděli, podívejme se na sekvenci volání, která se systém provádí, například když programátor ve své aplikaci zkouší vytvořit soubor.

Pavel Yosifovich – Windows Kernel Programming (2019)
Programátor používá funkci , která je deklarována v hlavičkovém souboru fileapi.h a implementována v Kernel32.dll. Nicméně tato funkce se samotným vytvářením souboru nezabývá, kontroluje pouze argumenty na vstupu a volá funkci (předpona Nt jasně naznačuje, že funkce je nativní). Tato funkce je deklarována v hlavičkovém souboru winternl.h a implementována v ntdll.dll. Provádí přípravu pro skok do jádrového prostoru, poté provádí systémové volání pro vytvoření souboru. V tomto případě se ukazuje, že Kernel32 je jen obálka pro Ntdll. Jeden z důvodů, proč je to takto uděláno, je, že Microsoft má možnost měnit funkce nativního světa, ale přitom se nedotýká standardních rozhraní. Microsoft nedoporučuje přímo volat nativní funkce a většinu z nich nedokumentuje. Mimochodem, nedokumentované funkce lze nalézt .
Hlavní výhodou nativních aplikací je, že ntdll se načítá do systému výrazně dříve než kernel32. To dává smysl, protože kernel32 vyžaduje ntdll k provozu. V důsledku toho aplikace využívající nativní funkce mohou začít fungovat mnohem dříve.
Windows Native Applications jsou tedy programy, které mohou být spuštěny v rané fázi načítání Windows. Používají POUZE funkce z ntdll. Příklad takové aplikace: který provádí pro kontrolu disku na chyby ještě před spuštěním hlavních služeb. Právě na takové úrovni chceme vidět náš Active Restore.
Co budeme potřebovat?
- (Driver Development Kit), nyní také známý pod názvem WDK 7 (Windows Driver Kit).
- Virtuální stroj (například Windows 7 x64)
- Není nutné, ale mohou pomoci hlavičkové soubory, které si lze stáhnout
Co tedy v kódu?
Pojďme si trochu procvičit a jako příklad napsat malou aplikaci, která:
- Vypíše zprávu na obrazovku
- Alokuje trochu paměti
- Čeká na vstup z klávesnice
- Uvolní zabranou paměť
V nativních aplikacích je vstupním bodem nikoli main nebo winmain, ale funkce NtProcessStartup, protože ve skutečnosti přímo spouštíme nové procesy v systému.
Začneme výstupem zprávy na obrazovku. K tomu máme nativní funkci , která jako argument přijímá ukazatel na objekt struktury UNICODE_STRING. Inicializovat ho nám pomůže RtlInitUnicodeString. V důsledku toho můžeme napsat tuto malou funkci pro zobrazení textu na obrazovce:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Jelikož máme k dispozici pouze funkce z ntdll a další knihovny v paměti zatím nejsou, určitě narazíme na problémy s alokací paměti. Operátor new ještě neexistuje (protože pochází z příliš vysoceúrovňového světa C++), také neexistuje funkce malloc (pro tu jsou potřeba knihovny runtime C). Můžeme samozřejmě používat pouze zásobník. Ale pokud potřebujeme dynamicky alokovat paměť, musíme to udělat na haldě (tj. heap). Takže si vytvoříme haldu a budeme čerpat paměť, kdykoli ji budeme potřebovat.
Pro tuto úlohu se hodí funkce . Dále budeme pomocí RtlAllocateHeap a RtlFreeHeap alokovat a uvolňovat paměť, kdykoli to budeme potřebovat.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// vytvořit haldu pro pozdější alokaci paměti
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// alokovat buffer o velikosti bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// uvolnit buffer (ve skutečnosti není potřeba, protože haldu zničíme v dalším kroku)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Přejdeme k čekání na vstup z klávesnice.
// 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;
}
}Všechno, co potřebujeme, je použít na otevřeném zařízení a čekat, až nám klávesnice vrátí nějaký stisk klávesy. V případě stisknutí klávesy ESC budeme pokračovat. K otevření zařízení musíme zavolat funkci NtCreateFile (budeme potřebovat otevřít DeviceKeyboardClass0). Také zavoláme , abychom inicializovali objekt pro čekání. Sami si deklarujeme strukturu KEYBOARD_INPUT_DATA, která představuje data klávesnice. To nám usnadní práci.
Práce nativní aplikace končí voláním funkce , protože jednoduše zabíjíme svůj vlastní proces.
Celý kód naší malé aplikace:
#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: Můžeme snadno použít v kódu funkci DbgBreakPoint() pro zastavení v debugu. Pravda, bude potřeba připojit WinDbg k virtuální mašině pro kernel debugging. Návod, jak to provést, můžete najít nebo jednoduše použít .
Kompilace a sestavení
Nejjednodušší způsob, jak sestavit nativní aplikaci, je použít (Driver Development Kit). Potřebujeme opravdu starou sedmou verzi, protože novější verze mají poněkud jiný přístup a úzce spolupracují s Visual Studio. Pokud však použijeme DDK, potřebujeme pro náš projekt jen Makefile a sources.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defsources:
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 = 1Váš Makefile bude přesně takový samý, u sources se zastavíme trochu podrobněji. V tomto souboru jsou uvedeny zdrojové kódy vašeho programu (soubory .c), možnosti sestavení a další parametry.
- TARGETNAME – jméno spustitelného souboru, který by měl vzniknout na konci.
- TARGETTYPE – typ spustitelného souboru, to může být ovladač (.sys), pak by hodnota pole měla být DRIVER, pokud je to knihovna (.lib), pak by hodnota měla být LIBRARY. V našem případě potřebujeme spustitelný soubor (.exe), proto nastavíme hodnotu PROGRAM.
- UMTYPE – možné hodnoty tohoto pole jsou: console pro konzolovou aplikaci, windows pro provoz v okně. Musíme však uvést nt, abychom získali nativní aplikaci.
- BUFFER_OVERFLOW_CHECKS – kontrola zásobníku na přetečení, bohužel to není náš případ, vypínáme.
- MINWIN_SDK_LIB_PATH – tato hodnota se odkazuje na proměnnou SDK_LIB_PATH, nemusíte se obávat, že máte tuto systémovou proměnnou nevyhlášenou; ve chvíli, kdy spustíme checked build z DDK, bude tato proměnná vyhlášena a odkazovat na potřebné knihovny.
- SOURCES – seznam zdrojů vaší programu.
- INCLUDES – hlavičkové soubory potřebné pro sestavení. Obvykle zde uvádíme cestu k souborům, které jsou dodávány s DDK, ale můžete přidat i další.
- TARGETLIBS – seznam knihoven, které je třeba propojit.
- USE_NTDLL – povinné pole, které je třeba nastavit na hodnotu 1. Z jasných důvodů.
- USER_C_FLAGS – jakékoli příznaky, které můžete použít v preprocesorových direktivách při přípravě kódu aplikace.
Abychom mohli sestavit, musíme spustit x86 (nebo x64) Checked Build, změnit pracovní adresář na složku s projektem a provést příkaz Build. Výsledek na screenshotu ukazuje, že jsme sestavili jeden spustitelný soubor.

Tento soubor se nedá tak snadno spustit, systém protestuje a posílá nás přemýšlet o jeho chování s následující chybou:

Jak spustit nativní aplikaci?
Při spuštění autochk je sekvence spuštění programů určena hodnotou klíče registru:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteSprávce relací postupně provádí programy z tohoto seznamu. Spustitelné soubory správce relací vyhledává v adresáři system32. Formát hodnoty klíče registru je následující:
autocheck autochk *MyNativeHodnota by měla být v šestnáctkové podobě, nikoli v běžném ASCII, takže klíč uvedený výše bude mít formát:
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,00Pro konverzi názvu můžete použít online službu, například, .

Abychom mohli spustit nativní aplikaci, musíme:
- Kopírovat spustitelný soubor do složky system32
- Přidat do registru klíč
- Restartovat stroj
Pro pohodlí vám zde je připravený skript pro instalaci nativní aplikace:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Příklad Native nainstalován
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,00Po instalaci a restartu, ještě před zobrazením obrazovky pro výběr uživatelů, uvidíme následující obraz.

Výsledek
Na příkladu této malé aplikace jsme se ujistili, že spuštění aplikace na úrovni Windows Native je zcela možné. Dále se spolu s kluky z univerzity Innopolis pustíme do vývoje služby, která zahájí interakci s ovladačem mnohem dříve než v předchozí verzi našeho projektu. S příchodem rozhraní win32 bude logické předat ovládání plnohodnotné službě, která již byla vyvinuta (o tom více později). ).
V dalším článku se dotkneme další součásti služby Active Restore, a to ovladače UEFI. Sledujte náš blog, abyste nezmeškali příští příspěvek.
Zdroj: habr.com
