Aujourd'hui, nous continuons notre histoire sur la façon dont nous développons, avec les jeunes de l'Université d'Innopolis, la technologie Active Restore pour permettre à l'utilisateur de commencer à travailler sur sa machine dÚs que possible aprÚs un incident. Nous allons parler des applications Windows natives, y compris les spécificités de leur création et de leur exécution. Dans cet article, vous trouverez un aperçu de notre projet, ainsi qu'un guide pratique sur la façon d'écrire des applications natives.

Dans les posts précédents, nous avons déjà expliqué ce qu'est , et comment les étudiants d'Innopolis développent . Aujourd'hui, je veux me concentrer sur les applications natives, au niveau duquel nous voulons "enterrer" notre service de récupération active. Si tout se passe bien, nous pourrons :
- Lancer le service beaucoup plus tĂŽt
- Nous connecter beaucoup plus tĂŽt au cloud oĂč se trouve la sauvegarde
- Comprendre beaucoup plus tÎt dans quel mode le systÚme se trouve - mode normal ou mode de récupération
- Restaurer beaucoup moins de fichiers Ă l'avance
- Permettre Ă l'utilisateur de commencer Ă travailler encore plus rapidement.
Qu'est-ce qu'une application native ?
Pour répondre à cette question, regardons la séquence d'appels que le systÚme effectue, par exemple, si un programmeur dans son application essaie de créer un fichier.

Pavel Yosifovich - Windows Kernel Programming (2019)
Le programmeur utilise la fonction , qui est dĂ©clarĂ©e dans le fichier d'en-tĂȘte fileapi.h et mise en Ćuvre dans Kernel32.dll. Cependant, cette fonction ne s'occupe pas de la crĂ©ation du fichier, elle vĂ©rifie simplement les arguments d'entrĂ©e et appelle la fonction (le prĂ©fixe Nt indique que la fonction est native). Cette fonction est dĂ©clarĂ©e dans le fichier d'en-tĂȘte winternl.h et mise en Ćuvre dans ntdll.dll. Elle prĂ©pare le saut dans l'espace noyau, aprĂšs quoi elle effectue un appel systĂšme pour crĂ©er le fichier. Dans ce cas, Kernel32 n'est qu'un wrapper pour Ntdll. L'une des raisons pour lesquelles cela est fait, c'est que Microsoft peut ainsi modifier les fonctions du monde natif sans toucher aux interfaces standard. Microsoft dĂ©conseille d'appeler directement les fonctions natives et ne documente pas une grande partie d'entre elles. Au fait, les fonctions non documentĂ©es peuvent ĂȘtre trouvĂ©es. .
Le principal avantage des applications natives est que ntdll se charge dans le systÚme bien avant kernel32. C'est logique, puisque kernel32 nécessite la présence de ntdll pour fonctionner. En conséquence, les applications qui utilisent des fonctions natives peuvent commencer à fonctionner beaucoup plus tÎt.
Ainsi, les applications Windows Native sont des programmes capables de dĂ©marrer Ă un stade prĂ©coce de la charge de Windows. Elles utilisent UNIQUEMENT des fonctions de ntdll. Un exemple de ce type d'application : qui exĂ©cute pour vĂ©rifier le disque Ă la recherche d'erreurs avant mĂȘme que les services principaux ne soient lancĂ©s. C'est Ă ce niveau que nous voulons voir notre Active Restore.
De quoi avons-nous besoin ?
- (Driver Development Kit), désormais également connu sous le nom de WDK 7 (Windows Driver Kit).
- Une machine virtuelle (par exemple, Windows 7 x64)
- Ce n'est pas obligatoire, mais des fichiers d'en-tĂȘte pouvant ĂȘtre tĂ©lĂ©chargĂ©s peuvent aider.
Qu'en est-il du code ?
Entraßnons-nous un peu et, par exemple, écrivons une petite application qui :
- Affiche un message à l'écran
- Alloue un peu de mémoire
- Attend une entrée du clavier
- LibÚre la mémoire allouée
Dans les applications natives, le point d'entrée n'est pas main ou winmain, mais la fonction NtProcessStartup, car nous lançons en fait directement un nouveau processus dans le systÚme.
Commençons par afficher un message à l'écran. Pour cela, nous avons la fonction native , qui prend comme argument un pointeur sur un objet de la structure UNICODE_STRING. Nous pourrons l'initialiser grùce à RtlInitUnicodeString. Ainsi, pour afficher un texte à l'écran, nous pouvons écrire une petite fonction comme celle-ci :
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Puisque nous avons uniquement accÚs aux fonctions de ntdll, et qu'aucune autre bibliothÚque n'est encore présente en mémoire, nous rencontrerons nécessairement des difficultés pour allouer de la mémoire. L'opérateur new n'existe pas encore (car il provient d'un monde C++ trop haut niveau), il n'y a pas non plus de fonction malloc (qui nécessite des bibliothÚques d'exécution C). Nous pouvons bien sûr n'utiliser que la pile. Mais si nous devons allouer de la mémoire dynamiquement, nous devrons le faire dans le tas (c.-à -d. heap). Donc, créons un tas pour nous et prenons de la mémoire lorsqu'il en est nécessaire.
Pour cette tùche, la fonction est appropriée. Ensuite, en utilisant RtlAllocateHeap et RtlFreeHeap, nous allouerons et libérerons de la mémoire lorsque nous en aurons besoin.
PVOID mémoire = NULL;
PVOID tampon = NULL;
ULONG tailleTampon = 42;
// créer un tas pour allouer de la mémoire plus tard
mémoire = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// allouer un tampon de taille tailleTampon
tampon = RtlAllocateHeap(
mémoire,
HEAP_ZERO_MEMORY,
tailleTampon
);
// libérer le tampon (en réalité pas nécessaire car nous détruisons le tas à l'étape suivante)
RtlFreeHeap(mémoire, 0, tampon);
RtlDestroyHeap(mémoire);Passons à l'attente d'une entrée au clavier.
// 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;
}
}Tout ce dont nous avons besoin est d'utiliser sur le dispositif ouvert et d'attendre que le clavier nous renvoie une pression de touche. Si la touche ESC est pressĂ©e, nous continuerons le travail. Pour ouvrir le dispositif, nous devrons appeler la fonction NtCreateFile (nous devrons ouvrir DeviceKeyboardClass0). Nous appellerons Ă©galement , pour initialiser un objet d'attente. Nous dĂ©clarerons nous-mĂȘmes une structure KEYBOARD_INPUT_DATA, qui reprĂ©sente les donnĂ©es du clavier. Cela facilitera notre travail.
Le fonctionnement de l'application native se termine par l'appel de la fonction , car nous tuons simplement notre propre processus.
Tout le code de notre petite application :
#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 : Nous pouvons facilement utiliser dans le code la fonction DbgBreakPoint() pour s'arrĂȘter dans le dĂ©bogueur. Il faudra cependant connecter WinDbg Ă la machine virtuelle pour le dĂ©bogage de noyau. Les instructions sur comment faire cela peuvent ĂȘtre trouvĂ©es ou simplement utiliser .
Compilation et assemblage
Le moyen le plus simple de compiler une application native est d'utiliser (Driver Development Kit). Nous avons besoin de la septiÚme version ancienne, car les versions plus récentes ont une approche légÚrement différente et travaillent étroitement avec Visual Studio. Si nous utilisons DDK, notre projet a seulement besoin de Makefile et de 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 = 1Votre Makefile sera exactement le mĂȘme, concentrons-nous un peu plus sur les sources. Ce fichier spĂ©cifie le code source de votre programme (fichiers .c), les options de compilation et d'autres paramĂštres.
- TARGETNAME â le nom du fichier exĂ©cutable qui doit ĂȘtre obtenu au final.
- TARGETTYPE â le type de fichier exĂ©cutable, cela peut ĂȘtre un pilote (.sys), auquel cas la valeur doit ĂȘtre DRIVER, si c'est une bibliothĂšque (.lib), alors la valeur LIBRARY. Dans notre cas, nous avons besoin d'un fichier exĂ©cutable (.exe), donc nous dĂ©finissons la valeur PROGRAM.
- UMTYPE â valeurs possibles pour ce champ : console pour une application console, windows pour fonctionner en mode fenĂȘtrĂ©. Mais nous devons indiquer nt pour obtenir une application native.
- BUFFER_OVERFLOW_CHECKS â vĂ©rification de la pile pour dĂ©bordement de tampon, malheureusement ce n'est pas notre cas, nous dĂ©sactivons.
- MINWIN_SDK_LIB_PATH â cette valeur fait rĂ©fĂ©rence Ă la variable SDK_LIB_PATH, ne vous inquiĂ©tez pas si vous nâavez pas cette variable systĂšme dĂ©clarĂ©e, au moment oĂč nous lancerons la build vĂ©rifiĂ©e depuis DDK, cette variable sera dĂ©clarĂ©e et pointera vers les bibliothĂšques nĂ©cessaires.
- SOURCES â liste des sources de votre programme.
- INCLUDES â fichiers d'en-tĂȘte nĂ©cessaires pour la compilation. Ici, on indique gĂ©nĂ©ralement le chemin vers les fichiers fournis avec le DDK, mais vous pouvez Ă©galement ajouter d'autres fichiers.
- TARGETLIBS â liste des bibliothĂšques Ă lier.
- USE_NTDLL â champ obligatoire Ă dĂ©finir sur 1. Pour des raisons Ă©videntes.
- USER_C_FLAGS â tous les drapeaux que vous pouvez utiliser dans les directives de prĂ©processeur lors de la prĂ©paration du code de l'application.
Ainsi, pour compiler, nous devons lancer x86 (ou x64) Checked Build, changer le répertoire de travail vers le dossier du projet et exécuter la commande Build. Le résultat sur la capture d'écran montre que nous avons compilé un fichier exécutable.

Ce fichier ne pourra pas ĂȘtre facilement exĂ©cutĂ©, le systĂšme se plaint et nous envoie rĂ©flĂ©chir Ă son comportement avec l'erreur suivante :

Comment exécuter une application native ?
Au démarrage, l'ordre d'exécution des programmes est déterminé par la valeur de la clé du registre :
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteLe gestionnaire de session exécute successivement les programmes de cette liste. Les fichiers exécutables sont recherchés par le gestionnaire de session dans le répertoire system32. Le format de la valeur de la clé de registre est le suivant :
autocheck autochk *MyNativeLa valeur doit ĂȘtre au format hexadĂ©cimal, et non en ASCII, par consĂ©quent, la clĂ© prĂ©sentĂ©e ci-dessus aura le 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,00Pour convertir le nom, vous pouvez utiliser un service en ligne, par exemple, .

En résumé, pour exécuter une application native, nous devons :
- Copier le fichier exécutable dans le dossier system32
- Ajouter une clé dans le registre
- Redémarrer la machine
Pour plus de commoditĂ©, voici un script prĂȘt Ă l'emploi pour installer une application native :
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Exemple natif installé
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,00AprĂšs l'installation et le redĂ©marrage, avant mĂȘme d'arriver Ă l'Ă©cran de sĂ©lection des utilisateurs, nous aurons le tableau suivant :

Conclusion
Avec cet exemple d'une petite application, nous avons prouvé qu'il est tout à fait possible de lancer une application en mode Windows Native. Par la suite, nous allons continuer avec les gars de l'Université d'Innopolis à construire un service qui initiera le processus d'interaction avec le pilote beaucoup plus tÎt que dans la version précédente de notre projet. Avec l'apparition de l'interface win32, il sera logique de transférer le contrÎle à un service complet qui a déjà été développé (plus de détails à ce sujet. ).
Dans un prochain article, nous aborderons un autre composant du service Active Restore, Ă savoir le pilote UEFI. Abonnez-vous Ă notre blog pour ne pas manquer le prochain post.
Source : habr.com
