Oggi continuiamo a raccontare di come stiamo sviluppando insieme ai ragazzi dell'Università di Innopolis la tecnologia Active Restore, per consentire all'utente di tornare a lavorare sulla propria macchina il prima possibile dopo un crash. Parleremo delle applicazioni native per Windows, comprese le peculiarità della loro creazione e lancio. Dopo il salto – un po' del nostro progetto e anche una guida pratica su come scrivere applicazioni native.

Nei post precedenti abbiamo già parlato di cosa sia , e di come gli studenti di Innopolis stiano sviluppando . Oggi voglio soffermarmi sulle applicazioni native, a cui vogliamo “affondare” il nostro servizio di ripristino attivo. Se tutto va bene, saremo in grado di:
- Avviare il servizio molto prima
- Contattare il cloud, dove è presente il backup, molto prima
- Capire molto prima in quale modalità si trova il sistema – avvio normale o ripristino
- Ripristinare molti meno file in anticipo
- Consentire all'utente di iniziare a lavorare ancora più rapidamente.
Che cos'è esattamente un'applicazione nativa?
Per rispondere a questa domanda, diamo un'occhiata alla sequenza di chiamate che il sistema esegue, ad esempio, se uno sviluppatore sta cercando di creare un file nella propria applicazione.

Pavel Yosifovich — Windows Kernel Programming (2019)
Lo sviluppatore utilizza la funzione , che è dichiarata nel file di intestazione fileapi.h e implementata in Kernel32.dll. Tuttavia, questa funzione non si occupa della creazione del file; verifica solo gli argomenti in ingresso e chiama la funzione (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 del kernel e poi effettua una chiamata di sistema per creare il file. In questo caso, Kernel32 è solo un involucro per Ntdll. Una delle ragioni per cui è stato fatto questo è che Microsoft ha la possibilità di modificare le funzioni del mondo nativo senza toccare le interfacce standard. Microsoft non raccomanda di chiamare direttamente le funzioni native e non documenta la maggior parte di esse. A proposito, le funzioni non documentate possono essere trovate .
Il principale vantaggio delle applicazioni native è che ntdll viene caricato nel sistema molto prima di kernel32. È logico, poiché kernel32 richiede la presenza di ntdll per funzionare. Di conseguenza, le applicazioni che utilizzano funzioni native possono iniziare a funzionare molto prima.
Pertanto, le Windows Native Applications sono programmi in grado di avviarsi nelle fasi iniziali di caricamento di Windows. Utilizzano SOLO funzioni da ntdll. Un esempio di tale applicazione: che esegue per controllare il disco per errori ancora prima che i servizi principali vengano avviati. È proprio a questo livello che vogliamo vedere il nostro Active Restore.
Di cosa abbiamo bisogno?
- (Driver Development Kit), ora noto anche come WDK 7 (Windows Driver Kit).
- Una macchina virtuale (ad esempio, Windows 7 x64)
- Non è essenziale, ma potrebbero essere utili gli header file che possono essere scaricati
E cosa c'è nel codice?
Facciamo un po' di pratica e come esempio scriviamo una piccola applicazione che:
- Visualizza un messaggio sullo schermo
- Alloca un po' di memoria
- Aspetta un input da tastiera
- Libera la memoria allocata
Nelle applicazioni native, il punto di ingresso non è main o winmain, ma la funzione NtProcessStartup, poiché stiamo effettivamente avviando direttamente un nuovo processo nel sistema.
Iniziamo con la visualizzazione di un messaggio sullo schermo. A tal fine, abbiamo a disposizione la funzione nativa , che come argomento accetta un puntatore a un oggetto della struttura UNICODE_STRING. Possiamo inizializzarlo con RtlInitUnicodeString. Di conseguenza, per visualizzare del 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 ad affrontare problemi su come allocare memoria. L'operatore new non esiste ancora (perché proviene da un mondo troppo ad alto livello di C++), inoltre non c'è la funzione malloc (che ha bisogno delle librerie runtime di C). Possiamo ovviamente usare solo lo stack. Ma se dobbiamo allocare dinamicamente memoria, dovremo farlo nel heap. Pertanto, creiamo un heap per noi e preleviamo memoria da esso quando ne avremo bisogno.
Per questo compito, la funzione adatta è . Successivamente, utilizzando RtlAllocateHeap e RtlFreeHeap, ci occuperemo di allocare e liberare memoria quando ci sarà necessario.
PVOID memoria = NULL;
PVOID buffer = NULL;
ULONG dimensioneBuffer = 42;
// Crea un heap per allocare memoria in seguito
memoria = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// Alloca un buffer di dimensione dimensioneBuffer
buffer = RtlAllocateHeap(
memoria,
HEAP_ZERO_MEMORY,
dimensioneBuffer
);
// Libera il buffer (in realtà non necessario perché distruggiamo l'heap nel passo successivo)
RtlFreeHeap(memoria, 0, buffer);
RtlDestroyHeap(memoria);Passiamo all'attesa di 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 è utilizzare su un dispositivo aperto e aspettare che la tastiera ci restituisca qualche pressione di tasto. Nel caso in cui venga premuto il tasto ESC, continueremo a lavorare. Per aprire il dispositivo, dovremo chiamare la funzione NtCreateFile (dobbiamo aprire DeviceKeyboardClass0). Inoltre, calleremo , per inizializzare un oggetto per l'attesa. Dichiareremo autonomamente 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 , perché stiamo semplicemente terminando il nostro stesso processo.
L'intero 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 usare nel codice la funzione DbgBreakPoint() per fermarci nel debugger. Dobbiamo però connettere WinDbg alla macchina virtuale per il debugging del kernel. Puoi trovare le istruzioni su come fare o semplicemente usare .
Compilazione e costruzione
Il modo più semplice per compilare un'applicazione nativa è usare (Driver Development Kit). Abbiamo bisogno esattamente della vecchia settima versione, poiché le versioni più recenti hanno un approccio leggermente diverso e lavorano a stretto contatto con Visual Studio. Se usiamo DDK, il nostro progetto ha bisogno solo di un Makefile e di fonti.
Makefile
!INCLUDE $(NTMAKEENV)makefile.deffonti:
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 = 1Il tuo Makefile sarà esattamente lo stesso, mentre fermiamoci un attimo sulle fonti. 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 dovrebbe risultare alla fine.
- TARGETTYPE – il tipo di file eseguibile, può essere un driver (.sys), quindi il valore del campo deve essere DRIVER, se è una libreria (.lib), allora il valore LIBRARY. Nel nostro caso serve un file eseguibile (.exe), quindi impostiamo il valore su PROGRAM.
- UMTYPE – possibili valori di questo campo: console per applicazioni console, windows per modalità finestra. Ma dobbiamo specificare nt per ottenere un'applicazione nativa.
- BUFFER_OVERFLOW_CHECKS – verifica dello stack per overflow del buffer, purtroppo non è il nostro caso, disattiviamo.
- MINWIN_SDK_LIB_PATH – questo valore fa riferimento alla variabile SDK_LIB_PATH, non preoccupatevi se non avete dichiarato una variabile di sistema simile, al momento in cui eseguiremo una build controllata da DDK, questa variabile verrà dichiarata e punterà alle librerie necessarie.
- SOURCES – elenco dei sorgenti della vostra programma.
- INCLUDES – file header necessari per la compilazione. Qui solitamente si indica il percorso ai file forniti con DDK, ma potete anche specificare ulteriori file.
- TARGETLIBS – elenco delle librerie da collegare.
- USE_NTDLL – campo obbligatorio che deve essere impostato a 1. Per ragioni ovvie.
- USER_C_FLAGS – eventuali flag che potrete utilizzare nelle direttive del preprocessore durante la preparazione del codice dell'applicazione.
Quindi, per compilarlo dobbiamo avviare 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 ottenuto un file eseguibile.

Non sarà così semplice avviare questo file, il sistema restituisce un errore e ci invita a riflettere sul suo comportamento con il seguente messaggio:

Come avviare un'applicazione nativa?
All'avvio di autochk, la sequenza di avvio dei programmi è determinata dal valore della chiave di registro:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteIl gestore delle sessioni esegue a turno i programmi di 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 *MyNativeIl valore deve essere in formato esadecimale, non in quello ASCII tradizionale, quindi la chiave presentata 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,00Per convertire il nome, potete utilizzare un servizio online, ad esempio, .

Quindi, per avviare un'applicazione nativa, dobbiamo:
- Copiare il file eseguibile nella cartella system32
- Aggiungere una chiave nel registro
- Riavviare la macchina
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 di Native Installato
pauseadd.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,00Dopo l'installazione e il riavvio, prima ancora dell'apparizione dello schermo di selezione degli utenti, avremo la seguente situazione:

Risultato
Nel caso di questa piccola applicazione, abbiamo dimostrato che è possibile avviare un'applicazione a livello di Windows Native. In seguito, con i ragazzi dell'Università di Innopolis, continueremo a costruire un servizio che inizierà il processo di interazione con il driver molto prima rispetto alla versione precedente del nostro progetto. E con l'arrivo dell'interfaccia win32 sarà logico trasferire il controllo a un servizio completo che è già stato sviluppato (di questo ne parleremo più dettagliatamente). ).
In un prossimo articolo affronteremo un altro componente del servizio Active Restore, ovvero il driver UEFI. Seguite il nostro blog per non perdere il prossimo post.
Fonte: habr.com
