Windows Native Applications en Acronis Active Restore-service
Vandaag vervolgen we het verhaal van hoe we, samen met de jongens van Innopolis University, Active Restore-technologie ontwikkelen zodat de gebruiker na een storing zo snel mogelijk aan zijn machine kan gaan werken. We zullen het hebben over native Windows-applicaties, inclusief de kenmerken van hun creatie en lancering. Hieronder vindt u iets over ons project, evenals een praktische gids over het schrijven van native applicaties.
In eerdere berichten hebben we al verteld wat het is Actief herstel, en hoe studenten van Innopolis zich ontwikkelen service. Vandaag wil ik me concentreren op native applicaties, tot op het niveau waarop we onze actieve herstelservice willen 'begraven'. Als alles lukt, kunnen we:
Start de service zelf veel eerder
Neem veel eerder contact op met de cloud waar de back-up staat
Veel eerder om te begrijpen in welke modus het systeem zich bevindt: normaal opstarten of herstel
Veel minder bestanden die vooraf moeten worden hersteld
Laat de gebruiker nog sneller aan de slag.
Wat is eigenlijk een native app?
Laten we, om deze vraag te beantwoorden, kijken naar de reeks oproepen die het systeem doet, bijvoorbeeld als een programmeur in zijn applicatie een bestand probeert te maken.
Pavel Yosifovich - Windows Kernel-programmering (2019)
De programmeur gebruikt de functie Bestand maken, dat wordt gedeclareerd in het headerbestand fileapi.h en geïmplementeerd in Kernel32.dll. Deze functie maakt het bestand echter niet zelf, maar controleert alleen de invoerargumenten en roept de functie aan NtCreateFile (het voorvoegsel Nt geeft alleen aan dat de functie native is). Deze functie wordt gedeclareerd in het headerbestand winternl.h en geïmplementeerd in ntdll.dll. Het bereidt zich voor om in de nucleaire ruimte te springen, waarna het een systeemoproep doet om een bestand te maken. In dit geval blijkt dat Kernel32 slechts een verpakking voor Ntdll is. Een van de redenen waarom dit werd gedaan, is dat Microsoft dus de mogelijkheid heeft om de functies van de oorspronkelijke wereld te veranderen, maar de standaardinterfaces niet aan te raken. Microsoft raadt af om native functies rechtstreeks aan te roepen en documenteert de meeste daarvan niet. Er zijn overigens ongedocumenteerde functies te vinden hier.
Het belangrijkste voordeel van native applicaties is dat ntdll veel eerder in het systeem wordt geladen dan kernel32. Dit is logisch, omdat kernel32 ntdll nodig heeft om te werken. Als gevolg hiervan kunnen applicaties die native functies gebruiken veel eerder aan de slag.
Windows Native Applications zijn dus programma's die al vroeg tijdens het opstarten van Windows kunnen starten. Ze gebruiken ALLEEN functies van ntdll. Een voorbeeld van zo’n toepassing: autochk wie presteert chkdisk-hulpprogramma om de schijf op fouten te controleren voordat u de hoofdservices start. Dit is precies het niveau dat we willen dat onze Active Restore is.
Wat hebben we nodig?
DDK (Driver Development Kit), nu ook bekend als WDK 7 (Windows Driver Kit).
Virtuele machine (bijvoorbeeld Windows 7 x64)
Niet nodig, maar headerbestanden die kunnen worden gedownload kunnen helpen hier
Wat staat er in de code?
Laten we een beetje oefenen en bijvoorbeeld een kleine applicatie schrijven die:
Geeft een bericht weer op het scherm
Wijst wat geheugen toe
Wacht op toetsenbordinvoer
Maakt gebruikt geheugen vrij
In native applicaties is het toegangspunt niet main of winmain, maar de NtProcessStartup-functie, omdat we feitelijk direct nieuwe processen in het systeem starten.
Laten we beginnen met het weergeven van een bericht op het scherm. Hiervoor hebben we een native functie NtDisplayString, dat als argument een verwijzing naar een UNICODE_STRING-structuurobject heeft. RtlInitUnicodeString helpt ons het te initialiseren. Als gevolg hiervan kunnen we deze kleine functie schrijven om tekst op het scherm weer te geven:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}
Omdat alleen functies van ntdll voor ons beschikbaar zijn, en er simpelweg nog geen andere bibliotheken in het geheugen zijn, zullen we zeker problemen krijgen met het toewijzen van geheugen. De nieuwe operator bestaat nog niet (omdat deze uit de te hoge wereld van C++ komt) en er is geen malloc-functie (er zijn runtime C-bibliotheken voor nodig). Je kunt natuurlijk alleen een stapel gebruiken. Maar als we geheugen dynamisch moeten toewijzen, zullen we dat op de heap (dat wil zeggen heap) moeten doen. Laten we dus een hoop voor onszelf creëren en er herinneringen uit halen wanneer we die nodig hebben.
De functie is geschikt voor deze taak RtlCreateHeap. Vervolgens zullen we met behulp van RtlAllocateHeap en RtlFreeHeap geheugen bezetten en vrijmaken wanneer we het nodig hebben.
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);
Laten we verder gaan met wachten op toetsenbordinvoer.
Het enige wat we nodig hebben is gebruiken NtReadFile op een open apparaat en wacht tot het toetsenbord elke druk op ons teruggeeft. Als de ESC-toets wordt ingedrukt, gaan we verder met werken. Om het apparaat te openen, moeten we de functie NtCreateFile aanroepen (we moeten DeviceKeyboardClass0 openen). Wij zullen ook bellen NtCreateEventom het wait-object te initialiseren. We zullen zelf de KEYBOARD_INPUT_DATA-structuur declareren, die de toetsenbordgegevens vertegenwoordigt. Dit zal ons werk gemakkelijker maken.
De native applicatie eindigt met een functieaanroep NtTerminateProcesomdat we eenvoudigweg ons eigen proces om zeep helpen.
Alle code voor onze kleine applicatie:
#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: We kunnen eenvoudig de functie DbgBreakPoint() in onze code gebruiken om deze in de debugger te stoppen. Het is waar dat u WinDbg moet verbinden met een virtuele machine voor het debuggen van de kernel. Instructies over hoe u dit kunt doen, vindt u hier of gewoon gebruiken VirtueleKD.
Compilatie en montage
De eenvoudigste manier om een native applicatie te bouwen is door gebruik te maken van DDK (Driverontwikkelingskit). We hebben de oude zevende versie nodig, omdat latere versies een iets andere aanpak hebben en nauw samenwerken met Visual Studio. Als we de DDK gebruiken, heeft ons project alleen Makefile en bronnen nodig.
Makefile
!INCLUDE $(NTMAKEENV)makefile.def
bronnen:
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
Je Makefile zal precies hetzelfde zijn, maar laten we de bronnen wat gedetailleerder bekijken. Dit bestand specificeert de bronnen van uw programma (.c-bestanden), build-opties en andere parameters.
TARGETNAME – de naam van het uitvoerbare bestand dat uiteindelijk moet worden geproduceerd.
TARGETTYPE – type uitvoerbaar bestand, het kan een stuurprogramma (.sys) zijn, dan moet de veldwaarde DRIVER zijn, als het een bibliotheek (.lib) is, dan is de waarde LIBRARY. In ons geval hebben we een uitvoerbaar bestand (.exe) nodig, dus stellen we de waarde in op PROGRAM.
UMTYPE – mogelijke waarden voor dit veld: console voor een consoletoepassing, vensters voor werken in venstermodus. Maar we moeten nt specificeren om een native applicatie te krijgen.
BUFFER_OVERFLOW_CHECKS – de stapel controleren op bufferoverflow, helaas niet ons geval, we zetten het uit.
MINWIN_SDK_LIB_PATH – deze waarde verwijst naar de SDK_LIB_PATH variabele. Maak je geen zorgen dat je zo’n systeemvariabele niet hebt gedeclareerd. Wanneer we een gecontroleerde build uitvoeren vanuit de DDK, wordt deze variabele gedeclareerd en verwijst deze naar de benodigde bibliotheken.
BRONNEN – een lijst met bronnen voor uw programma.
INCLUSIEF – headerbestanden die nodig zijn voor montage. Hier geven ze meestal het pad aan naar de bestanden die bij de DDK worden geleverd, maar u kunt ook eventuele andere opgeven.
TARGETLIBS – lijst met bibliotheken die moeten worden gekoppeld.
USE_NTDLL is een verplicht veld dat om voor de hand liggende redenen op 1 moet worden ingesteld.
USER_C_FLAGS – alle vlaggen die u kunt gebruiken in preprocessor-richtlijnen bij het voorbereiden van applicatiecode.
Om te bouwen moeten we dus x86 (of x64) Checked Build uitvoeren, de werkmap wijzigen in de projectmap en de opdracht Build uitvoeren. Het resultaat in de schermafbeelding laat zien dat we één uitvoerbaar bestand hebben.
Dit bestand kan niet zo gemakkelijk worden gestart, het systeem vloekt en stuurt ons om na te denken over zijn gedrag met de volgende foutmelding:
Hoe start u een native applicatie?
Wanneer autochk start, wordt de opstartvolgorde van programma's bepaald door de waarde van de registersleutel:
De sessiemanager voert programma's uit deze lijst één voor één uit. De sessiebeheerder zoekt zelf naar de uitvoerbare bestanden in de map system32. Het formaat van de registersleutelwaarde is als volgt:
autocheck autochk *MyNative
De waarde moet een hexadecimaal formaat hebben, niet de gebruikelijke ASCII, dus de hierboven getoonde sleutel heeft het formaat:
Na installatie en opnieuw opstarten, nog voordat het gebruikersselectiescherm verschijnt, krijgen we het volgende beeld:
Totaal
Aan de hand van het voorbeeld van zo'n kleine applicatie waren we ervan overtuigd dat het heel goed mogelijk is om de applicatie op Windows Native-niveau te draaien. Vervolgens zullen de jongens van Innopolis University en ik doorgaan met het bouwen van een service die het interactieproces met de bestuurder veel eerder zal initiëren dan in de vorige versie van ons project. En met de komst van de win32-shell zou het logisch zijn om de controle over te dragen aan een volwaardige service die al is ontwikkeld (meer hierover hier).
In het volgende artikel zullen we ingaan op een ander onderdeel van de Active Restore-service, namelijk het UEFI-stuurprogramma. Abonneer je op onze blog, zodat je het volgende bericht niet mist.