Windows Native Applications und Acronis Active Restore

Heute setzen wir die Geschichte fort, wie wir gemeinsam mit den Mitarbeitern der Innopolis University die Active Restore-Technologie entwickeln, damit der Benutzer nach einem Ausfall so schnell wie möglich mit der Arbeit an seiner Maschine beginnen kann. Wir werden über native Windows-Anwendungen sprechen, einschließlich der Funktionen ihrer Erstellung und ihres Starts. Under the cut - ein wenig über unser Projekt sowie eine praktische Anleitung zum Schreiben nativer Anwendungen.



In früheren Beiträgen haben wir bereits darüber gesprochen, was Active Restore ist und wie Studenten aus Innopolis den Service entwickeln . Heute möchte ich mich auf native Anwendungen konzentrieren, auf deren Ebene wir unseren aktiven Wiederherstellungsdienst begraben möchten. Wenn alles klappt, können wir:

  • Viel früher, um den Dienst selbst zu starten
  • Viel früher, um die Cloud zu kontaktieren, in der das Backup liegt
  • Es ist viel früher zu verstehen, in welchem ​​Modus sich das System befindet - normaler Start oder Wiederherstellung
  • Um im Voraus viel weniger Dateien wiederherzustellen
  • Ermöglichen Sie dem Benutzer einen noch schnelleren Einstieg.

Was ist eine native Anwendung im Allgemeinen?


Schauen wir uns zur Beantwortung dieser Frage die Reihenfolge der Aufrufe an, die das System durchführt, wenn beispielsweise ein Programmierer in seiner Anwendung versucht, eine Datei zu erstellen.


Pavel Yosifovich - Windows-Kernel-Programmierung (2019)

Der Programmierer verwendet die Funktion CreateFile , die in der Headerdatei fileapi.h deklariert und in Kernel32.dll implementiert ist. Diese Funktion selbst erstellt jedoch keine Datei, sondern überprüft nur die Argumente an der Eingabe und ruft die Funktion NtCreateFile auf (das Präfix Nt gibt lediglich an, dass die Funktion nativ ist). Diese Funktion ist in der Header-Datei winternl.h deklariert und in ntdll.dll implementiert. Sie bereitet den Sprung in den Nuklearraum vor und ruft anschließend das System auf, um eine Datei zu erstellen. In diesem Fall stellt sich heraus, dass Kernel32 nur ein Wrapper für Ntdll ist. Einer der Gründe dafür ist, dass Microsoft in der Lage ist, die Funktionen der einheimischen Welt zu ändern, die Standardschnittstellen jedoch nicht zu berühren. Microsoft rät davon ab, native Funktionen direkt aufzurufen, und dokumentiert die meisten davon nicht. Undokumentierte Features finden Sie übrigens hier .

Der Hauptvorteil nativer Anwendungen besteht darin, dass ntdll viel früher als kernel32 in das System geladen wird. Dies ist logisch, da Kernel32 ntdll benötigt, um zu funktionieren. Infolgedessen können Anwendungen, die native Funktionen verwenden, viel früher ausgeführt werden.

Daher sind Windows Native Applications Programme, die zu einem frühen Zeitpunkt beim Booten von Windows ausgeführt werden können. Sie verwenden NUR Funktionen von ntdll. Ein Beispiel für eine solche Anwendung: autochk führt das Dienstprogramm chkdisk aus , um die Festplatte vor dem Starten der Hauptdienste auf Fehler zu überprüfen. Auf dieser Ebene möchten wir unsere aktive Wiederherstellung sehen.

Was brauchen wir


  • DDK (Driver Development Kit), jetzt auch als WDK 7 (Windows Driver Kit) bekannt.
  • Virtuelle Maschine (z. B. Windows 7 x 64)
  • Nicht unbedingt, aber Header-Dateien können hier heruntergeladen werden.

Was ist im Code?


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

  1. Zeigt eine Meldung auf dem Bildschirm an.
  2. Weist ein wenig Speicher zu
  3. Warten auf Tastatureingabe
  4. Gibt den belegten Speicher frei

In nativen Anwendungen ist der Einstiegspunkt nicht der Haupt- oder WinMain-Punkt, sondern die NtProcessStartup-Funktion, da wir den neuen Prozess tatsächlich direkt im System starten.

Beginnen wir mit der Anzeige der Meldung auf dem Bildschirm. Dazu haben wir eine native Funktion NtDisplayString , die als Argument einen Zeiger auf ein Objekt der UNICODE_STRING-Struktur verwendet. RtlInitUnicodeString hilft uns beim Initialisieren. Um Text auf dem Bildschirm anzuzeigen, können wir daher eine so kleine Funktion schreiben:

//usage: WriteLn(L"Here is my text\n"); 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 haben, Speicher zuzuweisen. Der neue Operator existiert noch nicht (da er aus einer zu übergeordneten C ++ - Welt stammt), es gibt auch keine Malloc-Funktion (es werden Laufzeit-C-Bibliotheken benötigt). Sie können natürlich nur den Stapel verwenden. Wenn wir jedoch Speicher dynamisch zuweisen müssen, müssen wir dies auf dem Heap (d. H. Heap) tun. Lassen Sie uns daher einen Haufen für uns selbst zusammenstellen, und wir werden ihn uns merken, wenn wir ihn brauchen.

Die Funktion RtlCreateHeap ist für diese Aufgabe geeignet. Mit RtlAllocateHeap und RtlFreeHeap belegen wir außerdem Speicherplatz 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); 

Warten wir nun auf die Tastatureingabe.

 // 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"\\Device\\KeyboardClass0"); 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 tun müssen, ist, NtReadFile auf einem geöffneten Gerät zu verwenden und zu warten, bis die Tastatur einen Klick an uns zurückgibt. Wird die ESC-Taste gedrückt, arbeiten wir weiter. Um das Gerät zu öffnen, müssen wir die Funktion NtCreateFile aufrufen (Sie müssen \ Device \ KeyboardClass0 öffnen). Wir werden auch NtCreateEvent aufrufen , um das zu wartende Objekt zu initialisieren. Wir werden die KEYBOARD_INPUT_DATA-Struktur, die die Tastaturdaten darstellt, unabhängig deklarieren. Dies wird unsere Arbeit erleichtern.

Die native Anwendung endet mit einem Aufruf der NtTerminateProcess- Funktion, da wir nur unseren eigenen Prozess beenden .

Alle Codes unserer kleinen Anwendung:

 #include "ntifs.h" // \WinDDK\7600.16385.1\inc\ddk #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"\\Device\\KeyboardClass0"); 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 ready\n"); // create heap in order to allocate memory later memory = RtlCreateHeap( HEAP_GROWABLE, NULL, 1000, 0, NULL, NULL ); WriteLn(L"Heap ready\n"); // allocate buffer of size bufferSize buffer = RtlAllocateHeap( memory, HEAP_ZERO_MEMORY, bufferSize ); WriteLn(L"Buffer allocated\n"); // free buffer (actually not needed because we destroy heap in next step) RtlFreeHeap(memory, 0, buffer); RtlDestroyHeap(memory); WriteLn(L"Heap destroyed\n"); 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 DbgBreakPoint () - Funktion im Code verwenden, um den Debugger anzuhalten. Richtig, Sie müssen WinDbg für das Kernel-Debugging mit der virtuellen Maschine verbinden. Eine Anleitung dazu finden Sie hier oder verwenden Sie einfach VirtualKD .

Zusammenstellung und Montage


Der einfachste Weg, eine native Anwendung zu erstellen, ist die Verwendung von DDK (Driver Development Kit). Wir brauchen genau die alte siebte Version, da spätere Versionen einen etwas anderen Ansatz haben und eng mit Visual Studio zusammenarbeiten. Wenn wir DDK verwenden, benötigt unser Projekt nur Makefile und Sources.

Makefile
 !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:\WinDDK\7600.16385.1\ndk; TARGETLIBS = $(DDK_LIB_PATH)\ntdll.lib \ $(DDK_LIB_PATH)\nt.lib USE_NTDLL = 1 

Ihr Makefile wird genau das gleiche sein, aber lassen Sie uns näher auf die Quellen eingehen. Diese Datei enthält die Quellen Ihres Programms (.c-Dateien), Erstellungsoptionen und andere Parameter.

  • ZIELNAME - Der Name der ausführbaren Datei, die das Ergebnis sein soll.
  • TARGETTYPE - Typ der ausführbaren Datei, es kann ein Treiber (.sys) sein, dann sollte der Feldwert DRIVER sein, wenn die 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: Konsole für eine Konsolenanwendung, Fenster für den Betrieb im Fenstermodus. Wir müssen aber nt angeben, um die native Anwendung zu erhalten.
  • BUFFER_OVERFLOW_CHECKS - Überprüfung des Stacks auf Pufferüberlauf, leider nicht in unserem Fall, deaktivieren Sie ihn.
  • MINWIN_SDK_LIB_PATH - dieser Wert bezieht sich auf die Variable SDK_LIB_PATH. Machen Sie sich keine Sorgen, dass Sie eine solche Systemvariable nicht deklariert haben. Sobald wir den geprüften Build von DDK ausführen, wird diese Variable deklariert und verweist auf die erforderlichen Bibliotheken.
  • QUELLEN - eine Liste der Quellen Ihres Programms.
  • INCLUDES - Header-Dateien, die für die Assemblierung benötigt werden. Sie geben normalerweise den Pfad zu den Dateien an, die mit dem DDK geliefert werden. Sie können jedoch auch andere angeben.
  • TARGETLIBS - eine Liste der Bibliotheken, die verknüpft werden müssen.
  • USE_NTDLL ist ein Pflichtfeld, das aus offensichtlichen Gründen auf Position 1 gesetzt werden muss.
  • USER_C_FLAGS - alle Flags, die Sie in Präprozessoranweisungen verwenden können, wenn Sie Anwendungscode vorbereiten.

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

Bauen

Diese Datei kann nicht so einfach ausgeführt werden, das System schwört und veranlasst uns, mit folgendem Fehler über ihr Verhalten nachzudenken:

Fehler


Wie starte ich eine native Anwendung?


Beim Start von autochk wird die Startreihenfolge von Programmen durch den Wert des Registrierungsschlüssels bestimmt:

 HKLM\System\CurrentControlSet\Control\Session Manager\BootExecute 

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

 autocheck autochk *MyNative 

Der Wert sollte im hexadezimalen Format und nicht im üblichen ASCII-Format vorliegen. Daher hat der oben dargestellte 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 Namen zu konvertieren, können Sie einen Onlinedienst verwenden, beispielsweise diesen .


Es stellt sich heraus, dass wir zum Ausführen der 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. Maschine neu starten

Der Einfachheit halber finden Sie hier ein fertiges 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_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 Benutzerauswahlbildschirm angezeigt wird, wird das folgende Bild angezeigt:

Ergebnis

Zusammenfassung


Am Beispiel einer so kleinen Anwendung waren wir überzeugt, dass es durchaus möglich ist, die Anwendung auf Windows Native-Ebene auszuführen. Darüber hinaus werden die Mitarbeiter der Innopolis-Universität weiterhin einen Service aufbauen, der den Prozess der Interaktion mit dem Fahrer viel früher als in der vorherigen Version unseres Projekts einleitet. Und mit dem Aufkommen der win32-Shell wird es logisch sein, die Kontrolle auf einen bereits entwickelten, vollwertigen Dienst zu übertragen (mehr dazu hier ).

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

Source: https://habr.com/ru/post/de481382/


All Articles