Oggi proseguiremo la nostra storia su come stiamo collaborando con l'Università Innopolis per sviluppare la tecnologia Active Restore, che permette agli utenti di riprendere a lavorare sui propri computer il più rapidamente possibile dopo un arresto anomalo. Parleremo di app native. Windows, inclusi i dettagli della loro creazione e del loro lancio. Di seguito, troverete alcune informazioni di base sul nostro progetto, oltre a una guida pratica su come scrivere app native.

Nei post precedenti abbiamo già parlato di cosa si tratta e come si sviluppano gli studenti di Innopolis . 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.

Pavel Yosifovich — Windows Programmazione del kernel (2019)
Il programmatore utilizza la funzione , 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 (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 .
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.
Così, la Windows Le applicazioni native sono programmi che possono essere eseguiti nelle prime fasi di avvio del sistema. WindowsUtilizzano SOLO funzioni della libreria ntdll. Ecco un esempio di tale applicazione: chi esegue 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?
- (Driver Development Kit), ora noto anche come WDK 7 (Windows Kit di guida).
- Macchina virtuale (ad es. Windows 7 x64)
- Non necessario, ma i file di intestazione che possono essere scaricati possono essere d'aiuto
Cosa c'è nel codice?
Facciamo un po' di pratica e, ad esempio, scriviamo una piccola applicazione che:
- Visualizza un messaggio sullo schermo
- Assegna parte della memoria
- Attende l'input da tastiera
- 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 , 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 . 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 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 per 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 perché 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 o semplicemente usare .
Compilazione e assemblaggio
Il modo più semplice per creare un'applicazione nativa è utilizzare (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.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, 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.

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

Come avviare un'applicazione nativa?
All'avvio del controllo automatico, la sequenza di avvio dei programmi è determinata dal valore della chiave di registro:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteIl 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 *MyNativeIl 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,00Per convertire il titolo è possibile utilizzare un servizio online, ad esempio, .

Risulta che per avviare un'applicazione nativa, abbiamo bisogno di:
- Copia il file eseguibile nella cartella system32
- Aggiungi una chiave al registro
- 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
pauseadd.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,00Dopo l'installazione e il riavvio, anche prima che venga visualizzata la schermata di selezione dell'utente, otterremo la seguente immagine:

risultato
Utilizzando questa piccola applicazione come esempio, ci siamo convinti che lanciare un'applicazione a livello Windows L'implementazione nativa è assolutamente possibile. Successivamente, io e i ragazzi dell'Università di Innopolis continueremo a sviluppare un servizio che avvierà l'interazione con il driver molto prima rispetto alla versione precedente del nostro progetto. E con l'avvento della shell Win32, sarà logico trasferire il controllo a un servizio completo già sviluppato (ne parleremo più avanti). ).
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
