Windows Native Applications och Acronis Active Restore-tjänsten
Idag fortsätter vi historien om hur vi, tillsammans med killarna från Innopolis University, utvecklar Active Restore-teknologin för att användaren ska kunna börja arbeta på sin maskin så snart som möjligt efter ett fel. Vi kommer att prata om inbyggda Windows-applikationer, inklusive funktionerna i deras skapande och lansering. Nedan klippet är lite om vårt projekt, samt en praktisk guide om hur man skriver infödda ansökningar.
I tidigare inlägg har vi redan pratat om vad det är Aktiv återställning, och hur elever från Innopolis utvecklas tjänsten. Idag vill jag fokusera på inbyggda applikationer, till den nivå som vi vill "begrava" vår aktiva återställningstjänst. Om allt fungerar, kommer vi att kunna:
Starta själva tjänsten mycket tidigare
Kontakta molnet där säkerhetskopian finns mycket tidigare
Mycket tidigare för att förstå vilket läge systemet är i - normal start eller återställning
Mycket färre filer att återställa i förväg
Låt användaren komma igång ännu snabbare.
Vad är en inbyggd app egentligen?
För att svara på denna fråga, låt oss titta på sekvensen av samtal som systemet gör, till exempel om en programmerare i hans applikation försöker skapa en fil.
Pavel Yosifovich - Windows Kernel Programmering (2019)
Programmeraren använder funktionen Skapa fil, som deklareras i rubrikfilen fileapi.h och implementeras i Kernel32.dll. Denna funktion skapar dock inte själva filen, den kontrollerar bara inmatningsargumenten och anropar funktionen NtCreateFile (prefixet Nt indikerar bara att funktionen är ursprunglig). Denna funktion deklareras i rubrikfilen winternl.h och implementeras i ntdll.dll. Den förbereder sig för att hoppa in i kärnrymden, varefter den gör ett systemanrop för att skapa en fil. I det här fallet visar det sig att Kernel32 bara är ett omslag för Ntdll. En av anledningarna till att detta gjordes är att Microsoft därmed har möjlighet att ändra funktionerna i den infödda världen, men inte röra standardgränssnitten. Microsoft rekommenderar inte att anropa inbyggda funktioner direkt och dokumenterar inte de flesta av dem. Odokumenterade funktioner kan förresten hittas här.
Den största fördelen med inbyggda applikationer är att ntdll läses in i systemet mycket tidigare än kernel32. Detta är logiskt, eftersom kernel32 kräver att ntdll fungerar. Som ett resultat kan applikationer som använder inbyggda funktioner börja fungera mycket tidigare.
Windows Native Applications är alltså program som kan starta tidigt i Windows-start. De använder ENDAST funktioner från ntdll. Ett exempel på en sådan applikation: autochk som uppträder chkdisk verktyg för att kontrollera disken för fel innan du startar huvudtjänsterna. Det är precis den nivån vi vill att vår Active Restore ska vara.
Vad behöver vi?
DDK (Driver Development Kit), nu även känt som WDK 7 (Windows Driver Kit).
Virtuell maskin (till exempel Windows 7 x64)
Inte nödvändigt, men rubrikfiler som kan laddas ner kan hjälpa här
Vad står i koden?
Låt oss öva lite och till exempel skriva en liten ansökan som:
Visar ett meddelande på skärmen
Tilldelar lite minne
Väntar på tangentbordsinmatning
Frigör använt minne
I inbyggda applikationer är ingångspunkten inte main eller winmain, utan NtProcessStartup-funktionen, eftersom vi faktiskt direkt startar nya processer i systemet.
Låt oss börja med att visa ett meddelande på skärmen. För detta har vi en infödd funktion NtDisplayString, som tar som argument en pekare till ett UNICODE_STRING-strukturobjekt. RtlInitUnicodeString hjälper oss att initiera den. Som ett resultat kan vi skriva denna lilla funktion för att visa text på skärmen:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}
Eftersom endast funktioner från ntdll är tillgängliga för oss, och det helt enkelt inte finns några andra bibliotek i minnet än, kommer vi definitivt att ha problem med hur man allokerar minne. Den nya operatören existerar inte ännu (eftersom den kommer från C++-världen på alltför hög nivå), och det finns ingen malloc-funktion (den kräver runtime C-bibliotek). Naturligtvis kan du bara använda en stack. Men om vi behöver allokera minne dynamiskt måste vi göra det på högen (dvs. högen). Så låt oss skapa en hög för oss själva och ta minnet från den när vi behöver det.
Funktionen är lämplig för denna uppgift RtlCreateHeap. Därefter, med hjälp av RtlAllocateHeap och RtlFreeHeap, kommer vi att ockupera och frigöra minne när vi behöver det.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// create heap in order to allocate memory later
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// allocate buffer of size bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// free buffer (actually not needed because we destroy heap in next step)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);
Låt oss gå vidare till att vänta på tangentbordsinmatning.
Allt vi behöver är att använda NtReadFile på en öppen enhet och vänta tills tangentbordet returnerar någon tryckning till oss. Om du trycker på ESC-tangenten fortsätter vi att arbeta. För att öppna enheten måste vi anropa funktionen NtCreateFile (vi måste öppna DeviceKeyboardClass0). Vi kommer också att ringa NtCreateEventför att initiera vänta-objektet. Vi kommer att deklarera KEYBOARD_INPUT_DATA-strukturen själva, som representerar tangentbordsdata. Detta kommer att underlätta vårt arbete.
Den inbyggda applikationen avslutas med ett funktionsanrop NtTerminateProcesseftersom vi helt enkelt dödar vår egen process.
All kod för vår lilla 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 enkelt använda funktionen DbgBreakPoint() i vår kod för att stoppa den i felsökaren. Det är sant att du måste ansluta WinDbg till en virtuell maskin för kärnfelsökning. Instruktioner om hur du gör detta finns här eller bara använda VirtualKD.
Sammanställning och montering
Det enklaste sättet att bygga en inbyggd applikation är att använda DDK (Driver Development Kit). Vi behöver den gamla sjunde versionen, eftersom senare versioner har ett lite annorlunda tillvägagångssätt och arbetar nära med Visual Studio. Om vi använder DDK behöver vårt projekt bara Makefile och källor.
Makefile
!INCLUDE $(NTMAKEENV)makefile.def
källor:
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 = 1
Din Makefile kommer att vara exakt densamma, men låt oss titta på källorna lite mer detaljerat. Den här filen anger programmets källor (.c-filer), byggalternativ och andra parametrar.
TARGETNAME – namnet på den körbara filen som ska produceras i slutet.
TARGETTYPE – typ av körbar fil, det kan vara en drivrutin (.sys), då ska fältvärdet vara DRIVER, om ett bibliotek (.lib) så är värdet LIBRARY. I vårt fall behöver vi en körbar fil (.exe), så vi ställer in värdet på PROGRAM.
UMTYPE – möjliga värden för detta fält: konsol för en konsolapplikation, fönster för att arbeta i fönsterläge. Men vi måste ange nt för att få en inbyggd applikation.
BUFFER_OVERFLOW_CHECKS – kontrollerar stacken för buffertspill, tyvärr inte vårt fall, vi stänger av det.
MINWIN_SDK_LIB_PATH – detta värde hänvisar till variabeln SDK_LIB_PATH, oroa dig inte för att du inte har en sådan systemvariabel deklarerad, när vi kör checked build från DDK kommer denna variabel att deklareras och pekar på de nödvändiga biblioteken.
KÄLLOR – en lista med källor för ditt program.
INKLUDERAR – header-filer som krävs för montering. Här anger de vanligtvis sökvägen till filerna som följer med DDK, men du kan dessutom ange andra.
TARGETLIBS – lista över bibliotek som behöver länkas.
USE_NTDLL är ett obligatoriskt fält som måste sättas till 1 av uppenbara skäl.
USER_C_FLAGS – alla flaggor som du kan använda i preprocessor-direktiv när du förbereder applikationskod.
Så för att bygga måste vi köra x86 (eller x64) Markerad Build, ändra arbetskatalogen till projektmappen och köra Build-kommandot. Resultatet i skärmdumpen visar att vi har en körbar fil.
Den här filen kan inte startas så lätt, systemet förbannar och skickar oss att tänka på dess beteende med följande fel:
Hur startar man en inbyggd applikation?
När autochk startar, bestäms startsekvensen för program av värdet på registernyckeln:
Sessionshanteraren kör program från denna lista ett efter ett. Sessionshanteraren letar efter de körbara filerna i system32-katalogen. Formatet för registernyckelns värde är följande:
autocheck autochk *MyNative
Värdet måste vara i hexadecimalt format, inte det vanliga ASCII-formatet, så nyckeln som visas ovan kommer att ha formatet:
Efter installation och omstart, även innan skärmen för användarval visas, får vi följande bild:
Totalt
Med exemplet med en så liten applikation var vi övertygade om att det är fullt möjligt att köra applikationen på Windows Native-nivå. Därefter kommer killarna från Innopolis University och jag att fortsätta bygga en tjänst som kommer att initiera processen för interaktion med föraren mycket tidigare än i den tidigare versionen av vårt projekt. Och med tillkomsten av win32-skalet skulle det vara logiskt att överföra kontrollen till en fullfjädrad tjänst som redan har utvecklats (mer om detta här).
I nästa artikel kommer vi att beröra en annan komponent i Active Restore-tjänsten, nämligen UEFI-drivrutinen. Prenumerera på vår blogg så att du inte missar nästa inlägg.