Native Windows-Anwendungen und Acronis Active Restore-Dienst

Heute erzählen wir weiter, wie wir zusammen mit den Jungs von der Innopolis University die Active Restore-Technologie entwickeln, damit der Benutzer nach einem Ausfall so schnell wie möglich mit der Arbeit an seinem Computer beginnen kann. Wir werden über native Windows-Anwendungen sprechen, einschließlich der Funktionen für deren Erstellung und Start. Im Folgenden erfahren Sie ein wenig über unser Projekt sowie eine praktische Anleitung zum Schreiben nativer Anwendungen.

Native Windows-Anwendungen und Acronis Active Restore-Dienst

In früheren Beiträgen haben wir bereits darüber gesprochen, was es ist Aktive Wiederherstellungund wie sich Studenten aus Innopolis entwickeln Service. Heute möchte ich mich auf native Anwendungen konzentrieren, auf deren Ebene wir unseren aktiven Wiederherstellungsdienst „begraben“ wollen. Wenn alles klappt, können wir:

  • Starten Sie den Dienst selbst viel früher
  • Wenden Sie sich viel früher an die Cloud, in der sich das Backup befindet
  • Es ist viel früher möglich, zu verstehen, in welchem ​​Modus sich das System befindet – normaler Start oder Wiederherstellung
  • Viel weniger Dateien müssen im Voraus wiederhergestellt werden
  • Ermöglichen Sie dem Benutzer einen noch schnelleren Einstieg.

Was ist überhaupt eine native App?

Um diese Frage zu beantworten, schauen wir uns die Reihenfolge der Aufrufe an, die das System beispielsweise ausführt, wenn ein Programmierer in seiner Anwendung versucht, eine Datei zu erstellen.

Native Windows-Anwendungen und Acronis Active Restore-Dienst
Pavel Yosifovich – Windows-Kernel-Programmierung (2019)

Der Programmierer nutzt die Funktion CreateFile, das in der Header-Datei fileapi.h deklariert und in Kernel32.dll implementiert ist. Allerdings erstellt diese Funktion selbst nicht die Datei, sondern überprüft nur die Eingabeargumente und ruft die Funktion auf NtCreateFile (Das Präfix Nt zeigt lediglich an, dass die Funktion nativ ist). Diese Funktion wird in der Header-Datei winternl.h deklariert und in ntdll.dll implementiert. Es bereitet sich auf den Sprung in den nuklearen Raum vor und führt anschließend einen Systemaufruf durch, um eine Datei zu erstellen. In diesem Fall stellt sich heraus, dass Kernel32 nur ein Wrapper für Ntdll ist. Dies liegt unter anderem daran, dass Microsoft damit die Möglichkeit hat, die Funktionen der nativen Welt zu verändern, die Standardschnittstellen jedoch nicht anzutasten. Microsoft rät vom direkten Aufruf nativer Funktionen ab und dokumentiert die meisten davon nicht. Es gibt übrigens auch undokumentierte Funktionen hier.

Der Hauptvorteil nativer Anwendungen besteht darin, dass ntdll viel früher als kernel32 in das System geladen wird. Das ist logisch, da Kernel32 ntdll benötigt, um zu funktionieren. Dadurch können Anwendungen, die native Funktionen verwenden, viel früher mit der Arbeit beginnen.

Daher sind native Windows-Anwendungen Programme, die früh beim Windows-Start gestartet werden können. Sie verwenden NUR Funktionen von ntdll. Ein Beispiel für eine solche Anwendung: autochk wer auftritt chkdisk-Dienstprogramm um die Festplatte auf Fehler zu überprüfen, bevor Sie die Hauptdienste starten. Dies ist genau das Niveau, das wir mit unserem Active Restore erreichen möchten.

Was brauchen wir?

  • DDK (Driver Development Kit), jetzt auch bekannt als WDK 7 (Windows Driver Kit).
  • Virtuelle Maschine (z. B. Windows 7 x64)
  • Nicht notwendig, aber herunterladbare Header-Dateien können hilfreich sein hier

Was steht im Code?

Lasst uns ein wenig üben und zum Beispiel eine kleine Bewerbung schreiben, die:

  1. Zeigt eine Meldung auf dem Bildschirm an
  2. Reserviert etwas Speicher
  3. Wartet auf Tastatureingaben
  4. Gibt verbrauchten Speicher frei

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

Beginnen wir mit der Anzeige einer Meldung auf dem Bildschirm. Dafür haben wir eine native Funktion NtDisplayString, das als Argument einen Zeiger auf ein UNICODE_STRING-Strukturobjekt akzeptiert. RtlInitUnicodeString hilft uns bei der Initialisierung. Um Text auf dem Bildschirm anzuzeigen, können wir daher diese 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 Funktionen von ntdll zur Verfügung stehen und es einfach noch keine anderen Bibliotheken im Speicher gibt, werden wir definitiv Probleme mit der Speicherzuweisung haben. Der neue Operator existiert noch nicht (da er aus der zu hohen Ebene von C++ stammt) und es gibt keine Malloc-Funktion (er erfordert Laufzeit-C-Bibliotheken). Natürlich können Sie nur einen Stapel verwenden. Wenn wir jedoch Speicher dynamisch zuweisen müssen, müssen wir dies auf dem Heap (d. h. Heap) tun. Erstellen wir also einen Heap für uns selbst und nehmen uns daraus Speicher, wann immer wir ihn brauchen.

Die Funktion ist für diese Aufgabe geeignet RtlCreateHeap. Als nächstes belegen wir mit RtlAllocateHeap und RtlFreeHeap Speicher und geben ihn frei, wenn wir ihn benötigen.

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);

Fahren wir mit dem Warten auf Tastatureingaben fort.

// 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 brauchen, ist zu verwenden NtReadFile auf einem geöffneten Gerät und warten Sie, bis die Tastatur jeden Druck an uns zurückgibt. Wenn die ESC-Taste gedrückt wird, wird weitergearbeitet. Um das Gerät zu öffnen, müssen wir die NtCreateFile-Funktion aufrufen (wir müssen DeviceKeyboardClass0 öffnen). Wir werden auch anrufen NtCreateEventum das Warteobjekt zu initialisieren. Wir werden die Struktur KEYBOARD_INPUT_DATA selbst deklarieren, die die Tastaturdaten darstellt. Dies wird unsere Arbeit erleichtern.

Die native Anwendung endet mit einem Funktionsaufruf NtTerminateProcessweil wir einfach unseren eigenen Prozess töten.

Der gesamte Code für unsere kleine 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 die Funktion DbgBreakPoint() in unserem Code problemlos verwenden, um ihn im Debugger zu stoppen. Allerdings müssen Sie WinDbg für das Kernel-Debugging mit einer virtuellen Maschine verbinden. Eine Anleitung dazu finden Sie hier hier oder einfach verwenden VirtualKD.

Zusammenstellung und Montage

Der einfachste Weg, eine native Anwendung zu erstellen, ist die Verwendung DDK (Treiberentwicklungskit). Wir benötigen die alte siebte Version, da spätere Versionen einen etwas anderen Ansatz verfolgen und eng mit Visual Studio zusammenarbeiten. Wenn wir das DDK verwenden, benötigt unser Projekt nur Makefile und Quellen.

Make-Datei

!INCLUDE $(NTMAKEENV)makefile.def

Quellen:

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 das gleiche sein, aber schauen wir uns die Quellen etwas detaillierter an. Diese Datei gibt die Quellen Ihres Programms (.c-Dateien), Build-Optionen und andere Parameter an.

  • TARGETNAME – der Name der ausführbaren Datei, die am Ende erstellt werden soll.
  • TARGETTYPE – Typ der ausführbaren Datei, es kann ein Treiber (.sys) sein, dann sollte der Feldwert DRIVER sein, wenn eine Bibliothek (.lib), dann ist der Wert LIBRARY. In unserem Fall benötigen wir eine ausführbare Datei (.exe), daher setzen wir den Wert auf PROGRAM.
  • UMTYPE – mögliche Werte für dieses Feld: Console für eine Konsolenanwendung, Windows für das Arbeiten im Fenstermodus. Aber wir müssen nt angeben, um eine native Anwendung zu erhalten.
  • BUFFER_OVERFLOW_CHECKS – Überprüfung des Stapels auf Pufferüberlauf, leider nicht unser Fall, wir schalten es aus.
  • MINWIN_SDK_LIB_PATH – dieser Wert bezieht sich auf die Variable SDK_LIB_PATH. Machen Sie sich keine Sorgen, dass Sie keine solche Systemvariable deklariert haben. Wenn wir den geprüften Build aus dem DDK ausführen, wird diese Variable deklariert und zeigt auf die erforderlichen Bibliotheken.
  • QUELLEN – eine Liste von Quellen für Ihr Programm.
  • ENTHÄLT – Header-Dateien, die für die Montage erforderlich sind. Hier geben sie normalerweise den Pfad zu den Dateien an, die mit dem DDK geliefert werden, Sie können aber auch beliebige andere angeben.
  • TARGETLIBS – Liste der Bibliotheken, die verknüpft werden müssen.
  • USE_NTDLL ist ein Pflichtfeld, das aus offensichtlichen Gründen auf 1 gesetzt werden muss.
  • USER_C_FLAGS – alle Flags, die Sie in Präprozessoranweisungen beim Vorbereiten von Anwendungscode verwenden können.

Zum Erstellen müssen wir also x86 (oder x64) Checked Build ausführen, das Arbeitsverzeichnis in den Projektordner ändern und den Build-Befehl ausführen. Das Ergebnis im Screenshot zeigt, dass wir eine ausführbare Datei haben.

Native Windows-Anwendungen und Acronis Active Restore-Dienst

Diese Datei kann nicht so einfach gestartet werden, das System flucht und schickt uns mit der folgenden Fehlermeldung zum Nachdenken über ihr Verhalten:

Native Windows-Anwendungen und Acronis Active Restore-Dienst

Wie starte ich eine native Anwendung?

Beim Start von Autochk wird die Startreihenfolge der Programme durch den Wert des Registrierungsschlüssels bestimmt:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Der Sitzungsmanager führt nacheinander Programme aus dieser Liste aus. Der Sitzungsmanager sucht im System32-Verzeichnis nach den ausführbaren Dateien. Das Format des Registrierungsschlüsselwerts lautet wie folgt:

autocheck autochk *MyNative

Der Wert muss im Hexadezimalformat und nicht im üblichen ASCII-Format vorliegen, daher hat der oben gezeigte Schlüssel das folgende 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 Titel umzuwandeln, können Sie beispielsweise einen Online-Dienst nutzen diese.

Native Windows-Anwendungen und Acronis Active Restore-Dienst
Es stellt sich heraus, dass wir zum Starten einer nativen Anwendung Folgendes benötigen:

  1. Kopieren Sie die ausführbare Datei in den Ordner system32
  2. Fügen Sie der Registrierung einen Schlüssel hinzu
  3. Starten Sie die Maschine neu

Der Einfachheit halber finden Sie hier ein vorgefertigtes Skript zum Installieren einer 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_MACHINESYSTEMCurrentControlSetControlSession 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 Benutzerauswahlbildschirm erscheint, erhalten wir folgendes Bild:

Native Windows-Anwendungen und Acronis Active Restore-Dienst

Ergebnis

Am Beispiel einer so kleinen Anwendung konnten wir uns davon überzeugen, dass es durchaus möglich ist, die Anwendung auf Windows Native-Ebene auszuführen. Als nächstes werden die Jungs von der Innopolis University und ich weiterhin einen Dienst aufbauen, der den Interaktionsprozess mit dem Fahrer viel früher einleitet als in der vorherigen Version unseres Projekts. Und mit dem Aufkommen der Win32-Shell wäre es logisch, die Kontrolle an einen vollwertigen Dienst zu übertragen, der bereits entwickelt wurde (mehr dazu). hier).

Im nächsten Artikel gehen wir auf eine weitere Komponente des Active Restore-Dienstes ein, nämlich den UEFI-Treiber. Abonnieren Sie unseren Blog, um den nächsten Beitrag nicht zu verpassen.

Source: habr.com

Kommentar hinzufügen