Applications nativement Windows et service Acronis Active Restore

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.

Applications nativement Windows et service Acronis Active Restore

Dans les posts précédents, nous avons déjà expliqué ce qu'est Active Restore, et comment les étudiants d'Innopolis développent un service. 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.

Applications nativement Windows et service Acronis Active Restore
Pavel Yosifovich - Windows Kernel Programming (2019)

Le programmeur utilise la fonction CreateFile, 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 NtCreateFile (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. ici.

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 : autochk qui exĂ©cute l'outil chkdisk 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 ?

  • DDK (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. ici

Qu'en est-il du code ?

Entraßnons-nous un peu et, par exemple, écrivons une petite application qui :

  1. Affiche un message à l'écran
  2. Alloue un peu de mémoire
  3. Attend une entrée du clavier
  4. 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 NtDisplayString, 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 RtlCreateHeapest 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 NtReadFile 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 NtCreateEvent, 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 NtTerminateProcess, 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 ici ou simplement utiliser VirtualKD.

Compilation et assemblage

Le moyen le plus simple de compiler une application native est d'utiliser DDK (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.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, 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.

Applications nativement Windows et service Acronis Active Restore

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 :

Applications nativement Windows et service Acronis Active Restore

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 ManagerBootExecute

Le 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 *MyNative

La 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,00

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

Applications nativement Windows et service Acronis Active Restore
En résumé, pour exécuter une application native, nous devons :

  1. Copier le fichier exécutable dans le dossier system32
  2. Ajouter une clé dans le registre
  3. 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é
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 d'arriver Ă  l'Ă©cran de sĂ©lection des utilisateurs, nous aurons le tableau suivant :

Applications nativement Windows et service Acronis Active Restore

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

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

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster