Applications natives Windows et service Acronis Active Restore

Aujourd'hui, nous continuons l'histoire de la façon dont nous développons, avec les gars 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 une panne. Nous parlerons des applications Windows natives, y compris des fonctionnalités de leur création et de leur lancement. Ci-dessous, vous trouverez quelques informations sur notre projet, ainsi qu'un guide pratique sur la façon d'écrire des applications natives.

Applications natives Windows et service Acronis Active Restore

Dans les articles précédents, nous avons déjà parlé de ce que c'est Restauration active, et comment les étudiants d'Innopolis se développent service. Aujourd'hui, je souhaite me concentrer sur les applications natives, au niveau desquelles nous souhaitons « enterrer » notre service de récupération active. Si tout se passe bien, nous pourrons alors :

  • Lancer le service lui-même beaucoup plus tôt
  • Contactez le cloud où se trouve la sauvegarde beaucoup plus tôt
  • Beaucoup plus tôt pour comprendre dans quel mode se trouve le système - démarrage normal ou récupération
  • Beaucoup moins de fichiers à récupérer à l'avance
  • Permettez à l’utilisateur de démarrer encore plus rapidement.

Au fait, 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 tente de créer un fichier.

Applications natives Windows et service Acronis Active Restore
Pavel Yosifovich - Programmation du noyau Windows (2019)

Le programmeur utilise la fonction CreateFile, qui est déclaré dans le fichier d'en-tête fileapi.h et implémenté dans Kernel32.dll. Cependant, cette fonction elle-même ne crée pas le fichier, elle vérifie uniquement les arguments d'entrée et appelle la fonction NtCréerFichier (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 implémentée dans ntdll.dll. Il se prépare à sauter dans l'espace nucléaire, après quoi il effectue 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 a été fait est que Microsoft a ainsi la possibilité de modifier les fonctions du monde natif, mais pas de toucher aux interfaces standards. Microsoft ne recommande pas d'appeler directement les fonctions natives et ne documente pas la plupart d'entre elles. D'ailleurs, des fonctions non documentées peuvent être trouvées ici.

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

Ainsi, les applications natives Windows sont des programmes qui peuvent démarrer tôt au démarrage de Windows. Ils utilisent UNIQUEMENT les fonctions de ntdll. Un exemple d'une telle application : autochtone qui exécute utilitaire chkdisk pour vérifier les erreurs sur le disque avant de démarrer les services principaux. C’est exactement le niveau que nous voulons que notre restauration active soit.

De quoi avons nous besoin?

  • DDK (Driver Development Kit), désormais également connu sous le nom de WDK 7 (Windows Driver Kit).
  • Machine virtuelle (par exemple, Windows 7 x64)
  • Pas nécessaire, mais les fichiers d'en-tête téléchargeables peuvent aider ici

Qu'y a-t-il dans le code ?

Pratiquons un peu et, par exemple, écrivons une petite application qui :

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

Dans les applications natives, le point d'entrée n'est pas main ou winmain, mais la fonction NtProcessStartup, puisque nous lançons en fait directement de nouveaux processus dans le système.

Commençons par afficher un message à l'écran. Pour cela nous avons une fonction native NtDisplayString, qui prend en argument un pointeur vers un objet de structure UNICODE_STRING. RtlInitUnicodeString nous aidera à l'initialiser. Du coup, pour afficher du texte à l'écran on peut écrire cette petite fonction :

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

Étant donné que seules les fonctions de ntdll sont disponibles et qu'il n'y a tout simplement pas encore d'autres bibliothèques en mémoire, nous aurons certainement des problèmes avec la façon d'allouer de la mémoire. L'opérateur new n'existe pas encore (car il vient du monde trop haut niveau du C++), et il n'y a pas de fonction malloc (il nécessite des bibliothèques d'exécution C). Bien entendu, vous ne pouvez utiliser qu’une pile. Mais si nous devons allouer dynamiquement de la mémoire, nous devrons le faire sur le tas (c'est-à-dire le tas). Créons donc un tas pour nous-mêmes et retirons-en la mémoire chaque fois que nous en avons besoin.

La fonction est adaptée à cette tâche RtlCréerHeap. Ensuite, 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"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 c'est d'utiliser NtReadFichier sur un appareil ouvert, et attendez que le clavier nous renvoie une pression. Si la touche ESC est enfoncée, nous continuerons à travailler. Pour ouvrir l'appareil, nous devrons appeler la fonction NtCreateFile (nous devrons ouvrir DeviceKeyboardClass0). Nous appellerons également NtCreateEventpour initialiser l'objet d'attente. Nous déclarerons nous-mêmes 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 de fonction NtTerminateProcessparce que 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 la fonction DbgBreakPoint() dans notre code pour l'arrêter dans le débogueur. Certes, vous devrez connecter WinDbg à une machine virtuelle pour le débogage du noyau. Des instructions sur la façon de procéder peuvent être trouvées ici ou utilisez simplement VirtualKD.

Compilation et assemblage

Le moyen le plus simple de créer une application native est d'utiliser DDK (Kit de développement de pilotes). Nous avons besoin 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 le DDK, alors notre projet n'a besoin que du Makefile et des 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:WinDDK7600.16385.1ndk;

TARGETLIBS 			= $(DDK_LIB_PATH)ntdll.lib	
				  $(DDK_LIB_PATH)nt.lib

USE_NTDLL			= 1

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

  • TARGETNAME – le nom du fichier exécutable qui doit être produit à la fin.
  • TARGETTYPE – type de fichier exécutable, il peut s'agir d'un pilote (.sys), alors la valeur du champ doit être DRIVER, s'il s'agit d'une 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, windows pour travailler en mode fenêtré. Mais nous devons spécifier nt pour obtenir une application native.
  • BUFFER_OVERFLOW_CHECKS – vérification de la pile pour un débordement de tampon, malheureusement pas notre cas, nous la 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 déclaré une telle variable système, lorsque nous exécuterons une version vérifiée à partir du DDK, cette variable sera déclarée et pointera vers les bibliothèques nécessaires.
  • SOURCES – une liste de sources pour votre programme.
  • COMPREND – les fichiers d’en-tête requis pour l’assemblage. Ici, ils indiquent généralement le chemin d'accès aux fichiers fournis avec le DDK, mais vous pouvez également en spécifier d'autres.
  • TARGETLIBS – liste des bibliothèques qui doivent être liées.
  • USE_NTDLL est un champ obligatoire qui doit être défini sur 1 pour des raisons évidentes.
  • USER_C_FLAGS – tous les indicateurs que vous pouvez utiliser dans les directives du préprocesseur lors de la préparation du code d'application.

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

Applications natives Windows et service Acronis Active Restore

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

Applications natives Windows et service Acronis Active Restore

Comment lancer une application native ?

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

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

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

autocheck autochk *MyNative

La valeur doit être au format hexadécimal, et non au format ASCII habituel, donc la clé affichée ci-dessus sera au 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 titre, vous pouvez utiliser un service en ligne, par exemple : cette.

Applications natives Windows et service Acronis Active Restore
Il s'avère que pour lancer une application native, il nous faut :

  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 installer une application native :

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pause

ajouter.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,00

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

Applications natives Windows et service Acronis Active Restore

Total

En prenant l'exemple d'une si petite application, nous étions convaincus qu'il est tout à fait possible d'exécuter l'application au niveau Windows Native. Ensuite, les gars de l'Université d'Innopolis et moi continuerons à créer un service qui lancera 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 serait logique de transférer le contrôle à un service à part entière déjà développé (plus d'informations à 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 article.

Source: habr.com

Ajouter un commentaire