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.

In früheren Beiträgen haben wir bereits erklärt, was ist und wie die Studierenden aus Innopolis daran arbeiten. Heute 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.

Pavel Yosifovich — Windows Kernel Programmierung (2019)
Der Entwickler verwendet die Funktion , 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 (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. .
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: das die 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?
- (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.
Was ist im Code?
Lass uns ein wenig üben und als Beispiel eine kleine Anwendung schreiben, die:
- eine Nachricht auf dem Bildschirm ausgibt
- ein wenig Speicher alloziert
- auf Eingaben von der Tastatur wartet
- 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 , 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 . 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 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 , 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 , 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 oder einfach .
Kompilierung und Erstellung
Der einfachste Weg, eine native Anwendung zu erstellen, ist die Verwendung von (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.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, 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.

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

Wie startet man eine native Anwendung?
Zum Zeitpunkt des Starts bestimmt der Wert des Registrierungsschlüssels die Startreihenfolge der Programme:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteDer 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 *MyNativeDer 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,00Um den Namen zu konvertieren, kann man einen Online-Service verwenden, zum Beispiel, .

Das bedeutet, um eine native Anwendung zu starten, müssen wir:
- Die ausführbare Datei in den system32-Ordner kopieren
- Einen Schlüssel in die Registry hinzufügen
- 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
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 zur Benutzerauswahl erscheint, haben wir folgendes Bild:

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