Aplicaciones nativas de Windows y servicio Acronis Active Restore

Hoy continuamos la historia de cómo nosotros, junto con los chicos de la Universidad de Innopolis, estamos desarrollando la tecnología Active Restore para permitir al usuario comenzar a trabajar en su máquina lo antes posible después de una falla. Hablaremos sobre aplicaciones nativas de Windows, incluidas las características de su creación y ejecución. Debajo del corte hay un poco sobre nuestro proyecto, así como una guía práctica sobre cómo escribir aplicaciones nativas.

Aplicaciones nativas de Windows y servicio Acronis Active Restore

En posts anteriores ya hemos hablado de qué es Restauración activay cómo se desarrollan los estudiantes de Innopolis servicio. Hoy quiero centrarme en las aplicaciones nativas, hasta el punto de que queremos “enterrar” nuestro servicio de recuperación activa. Si todo sale bien, podremos:

  • Inicie el servicio mucho antes
  • Contacta con la nube donde se encuentra la copia de seguridad mucho antes
  • Mucho antes para comprender en qué modo se encuentra el sistema: arranque normal o recuperación
  • Muchos menos archivos para recuperar por adelantado
  • Permita que el usuario comience aún más rápido.

¿Qué es una aplicación nativa de todos modos?

Para responder a esta pregunta, veamos la secuencia de llamadas que realiza el sistema, por ejemplo, si un programador en su aplicación intenta crear un archivo.

Aplicaciones nativas de Windows y servicio Acronis Active Restore
Pavel Yosifovich - Programación del kernel de Windows (2019)

El programador utiliza la función. CrearFile, que se declara en el archivo de encabezado fileapi.h y se implementa en Kernel32.dll. Sin embargo, esta función en sí no crea el archivo, solo verifica los argumentos de entrada y llama a la función. NtCrearArchivo (el prefijo Nt simplemente indica que la función es nativa). Esta función se declara en el archivo de encabezado winternl.h y se implementa en ntdll.dll. Se prepara para saltar al espacio nuclear, tras lo cual realiza una llamada al sistema para crear un archivo. En este caso, resulta que Kernel32 es sólo un contenedor para Ntdll. Una de las razones por las que se hizo esto es que Microsoft tiene la capacidad de cambiar las funciones del mundo nativo, pero no tocar las interfaces estándar. Microsoft no recomienda llamar directamente a funciones nativas y no documenta la mayoría de ellas. Por cierto, se pueden encontrar funciones no documentadas. aquí.

La principal ventaja de las aplicaciones nativas es que ntdll se carga en el sistema mucho antes que kernel32. Esto es lógico, porque kernel32 requiere ntdll para funcionar. Como resultado, las aplicaciones que utilizan funciones nativas pueden empezar a funcionar mucho antes.

Por lo tanto, las aplicaciones nativas de Windows son programas que pueden iniciarse temprano en el inicio de Windows. SÓLO utilizan funciones de ntdll. Un ejemplo de tal aplicación: autochk quien realiza utilidad chkdisk para comprobar si hay errores en el disco antes de iniciar los servicios principales. Este es exactamente el nivel que queremos que tenga nuestra Restauración Activa.

¿Qué necesitamos?

  • DDK (Kit de desarrollo de controladores), ahora también conocido como WDK 7 (Kit de controladores de Windows).
  • Máquina virtual (por ejemplo, Windows 7 x64)
  • No es necesario, pero los archivos de encabezado que se pueden descargar pueden ayudar aquí

¿Qué hay en el código?

Practiquemos un poco y, por ejemplo, escribamos una pequeña aplicación que:

  1. Muestra un mensaje en la pantalla.
  2. Asigna algo de memoria
  3. Espera la entrada del teclado
  4. Libera memoria usada

En las aplicaciones nativas el punto de entrada no es main o winmain, sino la función NtProcessStartup, ya que en realidad lanzamos directamente nuevos procesos en el sistema.

Comencemos mostrando un mensaje en la pantalla. Para esto tenemos una función nativa NtDisplayString, que toma como argumento un puntero a un objeto de estructura UNICODE_STRING. RtlInitUnicodeString nos ayudará a inicializarlo. Como resultado, para mostrar texto en pantalla podemos escribir esta pequeña función:

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

Dado que solo tenemos disponibles funciones de ntdll y simplemente no hay otras bibliotecas en la memoria, definitivamente tendremos problemas con la asignación de memoria. El nuevo operador aún no existe (porque proviene del mundo de alto nivel de C++) y no existe una función malloc (requiere bibliotecas C en tiempo de ejecución). Por supuesto, sólo puedes utilizar una pila. Pero si necesitamos asignar memoria dinámicamente, tendremos que hacerlo en el montón (es decir, en el montón). Así que creemos un montón para nosotros y tomemos recuerdos de él cuando lo necesitemos.

La función es adecuada para esta tarea. RtlCrearMontón. A continuación, utilizando RtlAllocateHeap y RtlFreeHeap, ocuparemos y liberaremos memoria cuando la necesitemos.

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 la entrada del 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;
	}
}

Todo lo que necesitamos es usar NtReadFile en un dispositivo abierto, y esperar hasta que el teclado nos devuelva cualquier pulsación. Si se pulsa la tecla ESC continuaremos trabajando. Para abrir el dispositivo, necesitaremos llamar a la función NtCreateFile (necesitaremos abrir DeviceKeyboardClass0). También llamaremos NtCreateEventopara inicializar el objeto de espera. Nosotros mismos declararemos la estructura KEYBOARD_INPUT_DATA, que representa los datos del teclado. Esto facilitará nuestro trabajo.

La aplicación nativa finaliza con una llamada a función. Proceso NtTerminateporque simplemente estamos matando nuestro propio proceso.

Todo el código para nuestra pequeña 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 fácilmente la función DbgBreakPoint() en nuestro código para detenerlo en el depurador. Es cierto que necesitará conectar WinDbg a una máquina virtual para depurar el kernel. Las instrucciones sobre cómo hacer esto se pueden encontrar aquí o simplemente usar VirtualKD.

Compilación y montaje.

La forma más sencilla de crear una aplicación nativa es utilizar DDK (Kit de desarrollo de controladores). Necesitamos la séptima versión antigua, ya que las versiones posteriores tienen un enfoque ligeramente diferente y trabajan en estrecha colaboración con Visual Studio. Si usamos el DDK, entonces nuestro proyecto solo necesita Makefile y fuentes.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

fuentes:

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

Su Makefile será exactamente el mismo, pero veamos las fuentes con un poco más de detalle. Este archivo especifica las fuentes de su programa (archivos .c), las opciones de compilación y otros parámetros.

  • TARGETNAME: el nombre del archivo ejecutable que se debe generar al final.
  • TARGETTYPE – tipo de archivo ejecutable, puede ser un controlador (.sys), entonces el valor del campo debe ser DRIVER, si es una biblioteca (.lib), entonces el valor es LIBRARY. En nuestro caso, necesitamos un archivo ejecutable (.exe), por lo que establecemos el valor en PROGRAMA.
  • UMTYPE: valores posibles para este campo: consola para una aplicación de consola, ventanas para trabajar en modo ventana. Pero necesitamos especificar nt para obtener una aplicación nativa.
  • BUFFER_OVERFLOW_CHECKS: verificamos la pila en busca de desbordamiento del búfer, desafortunadamente no es nuestro caso, lo desactivamos.
  • MINWIN_SDK_LIB_PATH: este valor se refiere a la variable SDK_LIB_PATH, no se preocupe si no tiene dicha variable de sistema declarada, cuando ejecutemos la compilación verificada desde el DDK, esta variable se declarará y apuntará a las bibliotecas necesarias.
  • FUENTES: una lista de fuentes para su programa.
  • INCLUYE: archivos de encabezado necesarios para el ensamblaje. Aquí suelen indicar la ruta a los archivos que vienen con el DDK, pero además puedes especificar cualquier otra.
  • TARGETLIBS: lista de bibliotecas que deben vincularse.
  • USE_NTDLL es un campo obligatorio que debe establecerse en 1 por razones obvias.
  • USER_C_FLAGS: cualquier indicador que pueda utilizar en las directivas del preprocesador al preparar el código de la aplicación.

Entonces, para compilar, necesitamos ejecutar Compilación marcada x86 (o x64), cambiar el directorio de trabajo a la carpeta del proyecto y ejecutar el comando Compilación. El resultado en la captura de pantalla muestra que tenemos un archivo ejecutable.

Aplicaciones nativas de Windows y servicio Acronis Active Restore

Este archivo no se puede ejecutar tan fácilmente, el sistema maldice y nos manda a pensar en su comportamiento con el siguiente error:

Aplicaciones nativas de Windows y servicio Acronis Active Restore

¿Cómo iniciar una aplicación nativa?

Cuando se inicia autochk, la secuencia de inicio de los programas está determinada por el valor de la clave de registro:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

El administrador de sesión ejecuta los programas de esta lista uno por uno. El administrador de sesión busca los archivos ejecutables en el directorio system32. El formato del valor de la clave de registro es el siguiente:

autocheck autochk *MyNative

El valor debe estar en formato hexadecimal, no en el habitual ASCII, por lo que la clave que se muestra arriba tendrá el 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

Para convertir el título, puede utilizar un servicio en línea, por ejemplo, este.

Aplicaciones nativas de Windows y servicio Acronis Active Restore
Resulta que para iniciar una aplicación nativa necesitamos:

  1. Copie el archivo ejecutable a la carpeta system32
  2. Agregar una clave al registro
  3. Reiniciar la máquina

Para mayor comodidad, aquí hay un script listo para instalar una aplicación nativa:

install.bat

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

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

Después de la instalación y reinicio, incluso antes de que aparezca la pantalla de selección de usuario, obtendremos la siguiente imagen:

Aplicaciones nativas de Windows y servicio Acronis Active Restore

Total

Usando el ejemplo de una aplicación tan pequeña, estábamos convencidos de que es muy posible ejecutar la aplicación en el nivel nativo de Windows. A continuación, los chicos de la Universidad de Innopolis y yo continuaremos creando un servicio que iniciará el proceso de interacción con el conductor mucho antes que en la versión anterior de nuestro proyecto. Y con la llegada del shell win32, sería lógico transferir el control a un servicio completo que ya ha sido desarrollado (más sobre esto aquí).

En el próximo artículo abordaremos otro componente del servicio Active Restore, a saber, el controlador UEFI. Suscríbete a nuestro blog para no perderte el próximo post.

Fuente: habr.com

Añadir un comentario