Astăzi continuăm povestea despre cum, împreună cu colegii de la Universitatea Innopolis, dezvoltăm tehnologia Active Restore pentru a permite utilizatorului să înceapă să lucreze cât mai repede posibil pe mașina sa după o defecțiune. Vom discuta despre aplicațiile native Windows, inclusiv despre caracteristicile creării și lansării acestora. Sub acest articol – puțin despre proiectul nostru, dar și un ghid practic despre cum să scrii aplicații native.

În postările anterioare, am discutat deja despre ce este , și cum studenții de la Innopolis dezvoltă . Astăzi vreau să mă opresc asupra aplicațiilor native, la nivelul cărora dorim să „scufundăm” serviciul nostru de recuperare activă. Dacă totul merge bine, atunci vom putea:
- Să lansăm serviciul mult mai devreme
- Să ne conectăm mult mai repede la cloudul în care se află backup-ul
- Să înțelegem mult mai repede în ce mod se află sistemul – în modul de încărcare normală sau de recuperare
- Să restaurăm cu mult mai puține fișiere în avans
- Să permitem utilizatorului să înceapă să lucreze și mai repede.
Ce este, de fapt, o aplicație nativă?
Pentru a răspunde la această întrebare, haideți să ne uităm la secvența de apeluri pe care o face sistemul, de exemplu, dacă un programator în aplicația sa încearcă să creeze un fișier.

Pavel Yosifovich – Programare cu kernel Windows (2019)
Programatorul folosește funcția , care este declarată în fișierul de antet fileapi.h și implementată în Kernel32.dll. Totuși, această funcție nu se ocupă de crearea fișierului, ci doar verifică argumentele de intrare și apelează funcția (prefixul Nt indică că funcția este nativă). Această funcție este declarată în fișierul de antet winternl.h și implementată în ntdll.dll. Ea pregătește saltul în spațiul kernel-ului, după care efectuează un apel de sistem pentru a crea fișierul. Așadar, în acest caz, Kernel32 este doar un wrapper pentru Ntdll. Unul dintre motivele pentru care este făcut acest lucru este că Microsoft are posibilitatea să schimbe funcțiile lumii native fără a afecta interfețele standard. Microsoft nu recomandă apelarea directă a funcțiilor native și nu documentează cea mai mare parte dintre ele. Apropo, funcțiile nedocumentate pot fi găsite .
Principala avantaj al aplicațiilor native este că ntdll este încărcat în sistem mult mai devreme decât kernel32. Este logic, având în vedere că kernel32 necesită ntdll pentru a funcționa. Ca rezultat, aplicațiile care utilizează funcții native pot începe să funcționeze mult mai devreme.
Astfel, Aplicațiile Native Windows sunt programe capabile să pornească într-o fază timpurie a procesului de încărcare a Windows. Ele folosesc NUMAI funcții din ntdll. Un exemplu de astfel de aplicație: care execută pentru a verifica discul pentru erori înainte de a porni serviciile principale. Exact la acest nivel vrem să vedem Active Restore.
Ce ne va trebui?
- (Driver Development Kit), cunoscut acum și sub numele de WDK 7 (Windows Driver Kit).
- O mașină virtuală (de exemplu, Windows 7 x64)
- Nu este obligatorie, dar pot ajuta fișierele header care pot fi descărcate
Ce este în cod?
Să facem puțin exercițiu și, de exemplu, să scriem o mică aplicație care:
- Afișează un mesaj pe ecran
- Alocă puțină memorie
- Așteaptă introducerea de la tastatură
- Eliberează memoria ocupată
În aplicațiile native, punctul de intrare nu este main sau winmain, ci funcția NtProcessStartup, deoarece practic pornim direct un nou proces în sistem.
Să începem cu afișarea unui mesaj pe ecran. Pentru aceasta, avem funcția nativă , care ca argument primește un pointer la un obiect al structurii UNICODE_STRING. Îl putem initializa cu ajutorul lui RtlInitUnicodeString. Ca urmare, pentru a afișa textul pe ecran, putem scrie o mică funcție astfel:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Deoarece avem acces doar la funcții din ntdll și alte biblioteci nu sunt încă în memorie, vom avea neapărat probleme cu alocarea memoriei. Operatorul new nu există încă (deoarece provine dintr-o lume mult mai înaltă C++), de asemenea nu există funcția malloc (deoarece pentru ea sunt necesare biblioteci runtime C). Putem folosi doar stiva. Dar dacă trebuie să alocăm dinamic memorie, va trebui să facem acest lucru în heap. Așadar, să creăm pentru noi un heap și să luăm din el memorie atunci când avem nevoie.
Pentru această sarcină, funcția este potrivită. Apoi, folosind RtlAllocateHeap și RtlFreeHeap, vom ocupa și elibera memorie atunci când avem nevoie.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// creează heap pentru a aloca memorie mai târziu
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// alocă buffer de dimensiune bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// eliberează bufferul (de fapt, nu este necesar deoarece distrugem heapul în pasul următor)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Să trecem la așteptarea intrării de la tastatură.
// 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;
}
}Tot ce trebuie să facem este să folosim pe dispozitivul deschis și să așteptăm până când tastatura ne va returna o apăsare. În cazul în care este apăsată tasta ESC, vom continua lucrul. Pentru a deschide dispozitivul, va trebui să apelăm funcția NtCreateFile (va trebui să deschidem DeviceKeyboardClass0). De asemenea, vom apela , pentru a inițializa un obiect pentru așteptare. Vom declara singuri structura KEYBOARD_INPUT_DATA, care reprezintă datele tastaturii. Aceasta ne va facilita munca.
Funcția aplicației native se încheie prin apelarea funcției , pentru că practic ne ucidem propriul proces.
Întregul cod al aplicației noastre mici:
#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: Putem folosi cu ușurință în cod funcția DbgBreakPoint() pentru a ne opri în debugger. Cu toate acestea, va trebui să conectăm WinDbg la mașina virtuală pentru depanarea kernelului. Instrucțiunile despre cum să facem asta le putem găsi sau pur și simplu să folosim .
Compilarea și construcția
Cel mai simplu mod de a construi o aplicație nativă este să folosim (Driver Development Kit). Avem nevoie exact de vechea versiune a șaptea, deoarece versiunile mai recente au o abordare ușor diferită și colaborează strâns cu Visual Studio. Dacă folosim DDK, atunci proiectul nostru are nevoie doar de Makefile și surse.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defsurse:
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 = 1Makefile-ul tău va fi exact la fel, iar la surse să ne oprim puțin mai în detaliu. În acest fișier se specifică sursele programului tău (fișiere .c), opțiunile de construcție și alte parametere.
- TARGETNAME – numele fișierului executabil care ar trebui să rezulte la final.
- TARGETTYPE – tipul fișierului executabil, poate fi un driver (.sys), în acest caz valoarea câmpului trebuie să fie DRIVER, dacă este o bibliotecă (.lib), atunci valoarea LIBRARY. În cazul nostru, avem nevoie de un fișier executabil (.exe), așa că setăm valoarea PROGRAM.
- UMTYPE – valorile posibile pentru acest câmp sunt: console pentru aplicații de consolă, windows pentru aplicații în modul fereastră. Totuși, trebuie să specificăm nt pentru a obține o aplicație nativă.
- BUFFER_OVERFLOW_CHECKS – verificarea stivei pentru suprasarcină de buffer, din păcate nu este cazul nostru, deci dezactivăm.
- MINWIN_SDK_LIB_PATH – această valoare se referă la variabila SDK_LIB_PATH, nu este o problemă dacă această variabilă de sistem nu este declarată, atunci când vom rula build-ul verificat din DDK, această variabilă va fi declarată și va indica bibliotecile necesare.
- SOURCES – lista surselor programului tău.
- INCLUDES – fișierele de antet necesare pentru compilare. De obicei, aici se specifică calea către fișierele incluse cu DDK, dar poți adăuga și alte fișiere dacă este necesar.
- TARGETLIBS – lista bibliotecilor care trebuie legate.
- USE_NTDLL – câmp obligatoriu care trebuie setat pe 1. Din motive destul de evidente.
- USER_C_FLAGS – orice flage pe care le poți folosi în directivele preprocesorului în pregătirea codului aplicației.
Așadar, pentru a compila, trebuie să lansăm x86 (sau x64) Checked Build, să schimbăm directorul de lucru în folderul proiectului și să executăm comanda Build. Rezultatul de pe captură arată că am obținut un fișier executabil.

Acest fișier nu poate fi lansat cu ușurință, sistemul dă o eroare și ne trimite să ne gândim la comportamentul său cu următoarea eroare:

Cum se lansează o aplicație nativă?
La începutul autochk, secvența de lansare a programelor este determinată de valoarea cheia de registru:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteManagerul sesiunii execută programele din această listă pe rând. Executabilele sunt căutate de managerul sesiunii în directorul system32. Formatul valorii cheii de registru este următorul:
autocheck autochk *MyNativeValoarea trebuie să fie în format hexazecimal, nu în ASCII obișnuit, prin urmare cheia prezentată mai sus va avea formatul:
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,00Pentru a converti numele, poți folosi un serviciu online, de exemplu, .

Prin urmare, pentru a lansa o aplicație nativă, trebuie să:
- Copiați fișierul executabil în folderul system32
- Adăugați o cheie în registru
- Reporniți mașina
Pentru comoditate, iată un script pregătit pentru instalarea aplicației native:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Exemplu Native Instalate
pauseadd.reg
REGEDIT4
[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession 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,00După instalare și repornire, înainte de a apărea ecranul de selecție a utilizatorilor, vom obține următoarea imagine:

Rezultatul
Pe exemplul acestei aplicații mici, am demonstrat că este posibil să rulezi aplicația la nivel Windows Native. În continuare, împreună cu colegii de la Universitatea Innopolis, vom continua să construim un serviciu care va iniția procesul de interacțiune cu driverul mult mai devreme decât în versiunea anterioară a proiectului nostru. Odată cu apariția interfeței win32, va fi logic să transferăm controlul către un serviciu complet dezvoltat (despre care vom discuta mai în detaliu) ).
În următorul articol, ne vom ocupa de un alt component al serviciului Active Restore, și anume driverul UEFI. Abonați-vă la blogul nostru pentru a nu pierde următoarea postare.
Sursa: habr.com
