Applicazioni native Windows e servizio Acronis Active Restore

Oggi continuiamo a parlare di come stiamo sviluppando insieme ai ragazzi dell'Università Innopolis la tecnologia Active Restore, per permettere all'utente di iniziare a lavorare sulla propria macchina il prima possibile dopo un guasto. Parleremo delle applicazioni native Windows, comprese le caratteristiche della loro creazione e esecuzione. Di seguito, un po' del nostro progetto e una guida pratica su come scrivere applicazioni native.

Applicazioni native Windows e servizio Acronis Active Restore

Nei post precedenti abbiamo già parlato di cosa sia Active Restore, e di come gli studenti di Innopolis lo stiano sviluppando. servizioOggi voglio concentrarmi sulle applicazioni native, ai livelli delle quali vogliamo 'affondare' il nostro servizio di ripristino attivo. Se tutto va bene, saremo in grado di:

  • Avviare il servizio molto prima
  • Collegarci al cloud dove si trova il backup molto prima
  • Capire molto prima in quale modalità si trova il sistema – avvio normale o ripristino
  • Ripristinare molto meno file in anticipo
  • Consentire all'utente di iniziare a lavorare ancora più rapidamente.

Cos'è esattamente un'applicazione nativa?

Per rispondere a questa domanda, diamo un'occhiata alla sequenza di chiamate che il sistema effettua, ad esempio, quando un programmatore tenta di creare un file nella sua applicazione.

Applicazioni native Windows e servizio Acronis Active Restore
Pavel Yosifovich — Windows Kernel Programming (2019)

Il programmatore utilizza la funzione CreateFile, che è dichiarata nel file di intestazione fileapi.h e implementata in Kernel32.dll. Tuttavia, questa funzione non si occupa della creazione del file, ma verifica solo gli argomenti in ingresso e chiama la funzione NtCreateFile (il prefisso Nt indica che la funzione è nativa). Questa funzione è dichiarata nel file di intestazione winternl.h e implementata in ntdll.dll. Essa prepara il salto nello spazio kernel e quindi effettua una chiamata di sistema per creare il file. In questo caso risulta che Kernel32 è solo un involucro per Ntdll. Una delle ragioni per cui è stato fatto questo è che Microsoft può modificare le funzioni del mondo nativo senza toccare le interfacce standard. Microsoft sconsiglia di chiamare direttamente le funzioni native e non documenta la maggior parte di esse. Tra l'altro, le funzioni non documentate possono essere trovate qui.

Il principale vantaggio delle applicazioni native è che ntdll viene caricato nel sistema molto prima di kernel32. Questo è logico, poiché kernel32 richiede la presenza di ntdll per funzionare. Di conseguenza, le applicazioni che utilizzano funzioni native possono avviarsi molto prima.

Pertanto, le Applicazioni Native di Windows sono programmi capaci di avviarsi nelle fasi iniziali di caricamento di Windows. Utilizzano SOLO funzioni da ntdll. Un esempio di tale applicazione è: autochk che esegue l'utility chkdisk per controllare il disco per errori ancor prima dell'avvio dei servizi principali. È proprio a questo livello che desideriamo vedere il nostro Active Restore.

Di cosa abbiamo bisogno?

  • DDK (Driver Development Kit), ora anche noto come WDK 7 (Windows Driver Kit).
  • Una macchina virtuale (ad esempio, Windows 7 x64)
  • Non obbligatorio, ma possono essere utili i file header che è possibile scaricare. qui

Cosa c'è nel codice?

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

  1. Mostra un messaggio sullo schermo
  2. Alloca un po' di memoria
  3. Attende un'input da tastiera
  4. Libera la memoria occupata

Negli applicativi nativi, il punto di ingresso non è main o winmain, ma la funzione NtProcessStartup, poiché stiamo effettivamente avviando direttamente nuovi processi nel sistema.

Iniziamo col visualizzare un messaggio sullo schermo. A tal fine, abbiamo a disposizione la funzione nativa NtDisplayString, che accetta come argomento un puntatore a un oggetto della struttura UNICODE_STRING. Possiamo inizializzarlo con RtlInitUnicodeString. Di conseguenza, per visualizzare il testo sullo schermo possiamo scrivere una piccola funzione come questa:

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

Poiché abbiamo accesso solo alle funzioni di ntdll e non ci sono altre librerie in memoria, ci troveremo sicuramente di fronte a problemi su come allocare memoria. L’operatore new non esiste ancora (perché proviene da un mondo troppo di alto livello C++), e non c'è nemmeno la funzione malloc (che richiede librerie runtime C). Possiamo ovviamente utilizzare solo lo stack. Ma se abbiamo bisogno di allocare memoria dinamicamente, dovremo farlo nell'heap. Pertanto, creiamo un'area di heap e prendiamo memoria da essa quando ne abbiamo bisogno.

Per questo compito è adatta la funzione RtlCreateHeap. Quindi, utilizzando RtlAllocateHeap e RtlFreeHeap, occuperemo e libereremo la memoria quando ne avremo bisogno.

PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;

// crea un heap per allocare memoria in seguito
memory = RtlCreateHeap(
  HEAP_GROWABLE,
  NULL,
  1000,
  0, NULL, NULL
);

// alloca un buffer di dimensione bufferSize
buffer = RtlAllocateHeap(
  memory,
  HEAP_ZERO_MEMORY,
  bufferSize
);

// libera il buffer (in effetti non è necessario perché distruggiamo l'heap nel passo successivo)
RtlFreeHeap(memory, 0, buffer);

RtlDestroyHeap(memory);

Passiamo ad aspettare l'input dalla 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 è utilizzare NtReadFile sul dispositivo aperto e aspettare finché la tastiera non ci restituirà un qualsiasi tasto premuto. Se viene premuto il tasto ESC, continueremo a lavorare. Per aprire il dispositivo, dovremo chiamare la funzione NtCreateFile (dobbiamo aprire DeviceKeyboardClass0). Inoltre, chiameremo NtCreateEvent, per inizializzare un oggetto per l'attesa. Dichiareremo noi stessi la struttura KEYBOARD_INPUT_DATA, che rappresenta i dati della tastiera. Questo ci semplificherà il lavoro.

Il lavoro dell'applicazione nativa termina con la chiamata alla funzione NtTerminateProcess, perché uccidiamo semplicemente il nostro stesso processo.

Tutto il codice della 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 nel codice la funzione DbgBreakPoint() per fermarci nel debugger. Tuttavia, sarà necessario collegare WinDbg alla macchina virtuale per la debug di kernel. Puoi trovare le istruzioni su come farlo qui o semplicemente usare VirtualKD.

Compilazione e costruzione

Il modo più semplice per costruire un'applicazione nativa è utilizzare DDK (Driver Development Kit). Abbiamo bisogno della versione settima antica, poiché le versioni più recenti seguono un approccio leggermente diverso e lavorano a stretto contatto con Visual Studio. Se utilizzi DDK, il nostro progetto ha bisogno solo di Makefile e sorgenti.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

sorgenti:

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; per quanto riguarda le sorgenti, fermiamoci un attimo su questo. In questo file vengono specificate le sorgenti del tuo programma (file .c), le opzioni di compilazione e altri parametri.

  • TARGETNAME – il nome del file eseguibile che deve risultare alla fine.
  • TARGETTYPE – tipo di file eseguibile, può essere un driver (.sys), in tal caso il valore del campo deve essere DRIVER; se è una libreria (.lib), allora il valore deve essere LIBRARY. Nel nostro caso abbiamo bisogno di un file eseguibile (.exe), quindi impostiamo il valore PROGRAM.
  • UMTYPE – i possibili valori di questo campo sono: console per le applicazioni console, windows per quelle che operano in modalità finestra. Ma dobbiamo specificare nt per ottenere un'applicazione nativa.
  • BUFFER_OVERFLOW_CHECKS – controllo dello stack per buffer overflow, purtroppo non è il nostro caso, lo disattiviamo.
  • MINWIN_SDK_LIB_PATH – questo valore si riferisce alla variabile SDK_LIB_PATH; non è necessario preoccuparsi se non avete dichiarato tale variabile di sistema, al momento in cui eseguiremo il build controllato dal DDK, questa variabile sarà dichiarata e indicherà le librerie necessarie.
  • SOURCES – l'elenco dei file sorgente della vostra programma.
  • INCLUDES – i file di intestazione necessari per la compilazione. Qui di solito si indica il percorso ai file forniti con il DDK, ma è possibile specificare ulteriormente altri file.
  • TARGETLIBS – l'elenco delle librerie da linkare.
  • USE_NTDLL – campo obbligatorio che deve essere impostato su 1. Per motivi abbastanza ovvi.
  • USER_C_FLAGS – eventuali flag che puoi utilizzare nelle direttive del preprocessore durante la preparazione del codice dell'applicazione.

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

Applicazioni native Windows e servizio Acronis Active Restore

Questo file non può essere semplicemente avviato, il sistema restituisce un errore e ci invita a riflettere sul suo comportamento con il seguente messaggio:

Applicazioni native Windows e servizio Acronis Active Restore

Come avviare un'applicazione nativa?

Al momento dell'avvio, l'ordine di esecuzione dei programmi è determinato dal valore della chiave di registro:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Il gestore delle sessioni esegue sequentialmente i programmi da questo elenco. I file eseguibili sono cercati dal gestore delle sessioni nella directory system32. Il formato del valore della chiave di registro è il seguente:

autocheck autochk *MyNative

Il valore deve essere in formato esadecimale, e non nel consueto formato ASCII, pertanto la chiave mostrata sopra avrà il 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 nome, è possibile utilizzare un servizio online, come ad esempio, questo.

Applicazioni native Windows e servizio Acronis Active Restore
Quindi, per avviare un'applicazione nativa, è necessario:

  1. Copiare il file eseguibile nella cartella system32
  2. Aggiungere una chiave nel registro
  3. Riavviare il computer

Per comodità, ecco uno script pronto per l'installazione dell'applicazione nativa:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Esempio nativo installato
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, ancor prima della comparsa della schermata di selezione utenti, avremo la seguente situazione:

Applicazioni native Windows e servizio Acronis Active Restore

Risultato

Attraverso l'esempio di una piccola applicazione, abbiamo dimostrato che è possibile avviare un'applicazione a livello Windows Native. In seguito, insieme ai ragazzi dell'Università di Innopolis, continueremo a costruire un servizio che avvierà il processo di interazione con il driver molto prima rispetto alla versione precedente del nostro progetto. Con l'arrivo dell'interfaccia win32, sarà logico trasferire il controllo a un servizio completo che è già stato sviluppato (di questo parleremo in dettaglio qui).

In questo nuovo articolo esploreremo un altro componente del servizio Active Restore, in particolare il driver UEFI. Iscriviti al nostro blog per non perdere il prossimo post.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster