I dag fortsætter vi fortællingen om, hvordan vi sammen med gutterne fra Innopolis Universitet udvikler teknologien Active Restore, for at give brugeren mulighed for hurtigst muligt at begynde at arbejde på sin maskine efter et nedbrud. Vi vil tale om nativ Windows-applikationer, herunder særlige aspekter af deres oprettelse og kørsel. Under katten – lidt om vores projekt samt en praktisk vejledning til, hvordan man skriver native applikationer.

I tidligere indlæg har vi allerede fortalt, hvad er, og hvordan studerende fra Innopolis udvikler . I dag vil jeg fokusere på native applikationer, som vi ønsker, at vores aktive gendannelsestjeneste skal "grave sig ned" til. Hvis alt går godt, så kan vi:
- Start servicen meget tidligere
- Forbindes til skyen, hvor backup'en ligger, meget tidligere
- Forstå i hvilken tilstand systemet befinder sig – normal opstart eller gendannelse – meget tidligere
- Restaurere langt færre filer på forhånd
- Tillade brugeren at komme i gang med arbejdet hurtigere.
Hvad er egentlig en native applikation?
For at besvare dette spørgsmål, lad os se på rækkefølgen af opkald, som systemet foretager, når en programmør i sin applikation forsøger at oprette en fil.

Pavel Yosifovich – Windows Kernel Programming (2019)
Programmøren bruger funktionen , som er deklareret i header-filen fileapi.h og implementeret i Kernel32.dll. Men denne funktion beskæftiger sig ikke med at oprette filen; den kontrollerer kun de indkommende argumenter og kalder funktionen (præfikset Nt indikerer netop, at funktionen er native). Denne funktion er deklareret i header-filen winternl.h og implementeret i ntdll.dll. Den forbereder et spring ind i kernerummet og foretager derefter et systemopkald for at oprette filen. I dette tilfælde viser det sig, at Kernel32 blot er en wrapper for Ntdll. En af grundene til dette er, at Microsoft har mulighed for at ændre native funktioner, men stadig ikke røre ved standard grænseflader. Microsoft anbefaler ikke at kalde native funktioner direkte og dokumenterer størstedelen af dem ikke. For øvrigt kan man finde ikke-dokumenterede funktioner .
Den primære fordel ved native applikationer er, at ntdll indlæses i systemet betydeligt tidligere end kernel32. Det giver mening, da kernel32 kræver ntdll for at fungere. Som følge heraf kan applikationer, der bruger native funktioner, starte betydeligt tidligere.
Dermed er Windows Native Applications programmer, der kan startes i den tidlige opstart af Windows. De bruger KUN funktioner fra ntdll. Et eksempel på en sådan applikation: som udfører til at kontrollere disken for fejl, før de primære tjenester starter. Det er på dette niveau, vi ønsker at se vores Active Restore.
Hvad har vi brug for?
- (Driver Development Kit), nu også kendt som WDK 7 (Windows Driver Kit).
- En virtuel maskine (f.eks. Windows 7 x64)
- Ikke nødvendigt, men include-filer kan være nyttige, som kan downloades.
Hvad med koden?
Lad os øve os lidt og som eksempel skrive en lille applikation, der:
- Viser en besked på skærmen
- Allokerer lidt hukommelse
- Venter på input fra tastaturet
- Frigiver den optagne hukommelse
I native applikationer er indgangspunktet ikke main eller winmain, men funktionen NtProcessStartup, da vi faktisk direkte starter nye processer i systemet.
Lad os starte med at vise en besked på skærmen. Til dette har vi den native funktion , som som argument tager en pointer til et objekt af strukturen UNICODE_STRING. RtlInitUnicodeString kan hjælpe os med at initialisere det. Som resultat kan vi skrive en lille funktion til at vise teksten på skærmen:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Da vi kun har adgang til funktioner fra ntdll, og der ikke er andre biblioteker i hukommelsen endnu, vil vi helt sikkert støde på problemer med at allokere hukommelse. Operatoren new eksisterer ikke endnu (fordi den stammer fra en for høj-niveau verden C++), og der er heller ikke nogen malloc funktion (da den kræver C runtime biblioteker). Vi kan selvfølgelig kun bruge stakken. Men hvis vi har brug for dynamisk at allokere hukommelse, må vi gøre det i heapen. Så lad os oprette en heap til os selv og tage hukommelse fra den, når vi har brug for det.
Til denne opgave kan funktionen . Derefter, ved hjælp af RtlAllocateHeap og RtlFreeHeap, vil vi tage og frigive hukommelse, når vi har brug for det.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// opretter en bunke for at kunne allokere hukommelse senere
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// allokerer en buffer af størrelse bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// frigiver buffer (faktisk ikke nødvendigt, da vi ødelægger bunken i næste trin)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Lad os gå videre til at vente på input fra tastaturet.
// 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;
}
}Alt hvad vi skal bruge – er at bruge på den åbne enhed, og vente på at tastaturet returnerer et hvilket som helst tryk. Hvis ESC-tasten trykkes, fortsætter vi arbejdet. For at åbne enheden skal vi kalde funktionen NtCreateFile (vi skal åbne DeviceKeyboardClass0). Vi vil også kalde , for at initialisere en objekt til at vente. Vi vil selv erklære strukturen KEYBOARD_INPUT_DATA, som repræsenterer tastaturdata. Dette vil lette vores arbejde.
Dreneden af den native applikation afsluttes ved at kalde funktionen , fordi vi bare dræber vores egen proces.
Hele koden til vores lille applikation:
#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: Vi kan nemt bruge DbgBreakPoint() funktionen i koden for at stoppe i debuggeren. Dog skal vi forbinde WinDbg til den virtuelle maskine for kernel debugging. Instruktioner til hvordan man gør dette kan findes eller bare bruge .
Kompilering og opbygning
Den nemmeste måde at samle en native applikation på er at bruge (Driver Development Kit). Vi har brug for den gamle syvende version, da nyere versioner har en lidt anden tilgang og arbejder tæt sammen med Visual Studio. Hvis vi bruger DDK, har vores projekt kun brug for Makefile og sources.
Makefil
!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 = 1Din Makefile vil være præcis den samme, men lad os stoppe lidt længere ved sources. I denne fil angives kildeteksterne til dit program (filene .c), byggeindstillinger og andre parametre.
- TARGETNAME – navnet på den eksekverbare fil, der skal fremkomme i sidste ende.
- TARGETTYPE – typen af den eksekverbare fil, dette kan være en driver (.sys), så skal værdien være DRIVER, hvis en bibliotek (.lib), så skal værdien være LIBRARY. I vores tilfælde har vi brug for en eksekverbar fil (.exe), så vi sætter værdien til PROGRAM.
- UMTYPE – mulige værdier for dette felt: console for konsolapplikation, windows til brug i vinduing. Men vi skal angive nt for at få en native applikation.
- BUFFER_OVERFLOW_CHECKS – kontrol af stakken for buffer overflow, desværre ikke vores tilfælde, vi slukker for det.
- MINWIN_SDK_LIB_PATH – denne værdi henviser til variablen SDK_LIB_PATH, bekymr dig ikke hvis du ikke har denne systemvariabel erklæret; når vi kører checked build fra DDK, vil denne variabel blive erklæret og pege på de nødvendige biblioteker.
- SOURCES – listen over kilder til dit program.
- INCLUDES – headerfiler, der er nødvendige for buildprocessen. Her angiver man typisk stien til de filer, der følger med DDK, men du kan også angive andre filer.
- TARGETLIBS – liste over biblioteker, der skal linkes.
- USE_NTDLL – et obligatorisk felt, der skal indstilles til 1. Af åbenlyse grunde.
- USER_C_FLAGS – alle flags, du kan bruge i præprocessor direktiver under forberedelsen af applikationskoden.
Så for at bygge skal vi starte x86 (eller x64) Checked Build, ændre arbejdsbiblioteket til projektmappen og udføre Build-kommandoen. Resultatet på skærmbilledet viser, at vi har genereret en executabel fil.

Denne fil kan ikke bare startes. Systemet klager og sender os til at overveje dens adfærd med følgende fejl:

Hvordan starter man en native applikation?
Ved opstart af autochk bestemmes rækkefølgen af programmer i opstart via værdien af registreringsnøglen:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteSession manager kører programmerne i denne liste sekventielt. Selve eksekverbare filer findes af session manager i system32 biblioteket. Formatet for registreringsnøgleværdien er som følger:
autocheck autochk *MyNativeVærdien skal være i hexadecimal format og ikke i det sædvanlige ASCII, derfor vil nøglen, som præsenteret ovenfor, have formatet:
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,00For at konvertere navnet kan man bruge en online service, for eksempel, .

Så for at starte en native applikation skal vi:
- Kopiere den eksekverbare fil til system32 biblioteket
- Tilføje en nøgle til registreringsdatabasen
- Genstarte maskinen
For nemheds skyld, her er et færdigt script til installation af den native applikation:
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,00Efter installation og genstart, før skærmen for brugervalg vises, vil vi få følgende billede:

Resultat
Vi har gennem det lille eksempelapplikation set, at det er muligt at køre en applikation på Windows Native-niveau. Fremadrettet vil vi sammen med gutterne fra Innopolis Universitet fortsætte med at udvikle en service, der vil igangsætte processen for interaktion med driveren meget tidligere end i den tidligere version af vores projekt. Med introduktionen af win32-grænsefladen vil det være logisk at overdrage kontrol til en fuldt udviklet service, som allerede er lavet (mere om dette senere. ).
I en kommende artikel vil vi berøre endnu en komponent af Active Restore-tjenesten, nemlig UEFI-driveren. Tilmeld dig vores blog for ikke at gå glip af det næste indlæg.
Kilde: habr.com
