Applications natives Windows et Acronis Active Restore

Aujourd'hui, nous continuons l'histoire sur la façon dont nous, avec les gars de l'Université Innopolis, développons la technologie Active Restore pour permettre à l'utilisateur de commencer à travailler sur sa machine dès que possible après une panne. Nous parlerons des applications Windows natives, y compris les fonctionnalités de leur création et de leur lancement. Under the cut - un peu sur notre projet, ainsi qu'un guide pratique sur la façon d'écrire des applications natives.



Dans les articles précédents, nous avons déjà parlé de ce qu'est Active Restore et de la façon dont les étudiants d'Innopolis développent le service . Aujourd'hui, je veux me concentrer sur les applications natives, au niveau desquelles nous voulons «enterrer» notre service de récupération actif. Si tout fonctionne, alors nous pouvons:

  • Beaucoup plus tôt pour démarrer le service lui-même
  • Beaucoup plus tôt pour contacter le cloud dans lequel se trouve la sauvegarde
  • Il est beaucoup plus tôt pour comprendre dans quel mode est le système - démarrage normal ou récupération
  • Pour restaurer beaucoup moins de fichiers à l'avance
  • Permettez à l'utilisateur de démarrer encore plus rapidement.

Qu'est-ce qu'une application native en général?


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 - Programmation du noyau Windows (2019)

Le programmeur utilise la fonction CreateFile , qui est déclarée dans le fichier d'en-tête fileapi.h et est implémentée dans Kernel32.dll. Cependant, cette fonction elle-même ne crée pas de fichier, elle vérifie uniquement les arguments à l'entrée et appelle la fonction NtCreateFile (le préfixe Nt indique simplement que la fonction est native). Cette fonction est déclarée dans le fichier d'en-tête winternl.h et est implémentée dans ntdll.dll. Elle se prépare à sauter dans l'espace nucléaire, après quoi elle fait un appel système pour créer un fichier. Dans ce cas, il s'avère que Kernel32 n'est qu'un wrapper pour Ntdll. L'une des raisons pour lesquelles cela est fait, Microsoft a donc la possibilité de modifier les fonctions du monde natif, mais ne touche pas aux interfaces standard. Microsoft ne recommande pas d'appeler directement les fonctions natives et ne documente pas la plupart d'entre elles. À propos, des fonctionnalités non documentées peuvent être trouvées ici .

Le principal avantage des applications natives est que ntdll se charge dans le système beaucoup plus tôt que kernel32. C'est logique, car kernel32 nécessite ntdll pour fonctionner. Par conséquent, les applications qui utilisent des fonctions natives peuvent commencer à fonctionner beaucoup plus tôt.

Ainsi, les applications natives Windows sont des programmes qui peuvent s'exécuter à un stade précoce du démarrage de Windows. Ils utilisent UNIQUEMENT les fonctions de ntdll. Un exemple d'une telle application: autochk qui exécute l' utilitaire chkdisk pour rechercher des erreurs sur le disque avant de démarrer les services principaux. C'est à ce niveau que nous voulons voir notre restauration active.

De quoi avons-nous besoin?


  • DDK (Driver Development Kit), désormais également appelé WDK 7 (Windows Driver Kit).
  • Machine virtuelle (par exemple Windows 7 x64)
  • Pas nécessairement, mais les fichiers d'en-tête peuvent être téléchargés ici.

Que contient le code?


Pratiquons un peu et pour un exemple nous allons écrire une petite application qui:

  1. Affiche un message à l'écran.
  2. Alloue un peu de mémoire
  3. En attente de saisie au clavier
  4. Libère la mémoire occupée

Dans les applications natives, le point d'entrée n'est pas le principal ou le winmain, mais la fonction NtProcessStartup, car nous démarrons en fait directement le nouveau processus dans le système.

Commençons par afficher le message à l'écran. Pour ce faire, nous avons une fonction native NtDisplayString , qui prend comme argument un pointeur vers un objet de la structure UNICODE_STRING. RtlInitUnicodeString nous aidera à l'initialiser. Par conséquent, pour afficher du texte à l'écran, nous pouvons écrire une si petite fonction:

//usage: WriteLn(L"Here is my text\n"); void WriteLn(LPWSTR Message) { UNICODE_STRING string; RtlInitUnicodeString(&string, Message); NtDisplayString(&string); } 

Étant donné que seules les fonctions de ntdll sont disponibles pour nous, et qu'il n'y a tout simplement pas d'autres bibliothèques en mémoire pour le moment, nous aurons certainement des problèmes avec la façon d'allouer de la mémoire. Le nouvel opérateur n'existe pas encore (car il provient d'un monde C ++ de haut niveau), il n'y a pas non plus de fonction malloc (il a besoin de bibliothèques C d'exécution). Vous ne pouvez bien sûr utiliser que la pile. Mais si nous devons allouer dynamiquement de la mémoire, nous devrons le faire sur le tas (c'est-à-dire le tas). Par conséquent, créons un groupe pour nous-mêmes et nous en prendrons la mémoire lorsque nous en aurons besoin.

La fonction RtlCreateHeap convient à cette tâche. De plus, en utilisant RtlAllocateHeap et RtlFreeHeap, nous occuperons et libérerons de la mémoire lorsque nous en aurons besoin.

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

Passons à l'attente de la saisie 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"\\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; } } 

Tout ce que nous devons faire est d'utiliser NtReadFile sur un appareil ouvert et d'attendre que le clavier nous renvoie un clic. Si vous appuyez sur la touche ESC, nous continuerons à travailler. Pour ouvrir l'appareil, nous devons appeler la fonction NtCreateFile (vous devrez ouvrir \ Device \ KeyboardClass0). Nous appellerons également NtCreateEvent pour initialiser l'objet à attendre. Nous déclarerons indépendamment la structure KEYBOARD_INPUT_DATA qui représente les données du clavier. Cela facilitera notre travail.

L'application native se termine par un appel à la fonction NtTerminateProcess , car nous venons de tuer notre propre processus.

Tout le code de notre petite application:

 #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: Nous pouvons facilement utiliser la fonction DbgBreakPoint () dans le code pour nous arrêter dans le débogueur. Certes, vous devrez connecter WinDbg à la machine virtuelle pour le débogage du noyau. Vous trouverez des instructions sur la façon de procéder ici ou utilisez simplement VirtualKD .

Compilation et assemblage


La façon la plus simple de créer une application native consiste à utiliser DDK (Driver Development Kit). Nous avons besoin exactement de l'ancienne septième version, car les versions ultérieures ont une approche légèrement différente et fonctionnent en étroite collaboration avec Visual Studio. Si nous utilisons DDK, notre projet n'a besoin que de Makefile et de sources.

Makefile
 !INCLUDE $(NTMAKEENV)\makefile.def 

sources:
 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 

Votre Makefile sera exactement le même, mais revenons plus en détail sur les sources. Ce fichier contient les sources de votre programme (fichiers .c), les options de construction et d'autres paramètres.

  • TARGETNAME - le nom du fichier exécutable, qui devrait être le résultat.
  • TARGETTYPE - type de fichier exécutable, il peut s'agir d'un pilote (.sys), la valeur du champ doit être DRIVER, si la bibliothèque (.lib), alors la valeur est LIBRARY. Dans notre cas, nous avons besoin d'un fichier exécutable (.exe), nous définissons donc la valeur sur PROGRAM.
  • UMTYPE - valeurs possibles pour ce champ: console pour une application console, fenêtres pour fonctionner en mode fenêtré. Mais nous devons spécifier nt pour obtenir l'application native.
  • BUFFER_OVERFLOW_CHECKS - vérifier la pile pour le débordement de tampon, malheureusement pas notre cas, le désactiver.
  • 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 déclaré une telle variable système, au moment où nous exécutons la construction vérifiée à partir de DDK, cette variable sera déclarée et pointera vers les bibliothèques nécessaires.
  • SOURCES - une liste des sources de votre programme.
  • COMPREND - les fichiers d'en-tête nécessaires à l'assemblage. Ils indiquent généralement le chemin d'accès aux fichiers fournis avec le DDK, mais vous pouvez éventuellement en spécifier d'autres.
  • TARGETLIBS - une liste de bibliothèques qui doivent être liées.
  • USE_NTDLL est un champ obligatoire qui doit être défini sur la position 1. Pour des raisons évidentes.
  • USER_C_FLAGS - tous les indicateurs que vous pouvez utiliser dans les directives de préprocesseur lors de la préparation du code d'application.

Donc, pour construire, nous devons exécuter x86 (ou x64) Checked Build, changer le répertoire de travail dans le dossier du projet et exécuter la commande Build. Le résultat de la capture d'écran montre que nous avons rassemblé un fichier exécutable.

Construire

Ce fichier ne peut pas être exécuté si simplement, le système jure et nous envoie réfléchir à son comportement avec l'erreur suivante:

Erreur


Comment exécuter une application native?


Au début de autochk, la séquence de démarrage des programmes est déterminée par la valeur de la clé de registre:

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

Le gestionnaire de session exécute les programmes de cette liste un par un. Le gestionnaire de sessions lui-même recherche les fichiers exécutables dans le répertoire system32. Le format de la valeur de clé de registre est le suivant:

 autocheck autochk *MyNative 

La valeur doit être au format hexadécimal, et non au format ASCII habituel, 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,00 

Pour convertir le nom, vous pouvez utiliser un service en ligne, par exemple, celui-ci .


Il s'avère que pour exécuter l'application native, nous avons besoin de:

  1. Copiez le fichier exécutable dans le dossier system32
  2. Ajouter une clé au registre
  3. Redémarrez la machine

Pour plus de commodité, voici un script prêt à l'emploi pour l'installation d'une application native:

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 

Après l'installation et le redémarrage, avant même que l'écran de sélection de l'utilisateur n'apparaisse, nous obtenons l'image suivante:

résultat

Résumé


En utilisant l'exemple d'une si petite application, nous étions convaincus qu'il était tout à fait possible d'exécuter l'application au niveau Windows Native. De plus, les gars de l'Université Innopolis continueront de construire un service qui initiera le processus d'interaction avec le conducteur beaucoup plus tôt que dans la version précédente de notre projet. Et avec l'avènement du shell win32, il sera logique de transférer le contrôle à un service à part entière qui a déjà été développé (plus à ce sujet ici ).

Dans le 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: https://habr.com/ru/post/fr481382/


All Articles