Windows Native Applications und der Dienst Acronis Active Restore

Heute setzen wir unsere Erzählung darüber fort, wie wir gemeinsam mit den Kollegen von der Universität Innopolis die Technologie Active Restore entwickeln, um dem Benutzer zu ermöglichen, so schnell wie möglich nach einem Ausfall mit seiner Maschine zu arbeiten. Es geht um nativ entwickelte Windows-Anwendungen, einschließlich der Besonderheiten ihrer Erstellung und Ausführung. Unter dem Artikel finden Sie einige Informationen zu unserem Projekt sowie eine praktische Anleitung, wie man native Anwendungen entwickelt.

Windows Native Applications und der Dienst Acronis Active Restore

In früheren Beiträgen haben wir bereits erklärt, was Active Restoreist und wie die Studierenden aus Innopolis daran arbeiten. DienstHeute möchte ich auf native Anwendungen eingehen, auf die wir unseren aktiven Wiederherstellungsdienst entwickeln wollen. Wenn alles gut geht, können wir:

  • Den Dienst viel früher starten
  • Viel früher mit der Cloud verbinden, in der das Backup liegt
  • Viel früher verstehen, in welchem Modus sich das System befindet – im normalen Boot oder im Wiederherstellungsmodus
  • Viel weniger Dateien im Voraus wiederherstellen
  • Und dem Benutzer ermöglichen, noch schneller mit der Arbeit zu beginnen.

Was ist überhaupt eine native Anwendung?

Um diese Frage zu beantworten, werfen wir einen Blick auf die Abfolge von Aufrufen, die das System vornimmt, wenn ein Entwickler in seiner Anwendung versucht, eine Datei zu erstellen.

Windows Native Applications und der Dienst Acronis Active Restore
Pavel Yosifovich — Windows Kernel Programmierung (2019)

Der Entwickler verwendet die Funktion CreateFile, die in der Header-Datei fileapi.h deklariert und in Kernel32.dll implementiert ist. Diese Funktion selbst erstellt jedoch keine Datei, sondern prüft lediglich die Eingangsargumente und ruft die Funktion NtCreateFile (das Präfix Nt deutet darauf hin, dass es sich um eine nativen Funktion handelt) auf. Diese Funktion ist in der Header-Datei winternl.h deklariert und in ntdll.dll implementiert. Sie bereitet den Sprung in den Kernelmodus vor und führt anschließend den Systemaufruf zur Dateierstellung aus. In diesem Fall ist Kernel32 nur eine Schnittstelle für Ntdll. Einer der Gründe dafür ist, dass Microsoft auf diese Weise die nativen Funktionen ändern kann, ohne die standardisierten Schnittstellen zu beeinträchtigen. Microsoft empfiehlt nicht, native Funktionen direkt aufzurufen, und dokumentiert die meisten von ihnen nicht. Übrigens, undokumentierte Funktionen sind zu finden. hier.

Ein wesentliches Merkmal nativer Anwendungen besteht darin, dass ntdll erheblich früher in das System geladen wird als kernel32. Das ist sinnvoll, da kernel32 ntdll für seine Funktionalität benötigt. Dadurch können Anwendungen, die native Funktionen nutzen, viel früher starten.

Windows Native Applications sind also Programme, die in der Lage sind, in einem frühen Ladeprozess von Windows gestartet zu werden. Sie verwenden NUR Funktionen aus ntdll. Ein Beispiel für eine solche Anwendung ist: autochk das die chkdisk utility ausführt, um die Festplatte noch vor dem Start der Hauptdienste auf Fehler zu überprüfen. Genau auf diesem Niveau möchten wir unser Active Restore sehen.

Was benötigen wir?

  • DDK (Driver Development Kit), das heute auch als WDK 7 (Windows Driver Kit) bekannt ist.
  • Eine virtuelle Maschine (z. B. Windows 7 x64)
  • nicht zwingend erforderlich, aber Header-Dateien, die heruntergeladen werden können, könnten hilfreich sein. hier

Was ist im Code?

Lass uns ein wenig üben und als Beispiel eine kleine Anwendung schreiben, die:

  1. eine Nachricht auf dem Bildschirm ausgibt
  2. ein wenig Speicher alloziert
  3. auf Eingaben von der Tastatur wartet
  4. den belegten Speicher freigibt

In nativen Anwendungen ist der Einstiegspunkt nicht main oder winmain, sondern die Funktion NtProcessStartup, da wir im Grunde neue Prozesse direkt im System starten.

Beginnen wir damit, eine Nachricht auf dem Bildschirm auszugeben. Dazu verwenden wir die native Funktion NtDisplayString, die als Argument einen Zeiger auf das UNICODE_STRING-Strukturobjekt erwartet. RtlInitUnicodeString hilft uns, es zu initialisieren. Somit können wir zur Ausgabe von Text auf dem Bildschirm eine kleine Funktion schreiben:

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

Da uns nur die Funktionen aus ntdll zur Verfügung stehen und andere Bibliotheken im Speicher einfach noch nicht vorhanden sind, werden wir auf Probleme stoßen, wie wir Speicher allokieren. Der new-Operator existiert noch nicht (da er aus der zu hochabstrakten Welt von C++ stammt), ebenso gibt es keine malloc-Funktion (da sie Runtime-Bibliotheken benötigt). Man könnte natürlich nur den Stack nutzen. Wenn wir jedoch dynamisch Speicher allokieren müssen, müssen wir dies im Heap tun. Daher sollten wir einen Heap für uns erstellen und dort Speicher entnehmen, wenn wir ihn benötigen.

Für diese Aufgabe eignet sich die Funktion RtlCreateHeap. Danach werden wir mit RtlAllocateHeap und RtlFreeHeap Speicher belegen und freigeben, wenn wir ihn benötigen.

PVOID speicher = NULL;
PVOID pufferspeicher = NULL;
ULONG pufferspeicherGroesse = 42;

// Haufen erstellen, um später Speicher zuzuweisen
speicher = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// Puffer der Größe pufferspeicherGroesse zuweisen
pufferspeicher = RtlAllocateHeap(
  speicher, 
  HEAP_ZERO_MEMORY, 
  pufferspeicherGroesse
);

// Puffer freigeben (eigentlich nicht nötig, da wir den Haufen im nächsten Schritt zerstören)
RtlFreeHeap(speicher, 0, pufferspeicher);

RtlDestroyHeap(speicher);

Kommen wir nun zum Warten auf die Eingabe von der Tastatur.

// 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, was wir benötigen, ist die Nutzung von NtReadFile auf dem geöffneten Gerät und zu warten, bis die Tastatur uns eine Taste zurückgibt. Falls die ESC-Taste gedrückt wird, setzen wir unsere Arbeit fort. Um das Gerät zu öffnen, müssen wir die Funktion NtCreateFile aufrufen (DeviceKeyboardClass0 muss geöffnet werden). Zudem werden wir NtCreateEvent, um ein Objekt zum Warten zu initialisieren. Wir werden selbst eine Struktur KEYBOARD_INPUT_DATA deklarieren, die die Tastaturdaten darstellt. Dies wird unsere Arbeit erleichtern.

Die Arbeit der nativen Anwendung wird mit dem Aufruf der Funktion NtTerminateProcess, abgeschlossen, da wir einfach unseren eigenen Prozess beenden.

Der gesamte Code unserer kleinen Anwendung:

#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: Wir können einfach die Funktion DbgBreakPoint() im Code verwenden, um im Debugger anzuhalten. Dazu muss WinDbg mit der virtuellen Maschine für die Kernel-Debugging verbunden werden. Eine Anleitung, wie man das macht, findet man hier oder einfach VirtualKD.

Kompilierung und Erstellung

Der einfachste Weg, eine native Anwendung zu erstellen, ist die Verwendung von DDK (Driver Development Kit). Wir benötigen unbedingt die alte siebte Version, da spätere Versionen einen etwas anderen Ansatz haben und eng mit Visual Studio zusammenarbeiten. Wenn man jedoch DDK verwendet, benötigt unser Projekt lediglich ein Makefile und die Quellcodes.

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

Ihr Makefile wird genau so aussehen, aber lassen Sie uns etwas ausführlicher über die Quellen sprechen. In dieser Datei werden die Quellcodes Ihres Programms (Dateien .c), die Build-Optionen und andere Parameter angegeben.

  • TARGETNAME – der Name der ausführbaren Datei, die letztendlich entstehen soll.
  • TARGETTYPE – der Typ der ausführbaren Datei; das kann ein Treiber (.sys) sein, in diesem Fall sollte der Wert DRIVER sein. Wenn es sich um eine Bibliothek (.lib) handelt, sollte der Wert LIBRARY sein. In unserem Fall benötigen wir eine ausführbare Datei (.exe), daher setzen wir den Wert PROGRAM.
  • UMTYPE – mögliche Werte für dieses Feld: console für Konsolenanwendungen, windows für den Betrieb im Fenstermodus. Wir müssen jedoch nt angeben, um eine native Anwendung zu erhalten.
  • BUFFER_OVERFLOW_CHECKS – Überprüfung des Stacks auf Pufferüberläufe, leider nicht unser Fall, daher deaktivieren wir es.
  • MINWIN_SDK_LIB_PATH – dieser Wert verweist auf die Variable SDK_LIB_PATH. Machen Sie sich keine Sorgen, wenn diese Systemvariable bei Ihnen nicht deklariert ist; sobald wir den Checked Build aus dem DDK starten, wird diese Variable deklariert und auf die erforderlichen Bibliotheken zeigen.
  • SOURCES – die Liste der Quellcodes Ihrer Anwendung.
  • INCLUDES – Header-Dateien, die für den Build erforderlich sind. Hier gibt man normalerweise den Pfad zu den Dateien an, die im Lieferumfang des DDK enthalten sind, aber Sie können zusätzlich auch andere angeben.
  • TARGETLIBS – die Liste der Bibliotheken, die verlinkt werden müssen.
  • USE_NTDLL – ein erforderliches Feld, das auf 1 gesetzt werden muss. Aus gut verständlichen Gründen.
  • USER_C_FLAGS – alle Flags, die Sie in den Präprozessoranweisungen beim Vorbereiten des Anwendungscodes verwenden können.

Um zu kompilieren, müssen wir ein x86 (oder x64) Checked Build starten, das Arbeitsverzeichnis auf den Projektordner wechseln und den Befehl Build ausführen. Das Ergebnis im Screenshot zeigt, dass wir eine ausführbare Datei kompiliert haben.

Windows Native Applications und der Dienst Acronis Active Restore

Diese Datei kann nicht einfach gestartet werden; das System meldet einen Fehler und fordert uns auf, über unser Verhalten nachzudenken:

Windows Native Applications und der Dienst Acronis Active Restore

Wie startet man eine native Anwendung?

Zum Zeitpunkt des Starts bestimmt der Wert des Registrierungsschlüssels die Startreihenfolge der Programme:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Der Sitzungsmanager führt die Programme in dieser Reihenfolge aus. Die ausführbaren Dateien sucht der Sitzungsmanager im Verzeichnis system32. Das Format des Wertes des Registrierungsschlüssels ist wie folgt:

autocheck autochk *MyNative

Der Wert muss im hexadezimalen Format vorliegen und nicht im gewohnten ASCII, daher hat der oben genannte Schlüssel das 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,00

Um den Namen zu konvertieren, kann man einen Online-Service verwenden, zum Beispiel, diese.

Windows Native Applications und der Dienst Acronis Active Restore
Das bedeutet, um eine native Anwendung zu starten, müssen wir:

  1. Die ausführbare Datei in den system32-Ordner kopieren
  2. Einen Schlüssel in die Registry hinzufügen
  3. Die Maschine neu starten

Zur Vereinfachung finden Sie hier ein fertiges Skript zur Installation der nativen Anwendung:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pause

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

Nach der Installation und dem Neustart, noch bevor der Bildschirm zur Benutzerauswahl erscheint, haben wir folgendes Bild:

Windows Native Applications und der Dienst Acronis Active Restore

Zusammenfassung

Anhand dieser kleinen Anwendung haben wir festgestellt, dass es durchaus möglich ist, eine Anwendung auf der Ebene von Windows Native zu starten. Weiterhin werden wir mit den Kollegen von der Universität Innopolis den Dienst weiterentwickeln, der den Interaktionsprozess mit dem Treiber viel früher initialisieren wird als in der vorherigen Version unseres Projekts. Und mit dem Erscheinen der Win32-Oberfläche wird es sinnvoll sein, die Kontrolle an einen vollständigen Dienst zu übergeben, der bereits entwickelt wurde (darüber später mehr) hier).

In unserem nächsten Artikel werden wir einen weiteren Bestandteil des Active Restore-Dienstes behandeln, nämlich den UEFI-Treiber. Abonnieren Sie unseren Blog, um den nächsten Beitrag nicht zu verpassen.

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster