Danes nadaljujemo pripoved o tem, kako skupaj s fanti z Univerze Innopolis razvijamo tehnologijo Active Restore, da bi uporabniku omogočili, da čim prej začne delati na svojem računalniku po okvari. Govorili bomo o nativnih aplikacijah Windows, vključno z značilnostmi njihovega ustvarjanja in zagona. Podrobnosti o našem projektu in praktično vodilo o tem, kako pisati nativne aplikacije, najdete spodaj.

V prejšnjih objavah smo že govorili o tem, kaj je , in kako študenti iz Innopolisa razvijajo . Danes se želim osredotočiti na nativne aplikacije, do ravni katerih želimo 'poglobiti' naš servis aktivnega okrevanja. Če bo vse šlo po načrtih, bomo lahko:
- Zagnali sam servis precej prej
- Se povezali z oblakom, kjer je varnostna kopija, precej prej
- Zelo hitro razumeli, v katerem načinu deluje sistem - v načinu normalnega zagona ali okrevanja
- Zelo malo datotek predhodno obnovili
- Omogočili uporabniku, da se loti dela še hitreje.
Kaj sploh je nativna aplikacija?
Da bi odgovorili na to vprašanje, si poglejmo zaporedje klicev, ki jih izvede sistem, če programer v svoji aplikaciji poskuša ustvariti datoteko.

Pavel Yosifovich — Windows Kernel Programming (2019)
Programer uporablja funkcijo , ki je razglašena v glavi fileapi.h in implementirana v Kernel32.dll. Vendar sama ta funkcija ne ustvarja datoteke, ampak le preverja vhodne argumente in kliče funkcijo (predpona Nt prav tako potrjuje, da je funkcija nativna). Ta funkcija je razglašena v glavi winternl.h in implementirana v ntdll.dll. Opravi priprave na skok v jedrni prostor, nato pa izvede sistemski klic za ustvarjanje datoteke. V tem primeru imamo torej Kernel32 kot le ovitek za Ntdll. Eden od razlogov za to je, da ima Microsoft tako možnost spreminjati funkcije nativnega sveta, ne da bi se dotikal standardnih vmesnikov. Microsoft ne priporoča neposrednega klica nativnim funkcijam in večino od njih ne dokumentira. Mimogrede, nedokumentirane funkcije lahko najdete. .
Glavna prednost nativnih aplikacija je u tome što se ntdll učitava u sistem mnogo ranije od kernel32. To je logično, jer kernel32 zahteva prisustvo ntdll za rad. Kao posledica, aplikacije koje koriste nativne funkcije mogu započeti rad značajno ranije.
Dakle, Windows Native Applications su programi koji mogu da se pokrenu u ranoj fazi podizanja Windows-a. Oni koriste SAMO funkcije iz ntdll. Primer takve aplikacije: koji izvršava za proveru diska na greške još pre pokretanja osnovnih servisa. Upravo na tom nivou želimo da vidimo naš Active Restore.
Šta će nam biti potrebno?
- (Driver Development Kit), danas poznat i kao WDK 7 (Windows Driver Kit).
- Virtuelna mašina (na primer, Windows 7 x64)
- Nije obavezno, ali mogu pomoći zaglavlja koja se mogu preuzeti
Šta je u kodu?
Hajde da se malo vežbamo i kao primer napišemo malu aplikaciju koja:
- Ispisuje poruku na ekran
- Alocira malo memorije
- Čeka unos sa tastature
- Oslobađa zauzetu memoriju
U nativnim aplikacijama, ulazna tačka nije main ili winmain, već funkcija NtProcessStartup, budući da zapravo direktno pokrećemo nove procese u sistemu.
Počnimo sa ispisom poruke na ekran. Za to imamo nativnu funkciju , koja kao argument prima pokazivač na objekat strukture UNICODE_STRING. Inicijalizovati ćemo ga pomoću RtlInitUnicodeString. Kao rezultat, za ispis teksta na ekran možemo napisati ovakvu malu funkciju:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Pošto su nam dostupne samo funkcije iz ntdll, i drugih biblioteka u memoriji jednostavno još nema, definitivno ćemo imati problema sa alokacijom memorije. Operator new još ne postoji (jer pripada previše visokom svetu C++), takođe nema funkcije malloc (za nju su potrebne C runtime biblioteke). Možda bismo mogli da koristimo samo stek. Ali ako nam je potrebna dinamička alokacija memorije, to ćemo morati da uradimo u hrpi (tj. heap). Zato hajde da stvorimo hrpu za sebe i uzimamo iz nje memoriju kad nam zatreba.
Za ovu svrhu odgovara funkcija . Zatim, koristeći RtlAllocateHeap i RtlFreeHeap, zauzimaćemo i oslobađaćemo memoriju kada nam to bude potrebno.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// ustvarimo kup za dodelitev pomnilnika kasneje
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// dodelimo pomnilnik velikosti bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// sprostimo pomnilnik (v resnici ni potrebno, ker bomo uničili kup v naslednjem koraku)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Preidimo na čakanje na vnose s tipkovnice.
// 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;
}
}Vse kar potrebujemo – je uporabiti na odprtem napravi in čakati, da tipkovnica vrne kakršno koli pritiskanje tipke. V primeru, da je pritisnjena tipka ESC, bomo nadaljevali z delom. Da bi odprli napravo, bomo morali poklicati funkcijo NtCreateFile (odpreti bo treba DeviceKeyboardClass0). Prav tako bomo poklicali , da inicializiramo objekt za čakanje. Sami bomo razglasili strukturo KEYBOARD_INPUT_DATA, ki predstavlja podatke tipkovnice. To nam bo olajšalo delo.
Delo nativne aplikacije se konča s klicem funkcije , ker preprosto ubijemo svoj proces.
Vse kode naše majhne aplikacije:
#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: Lahko brez problema uporabimo v kodi funkcijo DbgBreakPoint() za ustavitev v debuggerju. Vendar bo treba povezati WinDbg z virtualno napravo za jedrno razhlevanje. Navodila, kako to storiti, lahko najdete ali pa preprosto uporabite .
Kompilacija in sestava
Najlažji način za sestavo nativne aplikacije je uporaba (Driver Development Kit). Potrebujemo prav staro sedmo različico, saj imajo novejše različice nekoliko drugačen pristop in tesno delujejo z Visual Studio. Če uporabimo DDK, potrebujemo za naš projekt le Makefile in 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 = 1Vaš Makefile bo popolnoma enak, na sources pa se ustavimo malo podrobneje. V tem datoteki so navedeni izvorni kodne datoteke (.c), možnosti sestave in druge nastavitve.
- TARGETNAME – ime izvršne datoteke, ki naj bi nastala na koncu.
- TARGETTYPE – tip izvršne datoteke, lahko je to gonilnik (.sys), takrat mora biti vrednost polja DRIVER, če je knjižnica (.lib), pa mora biti vrednost LIBRARY. V našem primeru potrebujemo izvršno datoteko (.exe), zato nastavimo vrednost PROGRAM.
- UMTYPE – možne vrednosti tega polja so: console za konzolno aplikacijo, windows za delo v okenskem načinu. Vendar pa moramo navesti nt, da dobimo nativno aplikacijo.
- BUFFER_OVERFLOW_CHECKS – preverjanje sklada na prelivanje pomnilnika, žal to ni naš primer, izklopimo.
- MINWIN_SDK_LIB_PATH – ta vrednost se nanaša na spremenljivko SDK_LIB_PATH, ni vam treba biti zaskrbljeni, da nimate take sistemske spremenljivke, ko bomo zagnali checked build iz DDK, ta spremenljivka bo razglašena in bo kazala na potrebne knjižnice.
- SOURCES – seznam izvorov vaše aplikacije.
- INCLUDES – datoteke z glavo, ki so potrebne za sestavo. Tukaj običajno navedemo pot do datotek, ki so priložene DDK, vendar lahko dodatno navedete tudi druge.
- TARGETLIBS – seznam knjižnic, ki jih je treba povezati.
- USE_NTDLL – obvezno polje, ki ga je treba nastaviti na vrednost 1. Zelo očitno.
- USER_C_FLAGS – vsi zastavice, ki jih lahko uporabljate v predprocesorskih direktivah pri pripravi kode aplikacije.
Torej za sestavo moramo zagnati x86 (ali x64) Checked Build, spremeniti delovni imenik na mapo s projektom in izvesti ukaz Build. Rezultat na posnetku zaslona kaže, da smo sestavili eno izvršljivo datoteko.

Te datoteke ne bo enostavno zagnati, sistem opozarja in nas pošilja, da razmislimo o svojem vedenju s sledečo napako:

Kako zagnati nativno aplikacijo?
Ob zagonu autochk zaporedje zagon programov določa vrednost registra:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteUpravitelj seje zaporedoma izvaja programe s tega seznama. Izvršljive datoteke upravitelj seje najde v direktoriju system32. Oblika vrednosti registra je naslednja:
autocheck autochk *MyNativeVrednost mora biti v šestnajstiškem formatu in ne v običajni ASCII, zato bo ključ, predstavljen zgoraj, imel obliko:
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,00Za pretvorbo imena lahko uporabite spletno storitev, na primer, .

Torej, da zagnamo nativno aplikacijo, moramo:
- Kopirati izvršljivo datoteko v mapo system32
- Dodati ključ v register
- Ponovno zagnati računalnik
Za lažjo uporabo vam predstavljam pripravljeno skripto za namestitev nativne aplikacije:
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 namestitvi in ponovnem zagonu, še preden se prikaže zaslon za izbiro uporabnikov, bomo videli naslednjo sliko:

Zaključek
Na primeru te majhne aplikacije smo se prepričali, da je zagon aplikacije na ravni Windows Native povsem mogoč. Nadalje bomo s kolegi z Univerze Innopolis nadaljevali z razvojem storitve, ki bo sprožila postopek interakcije z gonilnikom precej prezgodaj v primerjavi s prejšnjo različico našega projekta. S prihodom vmesnika win32 bo smiselno prenesti upravljanje celoviti storitvi, ki je bila že razvita (o tem podrobneje ).
V naslednjem članku se bomo dotaknili še ene komponente storitve Active Restore, in sicer gonilnika UEFI. Prijavite se na naš blog, da ne zamudite naslednjega prispevka.
Izvor: habr.com
