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 früh wie möglich nach einem Ausfall wieder auf seinem Gerät zu arbeiten. Es wird um native Windows-Anwendungen gehen, einschließlich der Besonderheiten ihrer Erstellung und Ausführung. Unter dem Schnitt – ein wenig über unser Projekt sowie eine praktische Anleitung, wie man native Anwendungen schreibt.

In früheren Beiträgen haben wir bereits erklärt, was ist und wie die Studenten aus Innopolis daran arbeiten. Heute möchte ich mich auf native Anwendungen konzentrieren, auf das Niveau, das wir mit unserem aktivem Wiederherstellungsdienst „beerdigen“ wollen. Wenn alles gut geht, können wir:
- Den Dienst viel früher starten
- Viel früher mit der Cloud kommunizieren, in der das Backup liegt
- Viel früher verstehen, in welchem Modus sich das System befindet – Normalbetrieb oder Wiederherstellung
- Viel weniger Dateien im Voraus wiederherstellen
- Dem Benutzer ermöglichen, noch schneller mit der Arbeit zu beginnen.
Was ist überhaupt eine native Anwendung?
Um diese Frage zu beantworten, lassen Sie uns die Reihenfolge der Aufrufe betrachten, die das System vornimmt, wenn ein Programmierer in seiner Anwendung versucht, eine Datei zu erstellen.

Pavel Yosifovich – Windows Kernel Programming (2019)
Der Programmierer verwendet die Funktion , die in der Header-Datei fileapi.h deklariert und in Kernel32.dll implementiert ist. Diese Funktion selbst befasst sich jedoch nicht mit der Erstellung von Dateien, sie prüft nur die Eingabeargumente und ruft die Funktion (die Präfix Nt weist darauf hin, 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 Kernelraum vor, gefolgt von einem Systemaufruf zur Datei-erstellung. In diesem Fall ist Kernel32 nur eine Hülle für Ntdll. Ein Grund dafür ist, dass Microsoft auf diese Weise die nativ-Weltfunktionen ändern kann, ohne die Standardinterfaces zu berühren. Microsoft empfiehlt nicht, native Funktionen direkt aufzurufen und dokumentiert den Großteil davon nicht. Übrigens können undokumentierte Funktionen gefunden werden. .
Der Hauptvorteil nativer Anwendungen liegt darin, dass ntdll erheblich früher in das System geladen wird als kernel32. Das ist logisch, da kernel32 die Verfügbarkeit von ntdll für seine Funktionalität benötigt. Als Folge können Anwendungen, die native Funktionen nutzen, deutlich früher starten.
Windows Native Applications sind somit Programme, die in der Lage sind, früh im Bootprozess von Windows zu starten. Sie verwenden NUR Funktionen aus ntdll. Ein Beispiel für eine solche Anwendung ist: die die ausführt, um die Festplatte vor dem Starten der Hauptdienste auf Fehler zu überprüfen. Genau auf dieser Ebene möchten wir unser Active Restore sehen.
Was benötigen wir?
- (Driver Development Kit), heute auch bekannt als WDK 7 (Windows Driver Kit).
- Eine virtuelle Maschine (zum Beispiel Windows 7 x64)
- Es ist nicht unbedingt erforderlich, aber Header-Dateien, die heruntergeladen werden können, können hilfreich sein.
Was bedeutet der Code?
Lassen Sie uns ein wenig üben und als Beispiel eine kleine Anwendung schreiben, die:
- eine Nachricht auf dem Bildschirm ausgibt
- ein wenig Speicher alloziert
- auf eine Eingabe von der Tastatur wartet
- den belegten Speicher freigibt
In nativen Anwendungen ist der Einstiegspunkt nicht main oder winmain, sondern die Funktion NtProcessStartup, da wir tatsächlich direkt einen neuen Prozess im System starten.
Lassen Sie uns mit der Ausgabe einer Nachricht auf dem Bildschirm beginnen. Dafür haben wir die native Funktion , die als Argument einen Zeiger auf ein Objekt der Struktur UNICODE_STRING erwartet. Sie können uns mit RtlInitUnicodeString dabei helfen, es zu initialisieren. Dadurch können wir eine kleine Funktion schreiben, die Text auf dem Bildschirm ausgibt:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Da uns nur Funktionen aus ntdll zur Verfügung stehen und es einfach noch keine anderen Bibliotheken im Speicher gibt, werden wir zwangsläufig Probleme haben, wie wir Speicher alloziieren. Der Operator new existiert noch nicht (denn er stammt aus einer zu hochstufigen C++-Welt), und auch die Funktion malloc gibt es nicht (da sie Runtime-C-Bibliotheken benötigt). Wir können natürlich nur den Stack verwenden. Aber wenn wir dynamischen Speicher alloziieren müssen, müssen wir diesen im Heap (d. h. im Heap) anfordern. Daher lassen Sie uns einen Heap erstellen und Speicher daraus anfordern, wann immer wir ihn benötigen.
Für diese Aufgabe eignet sich die Funktion . Anschließend werden wir mit RtlAllocateHeap und RtlFreeHeap Speicher anfordern und freigeben, wann immer wir es benötigen.
PVOID speicher = NULL;
PVOID pufferspeicher = NULL;
ULONG pufferspeicherGroesse = 42;
// Heap erstellen, um später Speicher zuzuweisen
speicher = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// Pufferspeicher der Größe pufferspeicherGroesse zuweisen
pufferspeicher = RtlAllocateHeap(
speicher,
HEAP_ZERO_MEMORY,
pufferspeicherGroesse
);
// Puffer freigeben (eigentlich nicht erforderlich, da wir den Heap im nächsten Schritt zerstören)
RtlFreeHeap(speicher, 0, pufferspeicher);
RtlDestroyHeap(speicher);Lass uns auf die Eingabe von der Tastatur warten.
// 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 die Verwendung von auf dem geöffneten Gerät, und auf eine Tasteneingabe warten. Wenn die ESC-Taste gedrückt wird, setzen wir die Arbeit fort. Um das Gerät zu öffnen, müssen wir die Funktion NtCreateFile aufrufen (DeviceKeyboardClass0 muss geöffnet werden). Außerdem werden wir , um ein Objekt für das Warten zu initialisieren. Wir werden die Struktur KEYBOARD_INPUT_DATA selbst deklarieren, die die Tastaturdaten darstellt. Das wird uns die Arbeit erleichtern.
Die nativen Anwendungen enden mit dem Aufruf der Funktion , weil 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 die Funktion DbgBreakPoint() problemlos im Code verwenden, um im Debugger anzuhalten. Es muss jedoch WinDbg mit der virtuellen Maschine für die Kernel-Debugging verbunden werden. Eine Anleitung, wie das geht, finden Sie oder einfach verwenden .
Kompilierung und Erstellung
Der einfachste Weg, eine native Anwendung zu erstellen, besteht darin, (Driver Development Kit) zu verwenden. Wir benötigen die alte siebte Version, da neuere Versionen einen etwas anderen Ansatz haben und eng mit Visual Studio arbeiten. Wenn wir DDK verwenden, benötigt unser Projekt lediglich Makefile und sources.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defsources:
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 = 1Ihr Makefile wird genau so aussehen, bei den sources bleiben wir jedoch etwas detaillierter. In dieser Datei geben Sie die Quellcodes Ihres Programms (Dateien .c), Kompilierungsoptionen und andere Parameter an.
- TARGETNAME – der Name der ausführbaren Datei, die letztendlich erzeugt werden soll.
- TARGETTYPE – der Typ der ausführbaren Datei, das kann ein Treiber (.sys) sein, dann sollte der Wert DRIVER sein, wenn eine Bibliothek (.lib), dann 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 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 ist das nicht unser Fall, also deaktivieren wir es.
- MINWIN_SDK_LIB_PATH – dieser Wert verweist auf die Variable SDK_LIB_PATH. Machen Sie sich keine Sorgen, wenn eine solche Systemvariable nicht deklariert ist. Sobald wir einen Checked Build aus dem DDK starten, wird diese Variable deklariert und auf die benötigten Bibliotheken verweisen.
- SOURCES – Liste der Quellen Ihrer Anwendung.
- INCLUDES – Headerdateien, die für den Build erforderlich sind. Hier wird normalerweise der Pfad zu den Dateien angegeben, die mit dem DDK geliefert werden, aber Sie können auch andere angeben.
- TARGETLIBS – Liste der Bibliotheken, die verlinkt werden müssen.
- USE_NTDLL – ein obligatorisches Feld, das auf 1 gesetzt werden muss. Aus ganz offensichtlichen Gründen.
- USER_C_FLAGS – alle Flags, die Sie in Präprozessor-Direktiven bei der Vorbereitung des Anwendungs-Codes verwenden können.
Um zu bauen, müssen wir einen x86 (oder x64) Checked Build starten, das Arbeitsverzeichnis auf den Projektordner ändern und den Befehl Build ausführen. Das Ergebnis im Screenshot zeigt, dass wir eine ausführbare Datei erstellt haben.

Diese Datei lässt sich nicht so einfach ausführen; das System beschwert sich und gibt uns den Hinweis, über sein Verhalten nachzudenken, mit der folgenden Fehlermeldung:

Wie starte ich eine native Anwendung?
Beim Start von autochk bestimmt der Wert des Registrierungs-Schlüssels die Reihenfolge des Programmstarts:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteDer Sitzungsmanager führt die Programme aus dieser Liste nacheinander aus. Die ausführbaren Dateien sucht der Sitzungsmanager im Verzeichnis system32. Das Format des Wertes des Registrierungs-Schlüssels ist wie folgt:
autocheck autochk *MyNativeDer Wert muss im hexadezimalen Format vorliegen und nicht im gewohnten ASCII, daher hat der oben dargestellte 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,00Um den Namen zu konvertieren, kann man einen Online-Service nutzen, zum Beispiel, .

Um eine native Anwendung zu starten, müssen wir also:
- Die ausführbare Datei in den Ordner system32 kopieren
- Einen Schlüssel in die Registrierung einfügen
- Den Computer neu starten
Zur Vereinfachung hier ein fertiges Skript zur Installation der nativen Anwendung:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native-Beispiel installiert
pauseadd.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,00Nach der Installation und dem Neustart, noch bevor der Bildschirm mit der Benutzerauswahl erscheint, erhalten wir folgendes Bild:

Fazit
Am Beispiel einer solchen kleinen Anwendung haben wir gesehen, dass es durchaus möglich ist, eine Anwendung auf Windows-Native-Ebene zu starten. Weiterhin werden wir mit den Kollegen der Universität Innopolis an einem Dienst arbeiten, der den Interaktionsprozess mit dem Treiber viel früher einleiten wird als in der vorherigen Version unseres Projekts. Mit dem Erscheinen der Win32-Oberfläche wird es logisch sein, die Kontrolle an einen vollwertigen Dienst zu übergeben, der bereits entwickelt wurde (darüber mehr in Kürze). ).
In einem kommenden Artikel werden wir noch 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
