I dag fortsetter vi historien om hvordan vi sammen med guttene fra Innopolis Universitet utvikler Active Restore-teknologien for å la brukeren begynne å jobbe på maskinen sin så raskt som mulig etter et crash. Vi skal snakke om nativ applikasjoner for Windows, inkludert spesifikasjoner for deres opprettelse og kjøring. Under vil det være litt om prosjektet vårt, samt en praktisk veiledning for hvordan man skriver native applikasjoner.

I tidligere innlegg har vi allerede snakket om hva er, og hvordan studentene fra Innopolis utvikler den. I dag vil jeg fokusere på nativ applikasjoner, det nivået vi ønsker å "grave ned" tjenesten for aktiv gjenoppretting. Hvis alt går etter planen, vil vi kunne: Starte selve tjenesten mye tidligere
- Koble til skyen som inneholder backupen mye tidligere
- Forstå mye tidligere hvilken modus systemet er i – normal oppstart eller gjenoppretting
- Restaurere mye færre filer på forhånd
- La brukeren begynne å jobbe enda raskere.
- Hva er egentlig en nativ applikasjon?
For å svare på dette spørsmålet, la oss se på sekvensen av anrop som systemet gjør, for eksempel hvis programmereren i sin applikasjon prøver å opprette en fil.
Pavel Yosifovich – Windows Kernel Programming (2019)

Programmereren bruker funksjonen
CreateFile NtCreateFile (prefiksen Nt viser nettopp at funksjonen er native). Denne funksjonen er erklært i header-filen winternl.h og er implementert i ntdll.dll. Den forbereder et sprang inn i kjernesonen, hvoretter den gjør et systemanrop for å opprette en fil. I dette tilfellet viser det seg at Kernel32 bare er et lag rundt Ntdll. En av grunnene til at dette gjøres, er at Microsoft på denne måten kan endre funksjonene i den native verden, men samtidig ikke berøre de standardgrensesnittene. Microsoft anbefaler ikke å kalle på native funksjoner direkte, og dokumenterer ikke det meste av dem. Forresten kan udokumenterte funksjoner finnes .
Det viktigste med native applikasjoner er at ntdll lastes inn i systemet betydelig før kernel32. Dette gir mening, ettersom kernel32 krever ntdll for å fungere. Som følge av dette kan applikasjoner som bruker native funksjoner starte opp betydelig tidligere.
Dermed er Windows Native Applications programmer som kan starte i de tidlige fasene av Windows-oppstarten. De bruker KUN funksjoner fra ntdll. Et eksempel på en slik applikasjon: som kjører for å sjekke disken for feil før de viktigste tjenestene starter. På akkurat dette nivået ønsker vi å se vår Active Restore.
Hva trenger vi?
- (Driver Development Kit), nå også kjent som WDK 7 (Windows Driver Kit).
- En virtuell maskin (for eksempel Windows 7 x64)
- Ikke nødvendig, men kan være nyttig med header-filer som kan lastes ned
Hva er i koden?
La oss trene litt og skrive et lite program som:
- Viser en melding på skjermen
- Allokerer litt minne
- Venter på input fra tastaturet
- Frigir allokert minne
I native applikasjoner er ikke inngangspunktet main eller winmain, men funksjonen NtProcessStartup, ettersom vi faktisk starter nye prosesser direkte i systemet.
La oss begynne med å vise en melding på skjermen. For dette har vi en native funksjon , som tar en peker til et UNICODE_STRING struktur som argument. Vi kan bruke RtlInitUnicodeString for å initialisere den. Som et resultat kan vi skrive en liten funksjon for å vise tekst på skjermen:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Siden vi bare har tilgang til funksjoner fra ntdll, og ingen andre biblioteker er lastet inn i minnet, vil vi definitivt støte på problemer med hvordan vi kan allokere minne. New-operatøren eksisterer ikke ennå (fordi den kommer fra en for høy nivå verden av C++), og funksjonen malloc finnes heller ikke (den krever C runtime-biblioteker). Vi kan selvfølgelig bruke bare stakken. Men hvis vi må allokere minne dynamisk, må vi gjøre det i heapen. Derfor la oss opprette en heap for oss selv og ta minne fra den når vi trenger det.
For denne oppgaven vil funksjonen være passende. Videre, ved å bruke RtlAllocateHeap og RtlFreeHeap, vil vi allokere og frigjøre minne når vi trenger det.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// opprett heap for å allokere minne senere
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// allokere buffer med størrelse bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// frigjør buffer (faktisk ikke nødvendig, fordi vi ødelegger heap i neste trinn)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);La oss forvente inndata 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 vi trenger er å bruke på den åpne enheten, og vente til tastaturet returnerer et tastetrykk. I tilfelle ESC-tasten trykkes, vil vi fortsette å jobbe. For å åpne enheten, må vi kalle funksjonen NtCreateFile (vi må åpne DeviceKeyboardClass0). Vi vil også kalle , for å initialisere et objekt for venting. Vi vil selv erklære strukturen KEYBOARD_INPUT_DATA, som representerer tastaturdata. Dette vil gjøre jobben vår enklere.
Arbeidet til det native programmet avsluttes med å kalle funksjonen , fordi vi bare dreper vår egen prosess.
Hele koden til vår lille applikasjon:
#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 enkelt bruke funksjonen DbgBreakPoint() i koden for å stoppe i debuggeren. Du må imidlertid koble WinDbg til den virtuelle maskinen for kjernedebugging. Instruksjoner om hvordan du gjør dette kan finnes eller bare bruke .
Kompilering og bygging
Den enkleste måten å bygge en native program på er å bruke (Driver Development Kit). Vi trenger faktisk den gamle syvende versjonen, siden nyere versjoner har en litt annen tilnærming og jobber tett med Visual Studio. Hvis du bruker DDK, trenger prosjektet vårt bare Makefile og 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 = 1Ditt Makefile vil være akkurat det samme, når det gjelder sources, la oss stoppe litt opp. I denne filen angir du kildefilene til programmet ditt (filer .c), byggalternativer og andre parametere.
- TARGETNAME – navnet på den kjørbare filen som skal oppnås til slutt.
- TARGETTYPE – typen av den kjørbare filen, dette kan være en driver (.sys), da skal verdien være DRIVER, hvis det er et bibliotek (.lib), så skal verdien være LIBRARY. I vårt tilfelle trenger vi en kjørbar fil (.exe), så vi setter verdien til PROGRAM.
- UMTYPE – mulige verdier for dette feltet: console for konsollapplikasjoner, windows for vindusmodus. Men vi må angi nt for å få en nativer applikasjon.
- BUFFER_OVERFLOW_CHECKS – sjekk av stakken for bufferoverløp, dessverre ikke vårt tilfelle, vi slår av.
- MINWIN_SDK_LIB_PATH – denne verdien refererer til variabelen SDK_LIB_PATH, det er ikke nødvendig å bekymre seg hvis denne systemvariabelen ikke er erklært, når vi kjører en checked build fra DDK, vil denne variabelen bli erklært og peke på nødvendige biblioteker.
- SOURCES – listen over kildekodene til programmet ditt.
- INCLUDES – headerfiler som er nødvendige for bygging. Vanligvis angis stien til filene som følger med DDK, men du kan også spesifisere tillegg andre.
- TARGETLIBS – listen over biblioteker som må lenkes.
- USE_NTDLL – et obligatorisk felt som må settes til 1. Av åpenbare grunner.
- USER_C_FLAGS – hvilke som helst flagg du kan bruke i preprossesordirektiver når du forbereder applikasjonskoden.
Så for å bygge må vi kjøre x86 (eller x64) Checked Build, endre arbeidskatalogen til mappen med prosjektet og utføre Build-kommandoen. Resultatet på skjermbildet viser at vi har bygget en kjørbar fil.

Denne filen kan ikke enkelt kjøres, systemet klager og sender oss til å tenke over oppførselen sin med følgende feil:

Hvordan kjøre en nativer applikasjon?
Ved oppstart bestemmes rekkefølgen for kjøring av programmene av verdien til registreringsnøkkelen:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteSessionsadministratoren kjører programmene fra denne listen i rekkefølge. De kjørbare filene leter sessionsadministratoren etter i system32-katalogen. Formatet til registreringsnøkkelverdien er som følger:
autocheck autochk *MyNativeVerdien må være i heksadesimal format, ikke det vanlige ASCII, derfor vil nøkkelen som angitt ovenfor ha format:
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 å konvertere navnet kan man bruke en nettjeneste, for eksempel, .

Så for å kjøre en nativer applikasjon, må vi:
- Kopiere den kjørbare filen til system32-mappen
- Legge til en nøkkel i registeret
- Starte maskinen på nytt
For enkelhets skyld her er et klart skript for å installere den nativer applikasjonen:
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,00Etter installasjonen og omstarten, før valgskjermen for brukere vises, vil vi få følgende bilde:

Resultat
Ved hjelp av et så lite program har vi vist at det er fullt mulig å kjøre programmet på Windows Native-nivå. Videre vil vi med gutta fra Universitetet Innopolis fortsette å bygge en tjeneste som vil starte interaksjonen med driveren mye tidligere enn i den forrige versjonen av prosjektet vårt. Når win32-grensesnittet blir tilgjengelig, vil det logisk være å overføre kontrollen til en fullverdig tjeneste som allerede er utviklet (mer om dette i neste artikkel). ).
I en kommende artikkel vil vi ta for oss en annen komponent av tjenesten Active Restore, nemlig UEFI-driveren. Abonner på bloggen vår, så du ikke går glipp av neste innlegg.
Kilde: habr.com
