Hoxe, continuaremos a nosa historia sobre como estamos a traballar coa Universidade de Innopolis para desenvolver a tecnoloxía Active Restore que permita aos usuarios retomar o traballo nas súas máquinas o máis rápido posible despois dun fallo. Falaremos de aplicacións nativas. Windows, incluíndo os detalles da súa creación e lanzamento. A continuación, ofrécese un pouco de información sobre o noso proxecto, así como unha guía práctica sobre como escribir aplicacións nativas.

En publicacións anteriores xa falamos do que é e como se están desenvolvendo os estudantes de Innopolis Hoxe gustaríame centrarme nas aplicacións nativas, o nivel no que queremos "enterrar" o noso servizo de recuperación activo. Se todo sae ben, poderemos:
- Lanzar o propio servizo moito antes
- Contacta coa nube onde se atopa a copia de seguridade moito antes
- Moito antes é posible comprender en que modo se atopa o sistema: arranque normal ou recuperación
- Moitos menos ficheiros para recuperar con antelación
- Permite que o usuario comece aínda máis rápido.
Que é, en realidade, unha aplicación nativa?
Para responder a esta pregunta, vexamos a secuencia de chamadas que realiza o sistema, por exemplo, se un programador na súa aplicación tenta crear un ficheiro.

Pavel Yosifovich — Windows Programación do núcleo (2019)
O programador usa a función , que se declara no ficheiro de cabeceira fileapi.h e se implementa en Kernel32.dll. Non obstante, esta función en si non crea o ficheiro; só comproba os argumentos de entrada e chama á función. (O prefixo Nt indica que a función é nativa). Esta función declárase no ficheiro de cabeceira winternl.h e impleméntase en ntdll.dll. Prepárase para o salto ao espazo do kernel, despois do cal realiza unha chamada ao sistema para crear o ficheiro. Neste caso, Kernel32 é simplemente un envoltorio para Ntdll. Unha das razóns disto é para que Microsoft poida modificar as funcións nativas sen afectar as interfaces estándar. Microsoft non recomenda chamar directamente ás funcións nativas e non documenta a maioría delas. Por certo, pódense atopar funcións non documentadas .
A principal vantaxe das aplicacións nativas é que ntdll se carga no sistema significativamente antes que kernel32. Isto é lóxico, xa que kernel32 require ntdll para funcionar. Como resultado, as aplicacións que usan funcións nativas poden comezar a funcionar significativamente antes.
Así, o Windows As aplicacións nativas son programas que se poden executar cedo durante o arranque. WindowsUsan SÓ funcións de ntdll. Un exemplo dunha aplicación deste tipo: que realiza Para comprobar se hai erros no disco antes de iniciar os servizos principais. Este é exactamente o nivel no que queremos que funcione a nosa Restauración activa.
Que necesitamos?
- (Kit de desenvolvemento de controladores), agora tamén coñecido como WDK 7 (Windows Kit de controladores).
- Máquina virtual (ex. Windows 7 x 64)
- Non é necesario, pero os ficheiros de cabeceira que se poden descargar poden axudar.
Que hai no código?
Practiquemos un pouco e escribamos unha pequena aplicación como exemplo que:
- Mostra unha mensaxe na pantalla
- Asigna algo de memoria
- Agardando pola entrada do teclado
- Libera memoria ocupada
Nas aplicacións nativas, o punto de entrada non é main nin winmain, senón a función NtProcessStartup, xa que en realidade estamos a iniciar directamente un novo proceso no sistema.
Comecemos mostrando unha mensaxe na pantalla. Temos unha función nativa para iso. , que toma un punteiro a un obxecto de estrutura UNICODE_STRING como argumento. RtlInitUnicodeString axudaranos a inicializalo. Como resultado, para mostrar texto na pantalla, podemos escribir unha pequena función coma esta:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Dado que só temos acceso ás funcións desde ntdll e aínda non hai outras bibliotecas na memoria, seguro que teremos problemas para asignar memoria. O operador new aínda non existe (porque provén do mundo de nivel demasiado alto de C++) e non hai ningunha función malloc (require bibliotecas C en tempo de execución). Por suposto, poderiamos usar simplemente a pila. Pero se precisamos asignar memoria dinamicamente, teremos que facelo no heap. Así que imos crear un heap para nós mesmos e coller memoria del cando a precisemos.
A función axeitada para esta tarefa é A continuación, usando RtlAllocateHeap e RtlFreeHeap, asignaremos e liberaremos memoria cando a precisemos.
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);Pasemos a esperar a entrada do teclado.
// 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;
}
}O único que temos que facer é usalo no dispositivo aberto e agardar a que o teclado devolva unha pulsación de tecla. Se se preme a tecla ESC, continuaremos traballando. Para abrir o dispositivo, teremos que chamar á función NtCreateFile (DeviceKeyboardClass0 debe estar aberta). Tamén chamaremos para inicializar o obxecto wait. Declararemos manualmente a estrutura KEYBOARD_INPUT_DATA, que representa os datos do teclado. Isto facilitaranos o traballo.
A aplicación nativa remata cunha chamada a función , porque simplemente estamos matando o noso propio proceso.
O código completo para a nosa pequena aplicación:
#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: Podemos usar facilmente a función DbgBreakPoint() no noso código para deter o depurador. Non obstante, teremos que conectar WinDbg á máquina virtual para a depuración do kernel. As instrucións sobre como facelo pódense atopar aquí. ou simplemente usar .
Compilación e montaxe
A forma máis sinxela de crear unha aplicación nativa é usar (Kit de desenvolvemento de controladores). Necesitamos especificamente a sétima versión antiga, xa que as versións posteriores teñen unha abordaxe lixeiramente diferente e funcionan en estreita colaboración con Visual Studio. Se usamos o DDK, o noso proxecto só precisa un Makefile e os códigos fonte.
makefile
!INCLUDE $(NTMAKEENV)makefile.deffontes:
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 = 1O teu Makefile será exactamente o mesmo, pero vexamos os códigos fonte con máis detalle. Este ficheiro especifica o código fonte do teu programa (ficheiros .c), as opcións de compilación e outros parámetros.
- TARGETNAME: o nome do ficheiro executábel que se debería producir como resultado.
- TIPO DE OBXECTIVO: o tipo de ficheiro executábel. Podería ser un controlador (.sys), nese caso o valor do campo debería ser CONTROLADOR; se é unha biblioteca (.lib), o valor debería ser BIBLIOTECA. No noso caso, necesitamos un ficheiro executábel (.exe), polo que definimos o valor como PROGRAMA.
- UMTYPE: valores posibles para este campo: console para unha aplicación de consola, windows para executarse en modo de xanela. Non obstante, precisamos especificar nt para obter unha aplicación nativa.
- BUFFER_OVERFLOW_CHECKS: comprobación do desbordamento do búfer na pila, por desgraza non é o noso caso, deshabilitámolo.
- MINWIN_SDK_LIB_PATH: este valor fai referencia á variable SDK_LIB_PATH. Non te preocupes se non tes unha variable de sistema similar declarada. Cando executamos unha compilación comprobada desde DDK, esta variable declararase e apuntará ás bibliotecas necesarias.
- FONTES – unha lista das fontes do teu programa.
- INCLUDES – ficheiros de cabeceira necesarios para a compilación. Isto normalmente especifica a ruta aos ficheiros incluídos no DDK, pero podes especificar calquera outro adicional.
- TARGETLIBS – lista de bibliotecas para enlazar.
- USE_NTDLL é un campo obrigatorio que debe definirse como 1. Por razóns obvias.
- USER_C_FLAGS: calquera indicador que se poida usar nas directivas do preprocesador ao preparar o código da aplicación.
Entón, para compilar, precisamos executar a versión marcada con x86 (ou x64), cambiar o directorio de traballo ao cartafol do proxecto e executar o comando Build. A captura de pantalla mostra que compilamos un único ficheiro executábel.

Este ficheiro non se executará tan facilmente; o sistema quéixase e pídenos que pensemos no seu comportamento co seguinte erro:

Como lanzar unha aplicación nativa?
Cando se inicia autochk, a secuencia de inicio do programa está determinada polo valor da clave do rexistro:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteO xestor de sesións executa os programas desta lista un por un. O xestor de sesións busca ficheiros executábeis no directorio system32. O formato do valor da clave do rexistro é o seguinte:
autocheck autochk *MyNativeO valor debe estar en formato hexadecimal, non no ASCII habitual, polo que a clave mostrada enriba terá o seguinte 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,00Para converter o nome, podes usar un servizo en liña, por exemplo, .

Resulta que para lanzar unha aplicación nativa, necesitamos:
- Copia o ficheiro executábel ao cartafol system32
- Engadir unha clave ao rexistro
- Reiniciar a máquina
Para a súa comodidade, aquí ten un script xa feito para instalar unha aplicación nativa:
instalar.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pauseengadir.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,00Despois da instalación e o reinicio, antes de que apareza a pantalla de selección de usuario, veremos a seguinte imaxe:

Total
Usando esta pequena aplicación como exemplo, estabamos convencidos de que lanzar unha aplicación ao nivel Windows O nativo é totalmente posible. A continuación, os rapaces da Universidade de Innopolis e eu continuaremos a construír un servizo que iniciará a interacción co controlador moito antes que na versión anterior do noso proxecto. E coa chegada do shell de Win32, será lóxico transferir o control a un servizo completo que xa foi desenvolvido (máis sobre iso máis tarde). ).
No noso seguinte artigo, trataremos outro compoñente do servizo Active Restore: o controlador UEFI. Subscríbete ao noso blog para manterte ao día da seguinte publicación.
Fonte: www.habr.com
