Windows Native Applications en de Acronis Active Restore-service

Vandaag gaan we verder met het verhaal over hoe we samen met onze vrienden van de Innopolis Universiteit de Active Restore-technologie ontwikkelen, zodat gebruikers zo snel mogelijk weer aan de slag kunnen na een storing. We zullen het hebben over native Windows-applicaties, inclusief de bijzonderheden bij hun ontwikkeling en uitvoering. Hieronder volgt wat informatie over ons project, evenals een praktische handleiding voor het schrijven van native applicaties.

Windows Native Applications en de Acronis Active Restore-service

In eerdere berichten hebben we al verteld wat Active Restoreis, en hoe studenten van Innopolis het ontwikkelen. Vandaag wil ik me richten op native applicaties, de niveaus waar we onze actieve herstelservice naar toe willen «graven». Als alles goed gaat, kunnen we: serviceVeel eerder de service zelf starten

  • Veel eerder contact maken met de cloud waar de backup ligt
  • Veel eerder begrijpen in welke modus het systeem zich bevindt – normale opstart of herstel
  • Veel minder bestanden vooraf herstellen
  • De gebruiker nog sneller laten beginnen met werken.
  • Wat is eigenlijk een native applicatie?

Om deze vraag te beantwoorden, laten we kijken naar de reeks aanroepen die het systeem doet, bijvoorbeeld als een programmeur in zijn applicatie probeert een bestand te maken.

Pavel Yosifovich — Windows Kernel Programming (2019)

Windows Native Applications en de Acronis Active Restore-service
De programmeur gebruikt de functie

CreateFile , die is gedeclareerd in de header file fileapi.h en geïmplementeerd in Kernel32.dll. Echter, deze functie is niet verantwoordelijk voor het maken van bestanden; het controleert alleen de argumenten en roept de functie aanNtCreateFile (de prefix Nt getuigt er juist van dat de functie native is). Deze functie is gedeclareerd in de header file winternl.h en geïmplementeerd in ntdll.dll. Het bereidt zich voor om naar de kernelruimte te springen, waarna het een systeemaanroep doet om een bestand te maken. In dit geval is Kernel32 slechts een wrapper voor Ntdll. Een van de redenen waarom dit zo is gedaan, is dat Microsoft op deze manier de functies van de native wereld kan wijzigen zonder de standaardinterfaces aan te raken. Microsoft raadt aan om native functies niet rechtstreeks aan te roepen en documenteert de meeste daarvan niet. Trouwens, je kunt ongedocumenteerde functies vinden. (de Nt-prefix geeft precies aan dat het een native functie is). Deze functie is gedefinieerd in de header-bestand winternl.h en wordt geïmplementeerd in ntdll.dll. Het bereidt de sprong naar de kernelruimte voor en doet vervolgens een systeemaanroep om een bestand te creëren. In dit geval blijkt dat Kernel32 slechts een wrapper is voor Ntdll. Een van de redenen hiervoor is dat Microsoft op deze manier in staat is om native functies te wijzigen zonder de standaardinterfaces aan te tasten. Microsoft raadt aan om native functies niet rechtstreeks aan te roepen en documenteert het merendeel van deze functies niet. Overigens kunnen niet-gedocumenteerde functies worden gevonden. here.

Het belangrijkste voordeel van native applicaties is dat ntdll veel eerder in het systeem wordt geladen dan kernel32. Dit is logisch, aangezien kernel32 afhankelijk is van ntdll om te functioneren. Als gevolg hiervan kunnen applicaties die gebruikmaken van native functies veel eerder beginnen met draaien.

Windows Native Applications zijn programma's die in staat zijn om in een vroeg stadium van het Windows-opstartproces te worden uitgevoerd. Ze gebruiken ENKEL functies uit ntdll. Een voorbeeld van zo'n applicatie: autochk dat uitvoert de chkdisk utility om de schijf op fouten te controleren nog vóór de hoofdservices worden gestart. Dit is het niveau waarop we onze Active Restore willen zien.

Wat hebben we nodig?

  • DDK (Driver Development Kit), tegenwoordig ook bekend als WDK 7 (Windows Driver Kit).
  • Een virtuele machine (bijvoorbeeld, Windows 7 x64)
  • Niet verplicht, maar kan helpen, headerbestanden die je kunt downloaden here

Wat is er in de code?

Laten we een beetje oefenen en ter illustratie een kleine applicatie schrijven die:

  1. Een boodschap op het scherm weergeeft
  2. Een beetje geheugen toewijst
  3. Wacht op invoer van het toetsenbord
  4. Het toegewezen geheugen vrijgeeft

In native applicaties is het toetspunt niet main of winmain, maar de functie NtProcessStartup, aangezien we feitelijk direct nieuwe processen in het systeem lanceren.

Laten we beginnen met het weergeven van een boodschap op het scherm. Hiervoor hebben we de native functie NtDisplayString, die als argument een pointer naar een object van de structuur UNICODE_STRING accepteert. We kunnen het hierin initialiseren met RtlInitUnicodeString. Als resultaat kunnen we voor het weergeven van tekst op het scherm de volgende kleine functie schrijven:

//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
    UNICODE_STRING string;
    RtlInitUnicodeString(&string, Message);
    NtDisplayString(&string);
}

Aangezien alleen functies uit ntdll beschikbaar zijn en andere bibliotheken gewoonweg nog niet in het geheugen zijn, zullen we onvermijdelijk problemen ondervinden bij het toewijzen van geheugen. De operator new bestaat nog niet (omdat deze afkomstig is uit de te hoog niveau wereld van C++), en de functie malloc is er ook niet (want deze heeft runtime C-bibliotheken nodig). We kunnen natuurlijk alleen de stack gebruiken. Maar als we dynamisch geheugen willen toewijzen, zullen we dit in de heap moeten doen. Laten we dus een heap voor ons maken en daaruit geheugen nemen wanneer we het nodig hebben.

Voor deze taak is de functie RtlCreateHeapgeschikt. Vervolgens kunnen we met RtlAllocateHeap en RtlFreeHeap geheugen toewijzen en vrijgeven wanneer dat nodig is.

PVOID geheugen = NULL;
PVOID buffer = NULL;
ULONG bufferGrootte = 42;

// maak heap aan om later geheugen toe te wijzen
geheugen = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// wijs buffer toe van grootte bufferGrootte
buffer = RtlAllocateHeap(
  geheugen, 
  HEAP_ZERO_MEMORY, 
  bufferGrootte
);

// vrijgeven van buffer (eigenlijk niet nodig omdat we heap in de volgende stap vernietigen)
RtlFreeHeap(geheugen, 0, buffer);

RtlDestroyHeap(geheugen);

Laten we overgaan naar het wachten op invoer vanaf het toetsenbord.

// 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;
	}
}

Alles wat we nodig hebben is om NtReadFile op het geopende apparaat te gebruiken, en te wachten totdat het toetsenbord ons een toetsdruk retourneert. Als de ESC-toets is ingedrukt, gaan we verder met onze taak. Om het apparaat te openen, moeten we de functie NtCreateFile aanroepen (we moeten DeviceKeyboardClass0 openen). Daarnaast zullen we NtCreateEvent, aanroepen om een object voor het wachten te initialiseren. We moeten zelf de structuur KEYBOARD_INPUT_DATA definiëren, die de toetsgegevens voorstelt. Dit zal ons werk vergemakkelijken.

De uitvoering van de native toepassing eindigt met de aanroep van de functie NtTerminateProcess, omdat we gewoon ons eigen proces beëindigen.

De volledige code van onze kleine toepassing:

#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 de code gebruiken om te stoppen in de debugger. Echter, we moeten WinDbg aan de virtuele machine verbinden voor kernel-debugging. Instructies hierover zijn te vinden here of gewoon gebruik maken van VirtualKD.

Compilatie en opbouw

De eenvoudigste manier om een native applicatie samen te stellen is door gebruik te maken van DDK (Driver Development Kit). We hebben precies de oude zevende versie nodig, omdat latere versies een iets andere benadering hebben en nauw samenwerken met Visual Studio. Als we DDK gebruiken, heeft ons project alleen Makefile en sources nodig.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

sources:

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

Jouw Makefile zal precies hetzelfde zijn; laten we echter iets dieper ingaan op sources. In dit bestand worden de bronnen van je programma aangegeven (bestanden .c), compilatie-opties en andere parameters.

  • TARGETNAME – de naam van het uitvoerbare bestand dat uiteindelijk moet worden gemaakt.
  • TARGETTYPE – het type uitvoerbare bestand, dit kan een stuurprogramma (.sys) zijn, in dat geval moet de waarde DRIVER zijn; als het een bibliotheek (.lib) is, dan moet de waarde LIBRARY zijn. In ons geval hebben we een uitvoerbaar bestand (.exe) nodig, dus we stellen de waarde PROGRAM in.
  • UMTYPE – mogelijke waarden voor dit veld: console voor een console-applicatie, windows voor werken in venstermodus. Maar we moeten nt opgeven om een native applicatie te verkrijgen.
  • BUFFER_OVERFLOW_CHECKS – controle van de stapel op bufferoverschrijding, helaas is dit niet ons geval, we schakelen het uit.
  • MINWIN_SDK_LIB_PATH – deze waarde verwijst naar de variabele SDK_LIB_PATH, maak je geen zorgen dat je deze systeemvariabele niet hebt gedeclareerd, op het moment dat we de gecontroleerde build uit DDK starten, zal deze variabele worden gedeclareerd en naar de benodigde bibliotheken verwijzen.
  • SOURCES – lijst van de bronnen van jouw programma.
  • INCLUDES – headerbestanden die nodig zijn voor de build. Hier geef je meestal het pad op naar de bestanden die bij DDK worden geleverd, maar je kunt ook andere bestande opgeven.
  • TARGETLIBS – lijst van bibliotheken die gelinkt moeten worden.
  • USE_NTDLL – verplicht veld dat op 1 moet worden ingesteld. Om voor de hand liggende redenen.
  • USER_C_FLAGS – elk vlag die je kunt gebruiken in preprocessordirectieven tijdens de voorbereiding van de applicatiecode.

Dus voor de build moeten we een x86 (of x64) Checked Build uitvoeren, de werkdirectory naar de projectmap wijzigen en de Build-opdracht uitvoeren. Het resultaat op de screenshot toont aan dat er één uitvoerbaar bestand is opgebouwd.

Windows Native Applications en de Acronis Active Restore-service

Dit bestand kan niet zo eenvoudig worden uitgevoerd, het systeem klaagt en stuurt ons om na te denken over ons gedrag met de volgende fout:

Windows Native Applications en de Acronis Active Restore-service

Hoe start je een native applicatie?

Bij de opstart bepaalt de autochk de volgorde waarin programma's worden gestart, op basis van de waarde van de registersleutel:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

De sessiemanager voert de programma's uit in deze lijst één voor één. De uitvoerbare bestanden zoekt de sessiemanager in de directory system32. Het formaat van de waarde van de registersleutel is als volgt:

autocheck autochk *MyNative

De waarde moet in hexadecimaal formaat zijn, en niet in het gebruikelijke ASCII, daarom zal de sleutel die hierboven is gepresenteerd er als volgt uitzien:

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,00

Om de naam te converteren, kun je een online service gebruiken, bijvoorbeeld: deze.

Windows Native Applications en de Acronis Active Restore-service
Dus, om een native applicatie te starten, moeten we:

  1. Het uitvoerbare bestand naar de map system32 kopiëren.
  2. Een sleutel aan het register toevoegen.
  3. De machine herstarten.

Voor de eenvoud, hier is een kant-en-klaar script voor het installeren van de native applicatie:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Voorbeeld Geïnstalleerd
pauze

add.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,00

Na de installatie en herstart, nog vóór het verschijnen van het scherm voor gebruikerskeuze, zullen we de volgende afbeelding zien:

Windows Native Applications en de Acronis Active Restore-service

Conclusie

Aan de hand van deze kleine applicatie hebben we bevestigd dat het mogelijk is om een applicatie op Windows Native-niveau te starten. Vervolgens zullen we met de jongens van de Universiteit Innopolis doorgaan met het bouwen van de service, die het proces van interactie met de stuurprogramma veel eerder zal opstarten dan in de vorige versie van ons project. Met de introductie van de win32-interface zal het logisch zijn om de controle over te dragen aan een volledig ontworpen service, waarover we meer zullen vertellen. hier).

In de volgende artikel zullen we een ander onderdeel van de Active Restore-service bespreken, namelijk de UEFI-stuurprogramma. Volg onze blog om de volgende post niet te missen.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster