Dnes pokračujeme v rozprávaní o tom, ako spolu s chlapcami z Innopolis University vyvíjame technológiu Active Restore, aby sme používateľovi umožnili čo najskôr sa znova zapojiť do práce na svojom počítači po zlyhaní. Bude reč o natívnych aplikáciách Windows, vrátane vlastností ich vytvorenia a spustenia. Pod katom – trochu o našom projekte, ako aj praktický návod, ako písať natívne aplikácie.

V predchádzajúcich príspevkoch sme už hovorili o tom, čo je to , a ako študenti z Innopolisu vyvíjajú . Dnes sa chcem zamerať na natívne aplikácie, do úrovne ktorých chceme "zakopávať" našu službu aktívneho obnovenia. Ak sa všetko podarí, budeme môcť:
- Oveľa skôr spustiť samotnú službu
- Oveľa skôr sa spojiť s cloudom, v ktorom leží záloha
- Oveľa skôr pochopiť, v akom režime sa nachádza systém – normálnom spustení alebo obnove
- Oveľa menej súborov obnovovať vopred
- Umožniť používateľovi začať pracovať ešte rýchlejšie.
Čo je vlastne natívna aplikácia?
Aby sme na túto otázku odpovedali, pozrime sa na sekvenciu volaní, ktoré vykonáva systém, napríklad ak programátor vo svojej aplikácii sa pokúša vytvoriť súbor.

Pavel Yosifovich – Programovanie jadra Windows (2019)
Programátor používa funkciu , ktorá je deklarovaná v hlavičkovom súbore fileapi.h a implementovaná v Kernel32.dll. Avšak táto funkcia sa neposkytuje pre vytvorenie súboru, len kontroluje argumenty na vstupe a volá funkciu (predpona Nt svedčí o tom, že funkcia je natívna). Táto funkcia je deklarovaná v hlavičkovom súbore winternl.h a implementovaná v ntdll.dll. Pripravuje sa na skok do jadrového priestoru, po čom uskutočňuje systémové volanie na vytvorenie súboru. V tomto prípade ide o to, že Kernel32 je len obal pre Ntdll. Jedným z dôvodov, prečo je to takto urobené, je, že Microsoft má možnosť meniť funkcie natívneho sveta, ale pritom nezasahovať do štandardných rozhraní. Microsoft neodporúča priamo volať natívne funkcie a väčšinu z nich nedokumentuje. Mimochodom, nedokumentované funkcie je možné nájsť .
Hlavnou výhodou natívnych aplikácií je, že ntdll sa načítava do systému podstatne skôr ako kernel32. Je to logické, pretože kernel32 potrebuje ntdll na svoju činnosť. V dôsledku toho môžu aplikácie, ktoré používajú natívne funkcie, začať pracovať podstatne skôr.
Tým pádom, Windows Native Applications sú programy, ktoré sa dokážu spustiť v ranných fázach spúšťania Windows. Používajú IBA funkcie z ntdll. Príklad takejto aplikácie: ktorý vykonáva na kontrolu disku na chyby ešte pred spustením hlavných služieb. Presne na tejto úrovni chceme vidieť náš Active Restore.
Čo budeme potrebovať?
- (Driver Development Kit), dnes známy tiež ako WDK 7 (Windows Driver Kit).
- Virtuálny stroj (napr. Windows 7 x64)
- Nie je to nutné, ale môžu pomôcť hlavičkové súbory, ktoré si môžeme stiahnuť.
Čo sa nachádza v kóde?
Poďme si na chvíľu zacvičiť a ako príklad si napíšme malú aplikáciu, ktorá:
- Zobrazí správu na obrazovke
- Alokuje trochu pamäte
- Čaká na vstup z klávesnice
- Uvoľní obsadenú pamäť
V natívnych aplikáciách je bodom vstupu nie main alebo winmain, ale funkcia NtProcessStartup, pretože v podstate priamo spúšťame nové procesy v systéme.
Začneme so zobrazením správy na obrazovke. Na to máme natívnu funkciu , ktorá ako argument prijíma ukazovateľ na objekt štruktúry UNICODE_STRING. Inicializovať ho nám pomôže RtlInitUnicodeString. Výsledkom je, že na zobrazenie textu na obrazovke môžeme napísať takúto malú funkciu:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Keďže máme prístup len k funkciám z ntdll a v pamäti ešte nie sú žiadne iné knižnice, určite sa nám vyskytnú problémy s alokovaním pamäte. Operátor new ešte neexistuje (pretože pochádza z príliš vysokoúrovňového sveta C++), rovnako chýba funkcia malloc (pre ňu sú potrebné knižnice runtime C). Môžeme síce používať len zásobník. Ale ak potrebujeme dynamicky alokovať pamäť, musíme to urobiť v hromade (t.j. heap). Preto si poďme vytvoriť hromadu a budeme si z nej brať pamäť, keď ju budeme potrebovať.
Na tento úkol nám vyhovuje funkcia . Následne, pomocou RtlAllocateHeap a RtlFreeHeap, budeme zaberať a uvoľňovať pamäť, keď to budeme potrebovať.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// vytvorenie haldy na priradenie pamäte neskôr
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// priradenie buffera s veľkosťou bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// uvoľnenie buffera (v skutočnosti nie je potrebné, pretože zničíme haldu v nasledujúcom kroku)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Prejdime k čakaniu 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šetko, čo potrebujeme, je použiť na otvorenom zariadení a čakať, kým klávesnica nevráti akékoľvek stlačenie. V prípade, že bola stlačená klávesa ESC, budeme pokračovať v práci. Na otvorenie zariadenia budeme musieť zavolať funkciu NtCreateFile (otvoriť budeme musieť DeviceKeyboardClass0). Takisto zavoláme , aby sme inicializovali objekt na čakanie. Samostatne deklarujeme štruktúru KEYBOARD_INPUT_DATA, ktorá predstavuje dáta z klávesnice. To nám uľahčí prácu.
Práca natívnej aplikácie končí zavolaním funkcie , pretože jednoducho zabíjame vlastný proces.
Celý kód našej malej aplikácie:
#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 bez problémov použiť v kóde funkciu DbgBreakPoint() na zastavenie v debuggery. Pravda, budeme musieť pripojiť WinDbg k virtuálnemu stroju pre kernel debugging. Inštrukciu, ako to urobiť, možno nájsť alebo jednoducho použiť .
Kompilácia a zostavenie
Najjednoduchší spôsob zostaviť natívnu aplikáciu je použiť (Driver Development Kit). Potrebujeme presne starú siedmu verziu, pretože novšie verzie majú trochu iný prístup a úzko spolupracujú s Visual Studio. Ak použijeme DDK, náš projekt potrebuje iba 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 presne taký istý, na sources sa však zastavme trochu podrobnejšie. V tomto súbore sa uvádzajú zdrojové súbory vašej aplikácie (súbory .c), možnosti zostavenia a ďalšie parametre.
- TARGETNAME – názov spustiteľného súboru, ktorý by mal byť nakoniec vytvorený.
- TARGETTYPE – typ spustiteľného súboru, môže to byť ovládač (.sys), v takom prípade by hodnota poľa mala byť DRIVER, ak knižnica (.lib), tak hodnota LIBRARY. V našom prípade potrebujeme spustiteľný súbor (.exe), preto nastavujeme hodnotu PROGRAM.
- UMTYPE – možné hodnoty tohoto pole: console pro konzolovou aplikaci, windows pro provoz v režimu oken. 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 odkazuje na proměnnou SDK_LIB_PATH, nemusíte se obávat, že nemáte takovou systémovou proměnnou deklarovanou, v okamžiku, kdy spustíme checked build z DDK, bude tato proměnná deklarována a bude ukazovat na potřebné knihovny.
- SOURCES – seznam zdrojů vaší aplikace.
- INCLUDES – hlavičkové soubory, které jsou potřebné pro sestavení. Obvykle zde uvádějí cestu k souborům, které přicházejí s DDK, ale můžete uvést i jakékoliv další.
- TARGETLIBS – seznam knihoven, které je třeba linkovat.
- USE_NTDLL – povinné pole, které je třeba nastavit na hodnotu 1. Z očividných důvodů.
- USER_C_FLAGS – jakékoliv příznaky, které můžete použít v preprocesorových direktivách při přípravě kódu aplikace.
Takže pro sestavení 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 snímku obrazovky ukazuje, že jsme vytvořili jeden spustitelný soubor.

Tento soubor nelze tak snadno spustit, systém si stěžuje a posílá nás přemýšlet o svém chování s následující chybou:

Jak spustit nativní aplikaci?
Při spuštění autochk určuje sekvenci spuštění programů hodnota klíče registru:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteSprávce relace postupně vykonává programy z tohoto seznamu. Samotné spustitelné soubory správce relace hledá v adresáři system32. Formát hodnoty klíče registru je následující:
autocheck autochk *MyNativeHodnota musí být ve formátu hexadecimálním, nikoli v obvyklém ASCII, proto uvedený klíč 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 převod názvu můžete použít online službu, například: .

Takže, abychom spustili nativní aplikaci, musíme:
- Zkopírovat spustitelný soubor do složky system32
- Přidat do registru klíč
- Restartovat stroj
Pro pohodlí zde máte připravený skript pro instalaci nativní aplikace:
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,00Po inštalácii a reštarte, ešte pred zobrazením obrazovky na výber používateľov, získame nasledujúci pohľad:

Záver
Na príklade tejto malej aplikácie sme sa presvedčili, že spustiť aplikáciu na úrovni Windows Native je úplne možné. Pokračujeme so spolupracovníkmi z Innopolis University v budovaní služby, ktorá spustí proces interakcie s ovládačom oveľa skôr, než to bola v predchádzajúcej verzii nášho projektu. S príchodom shellwin32 bude logické odovzdať správu plnohodnotnej službe, ktorá už bola vyvinutá (o tom podrobnejšie ).
V ďalšom článku sa budeme zaoberať ďalšou súčasťou služby Active Restore, konkrétne UEFI ovládačom. Sledujte náš blog, aby ste nezmeškali ďalší príspevok.
Zdroj: habr.com
