Applicazioni native di Windows e servizio Acronis Active Restore

Oggi continuiamo la storia di come noi, insieme ai ragazzi dell'Università di Innopolis, stiamo sviluppando la tecnologia Active Restore per consentire all'utente di iniziare a lavorare sulla propria macchina il prima possibile dopo un guasto. Parleremo delle applicazioni Windows native, comprese le funzionalità della loro creazione e avvio. Sotto il taglio c'è qualcosa sul nostro progetto, oltre a una guida pratica su come scrivere applicazioni native.

Applicazioni native di Windows e servizio Acronis Active Restore

Nei post precedenti abbiamo già parlato di cosa si tratta Ripristino attivoe come si sviluppano gli studenti di Innopolis servizio. Oggi voglio concentrarmi sulle applicazioni native, al livello delle quali vogliamo "seppellire" il nostro servizio di ripristino attivo. Se tutto funziona, allora saremo in grado di:

  • Avvia il servizio stesso molto prima
  • Contatta il cloud in cui si trova il backup molto prima
  • Molto prima per capire in quale modalità si trova il sistema: avvio normale o ripristino
  • Molti meno file da recuperare in anticipo
  • Consenti all'utente di iniziare ancora più velocemente.

Cos'è comunque un'app nativa?

Per rispondere a questa domanda, guardiamo la sequenza di chiamate che il sistema effettua, ad esempio, se un programmatore nella sua applicazione tenta di creare un file.

Applicazioni native di Windows e servizio Acronis Active Restore
Pavel Yosifovich - Programmazione del kernel di Windows (2019)

Il programmatore utilizza la funzione CreateFile, che è dichiarato nel file di intestazione fileapi.h e implementato in Kernel32.dll. Tuttavia, questa funzione stessa non crea il file, controlla solo gli argomenti di input e chiama la funzione NtCreateFile (il prefisso Nt indica semplicemente che la funzione è nativa). Questa funzione è dichiarata nel file di intestazione winternl.h e implementata in ntdll.dll. Si prepara a saltare nello spazio nucleare, dopodiché effettua una chiamata di sistema per creare un file. In questo caso risulta che Kernel32 è solo un wrapper per Ntdll. Uno dei motivi per cui ciò è stato fatto è che Microsoft ha così la capacità di modificare le funzioni del mondo nativo, ma non di toccare le interfacce standard. Microsoft sconsiglia di chiamare direttamente le funzioni native e non ne documenta la maggior parte. A proposito, è possibile trovare funzioni non documentate qui.

Il vantaggio principale delle applicazioni native è che ntdll viene caricato nel sistema molto prima rispetto a kernel32. Questo è logico, perché kernel32 richiede che ntdll funzioni. Di conseguenza, le applicazioni che utilizzano funzioni native possono iniziare a funzionare molto prima.

Pertanto, le applicazioni native di Windows sono programmi che possono essere avviati nelle prime fasi dell'avvio di Windows. Usano SOLO funzioni di NTDll. Un esempio di tale applicazione: autoc chi esegue utilità chkdisk per verificare la presenza di errori sul disco prima di avviare i servizi principali. Questo è esattamente il livello che vogliamo che sia il nostro ripristino attivo.

Cosa abbiamo bisogno?

  • DDK (Driver Development Kit), ora noto anche come WDK 7 (Windows Driver Kit).
  • Macchina virtuale (ad esempio, Windows 7 x64)
  • Non necessario, ma i file di intestazione che possono essere scaricati possono essere d'aiuto qui

Cosa c'è nel codice?

Facciamo un po' di pratica e, ad esempio, scriviamo una piccola applicazione che:

  1. Visualizza un messaggio sullo schermo
  2. Assegna parte della memoria
  3. Attende l'input da tastiera
  4. Libera la memoria utilizzata

Nelle applicazioni native, il punto di ingresso non è main o winmain, ma la funzione NtProcessStartup, poiché in realtà lanciamo direttamente i nuovi processi nel sistema.

Iniziamo visualizzando un messaggio sullo schermo. Per questo abbiamo una funzione nativa NtDisplayString, che accetta come argomento un puntatore a un oggetto struttura UNICODE_STRING. RtlInitUnicodeString ci aiuterà a inizializzarlo. Di conseguenza, per visualizzare il testo sullo schermo possiamo scrivere questa piccola funzione:

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

Poiché abbiamo a disposizione solo le funzioni di NTDll e semplicemente non ci sono ancora altre librerie in memoria, avremo sicuramente problemi con come allocare la memoria. L'operatore new non esiste ancora (perché proviene dal mondo di livello troppo alto del C++) e non esiste una funzione malloc (richiede librerie C runtime). Ovviamente puoi usare solo uno stack. Ma se abbiamo bisogno di allocare dinamicamente la memoria, dovremo farlo sull'heap (cioè heap). Quindi creiamo un mucchio per noi stessi e prendiamone memoria ogni volta che ne abbiamo bisogno.

La funzione è adatta a questo compito RtlCreateHeap. Successivamente, utilizzando RtlAllocateHeap e RtlFreeHeap, occuperemo e libereremo memoria quando ne avremo bisogno.

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

Passiamo all'attesa dell'input da tastiera.

// 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;
	}
}

Tutto ciò di cui abbiamo bisogno è usare NtReadFile su un dispositivo aperto e attendi fino a quando la tastiera non ci restituisce qualsiasi pressione. Se si preme il tasto ESC, continueremo a lavorare. Per aprire il dispositivo dovremo chiamare la funzione NtCreateFile (dovremo aprire DeviceKeyboardClass0). Chiameremo anche noi NtCreateEventper inizializzare l'oggetto wait. Dichiareremo noi stessi la struttura KEYBOARD_INPUT_DATA, che rappresenta i dati della tastiera. Ciò renderà il nostro lavoro più semplice.

L'applicazione nativa termina con una chiamata di funzione NtTerminateProcessperché stiamo semplicemente uccidendo il nostro stesso processo.

Tutto il codice per la nostra piccola applicazione:

#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: Possiamo facilmente utilizzare la funzione DbgBreakPoint() nel nostro codice per fermarlo nel debugger. È vero, dovrai connettere WinDbg a una macchina virtuale per il debug del kernel. È possibile trovare istruzioni su come eseguire questa operazione qui o semplicemente usare VirtualKD.

Compilazione e assemblaggio

Il modo più semplice per creare un'applicazione nativa è utilizzare DDK (Kit di sviluppo driver). Abbiamo bisogno dell'antica settima versione, poiché le versioni successive hanno un approccio leggermente diverso e lavorano a stretto contatto con Visual Studio. Se utilizziamo il DDK, il nostro progetto necessita solo di Makefile e sorgenti.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

fonti:

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

Il tuo Makefile sarà esattamente lo stesso, ma diamo un'occhiata alle fonti un po' più in dettaglio. Questo file specifica le origini del programma (file .c), le opzioni di creazione e altri parametri.

  • TARGETNAME – il nome del file eseguibile che dovrebbe essere prodotto alla fine.
  • TARGETTYPE – tipo di file eseguibile, può essere un driver (.sys), quindi il valore del campo dovrebbe essere DRIVER, se una libreria (.lib), quindi il valore è LIBRARY. Nel nostro caso abbiamo bisogno di un file eseguibile (.exe), quindi impostiamo il valore su PROGRAM.
  • UMTYPE – valori possibili per questo campo: console per un'applicazione console, windows per lavorare in modalità finestra. Ma dobbiamo specificare nt per ottenere un'applicazione nativa.
  • BUFFER_OVERFLOW_CHECKS – controllando lo stack per l'overflow del buffer, sfortunatamente non è il nostro caso, lo disattiviamo.
  • MINWIN_SDK_LIB_PATH – questo valore si riferisce alla variabile SDK_LIB_PATH, non preoccuparti se non hai dichiarato tale variabile di sistema, quando eseguiamo la build verificata dal DDK, questa variabile verrà dichiarata e punterà alle librerie necessarie.
  • FONTI – un elenco di fonti per il tuo programma.
  • INCLUDE: file di intestazione necessari per l'assemblaggio. Qui di solito indicano il percorso dei file forniti con il DDK, ma puoi specificarne anche altri.
  • TARGETLIBS – elenco delle librerie che devono essere collegate.
  • USE_NTDLL è un campo obbligatorio che deve essere impostato su 1 per ovvi motivi.
  • USER_C_FLAGS – qualsiasi flag che è possibile utilizzare nelle direttive del preprocessore durante la preparazione del codice dell'applicazione.

Quindi, per creare, dobbiamo eseguire x86 (o x64) Checked Build, cambiare la directory di lavoro nella cartella del progetto ed eseguire il comando Build. Il risultato nello screenshot mostra che abbiamo un file eseguibile.

Applicazioni native di Windows e servizio Acronis Active Restore

Questo file non può essere avviato così facilmente, il sistema impreca e ci manda a riflettere sul suo comportamento con il seguente errore:

Applicazioni native di Windows e servizio Acronis Active Restore

Come avviare un'applicazione nativa?

All'avvio del controllo automatico, la sequenza di avvio dei programmi è determinata dal valore della chiave di registro:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Il gestore della sessione esegue i programmi da questo elenco uno per uno. Il gestore della sessione cerca i file eseguibili nella directory system32. Il formato del valore della chiave del registro è il seguente:

autocheck autochk *MyNative

Il valore deve essere in formato esadecimale, non nel solito ASCII, quindi la chiave mostrata sopra sarà nel formato:

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

Per convertire il titolo è possibile utilizzare un servizio online, ad esempio, questo.

Applicazioni native di Windows e servizio Acronis Active Restore
Risulta che per avviare un'applicazione nativa, abbiamo bisogno di:

  1. Copia il file eseguibile nella cartella system32
  2. Aggiungi una chiave al registro
  3. Riavviare la macchina

Per comodità, ecco uno script già pronto per l'installazione di un'applicazione nativa:

install.bat

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

add.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

Dopo l'installazione e il riavvio, anche prima che venga visualizzata la schermata di selezione dell'utente, otterremo la seguente immagine:

Applicazioni native di Windows e servizio Acronis Active Restore

risultato

Usando l'esempio di un'applicazione così piccola, eravamo convinti che fosse del tutto possibile eseguire l'applicazione a livello nativo di Windows. Successivamente, io e i ragazzi dell'Università di Innopolis continueremo a costruire un servizio che avvierà il processo di interazione con l'autista molto prima rispetto alla versione precedente del nostro progetto. E con l'avvento della shell win32, sarebbe logico trasferire il controllo a un servizio a tutti gli effetti che è già stato sviluppato (maggiori informazioni su questo argomento) qui).

Nel prossimo articolo toccheremo un altro componente del servizio Active Restore, ovvero il driver UEFI. Iscriviti al nostro blog per non perdere il prossimo post.

Fonte: habr.com

Aggiungi un commento