Heute setzen wir unsere Berichterstattung über unsere Zusammenarbeit mit der Innopolis University fort, um die Active Restore-Technologie zu entwickeln. Diese ermöglicht es Nutzern, nach einem Absturz schnellstmöglich wieder an ihren Geräten weiterzuarbeiten. Wir werden über native Apps sprechen. WindowsDazu gehören auch Details zu ihrer Entstehung und Veröffentlichung. Im Folgenden finden Sie einige Hintergrundinformationen zu unserem Projekt sowie eine praktische Anleitung zur Entwicklung nativer Apps.

In früheren Beiträgen haben wir bereits darüber gesprochen, was es ist und wie sich Studenten aus Innopolis entwickeln . 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.

Pavel Yosifovich — Windows Kernelprogrammierung (2019)
Der Programmierer nutzt die Funktion , 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 (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 .
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.
Somit kann die Windows Native Anwendungen sind Programme, die bereits früh während des Systemstarts ausgeführt werden können. WindowsSie verwenden AUSSCHLIESSLICH Funktionen aus ntdll. Ein Beispiel für eine solche Anwendung: wer auftritt 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?
- (Treiberentwicklungskit), jetzt auch bekannt als WDK 7 (Windows Treiber-Kit).
- Virtuelle Maschine (z.B. Windows 7 x64)
- Nicht notwendig, aber herunterladbare Header-Dateien können hilfreich sein
Was steht im Code?
Lasst uns ein wenig üben und zum Beispiel eine kleine Bewerbung schreiben, die:
- Zeigt eine Meldung auf dem Bildschirm an
- Reserviert etwas Speicher
- Wartet auf Tastatureingaben
- 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 , 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 . 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 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 um 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 weil 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 oder einfach verwenden .
Zusammenstellung und Montage
Der einfachste Weg, eine native Anwendung zu erstellen, ist die Verwendung (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.defQuellen:
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 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.

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

Wie starte ich eine native Anwendung?
Beim Start von Autochk wird die Startreihenfolge der Programme durch den Wert des Registrierungsschlüssels bestimmt:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteDer 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 *MyNativeDer 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,00Um den Titel umzuwandeln, können Sie beispielsweise einen Online-Dienst nutzen .

Es stellt sich heraus, dass wir zum Starten einer nativen Anwendung Folgendes benötigen:
- Kopieren Sie die ausführbare Datei in den Ordner system32
- Fügen Sie der Registrierung einen Schlüssel hinzu
- 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
pauseadd.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,00Nach der Installation und dem Neustart, noch bevor der Benutzerauswahlbildschirm erscheint, erhalten wir folgendes Bild:

Ergebnis
Anhand dieser kleinen Anwendung als Beispiel waren wir davon überzeugt, dass der Start einer Anwendung auf dem Niveau Windows Native Integration ist durchaus möglich. Als Nächstes werden die Kollegen der Universität Innopolis und ich einen Dienst weiterentwickeln, der die Interaktion mit dem Treiber deutlich früher initiiert als in der vorherigen Version unseres Projekts. Mit dem Aufkommen der Win32-Shell wird es logisch sein, die Steuerung an einen bereits entwickelten, vollwertigen Dienst zu übergeben (mehr dazu später). ).
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
