Aplicaciones nativas de Windows y servicio Acronis Active Restore

Hoy continuamos hablando sobre cómo, junto con los chicos de la Universidad 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 un fallo. Hablaremos de aplicaciones nativas de Windows, incluyendo las características de su creación y ejecución. A continuación, 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 publicaciones anteriores ya hemos comentado qué es Active Restore, y cómo los estudiantes de Innopolis están desarrollando servicio. Hoy quiero centrarme en las aplicaciones nativas, hasta el nivel en el que queremos “enterrar” nuestro servicio de recuperación activa. Si todo sale bien, podremos:

  • Iniciar el servicio mucho antes
  • Conectarnos al cloud donde está el respaldo mucho antes
  • Entender mucho antes en qué modo está el sistema: carga normal o recuperación
  • Restaurar muchos menos archivos por adelantado
  • Permitir al usuario comenzar a trabajar aún más rápido.

¿Qué es exactamente una aplicación nativa?

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

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

El programador utiliza la función CreateFile, que está declarada en el archivo de encabezado fileapi.h y se implementa en Kernel32.dll. Sin embargo, esta función no se encarga de crear el archivo, solo verifica los argumentos de entrada y llama a la función NtCreateFile (el prefijo Nt indica que la función es nativa). Esta función está declarada en el archivo de encabezado winternl.h y se implementa en ntdll.dll. Realiza los preparativos para saltar al espacio del kernel, después de lo cual realiza una llamada al sistema para crear el archivo. En este caso, Kernel32 es solo un envoltorio para Ntdll. Una de las razones por las que se hace esto es que Microsoft tiene la oportunidad de modificar funciones del mundo nativo sin alterar las interfaces estándar. Microsoft no recomienda llamar directamente a funciones nativas y no documenta la mayor parte 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 tiene sentido, ya que kernel32 requiere que ntdll esté presente para funcionar. Como resultado, las aplicaciones que utilizan funciones nativas pueden comenzar a ejecutarse mucho antes.

Por lo tanto, las Aplicaciones Nativas de Windows son programas que pueden iniciarse en las primeras etapas de carga de Windows. Utilizan ÚNICAMENTE funciones de ntdll. Un ejemplo de tal aplicación: autochk que ejecuta la utilidad chkdisk para verificar el disco en busca de errores incluso antes de que se inicien los servicios principales. Es precisamente a este nivel donde queremos ver nuestra Restauración Activa.

¿Qué necesitamos?

  • DDK (Driver Development Kit), actualmente también conocido como WDK 7 (Windows Driver Kit).
  • Una máquina virtual (por ejemplo, Windows 7 x64)
  • No es imprescindible, pero pueden ayudar los archivos de encabezado que se pueden descargar aquí

¿Qué hay en el código?

Hagamos un poco de práctica y, como ejemplo, escribamos una pequeña aplicación que:

  1. Muestre un mensaje en la pantalla
  2. Almacene un poco de memoria
  3. Espere la entrada del teclado
  4. Libere la memoria ocupada

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

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

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

Dado que solo tenemos acceso a funciones de ntdll y otras bibliotecas en memoria aún no están presentes, seguramente tendremos problemas sobre cómo asignar memoria. El operador new aún no existe (porque proviene de un mundo demasiado de alto nivel en C++), tampoco existe la función malloc (porque necesita bibliotecas de runtime de C). Podríamos usar solo la pila. Pero si necesitamos asignar memoria dinámicamente, tendremos que hacerlo en el montón (es decir, heap). Por lo tanto, vamos a crear un montón para nosotros y tomaremos memoria de él cuando la necesitemos.

Para esta tarea, la función que se ajusta es RtlCreateHeap. Luego, usando RtlAllocateHeap y RtlFreeHeap, ocuparemos y liberaremos memoria cuando lo necesitemos.

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

// crear un montón para poder asignar memoria más tarde
memory = RtlCreateHeap(
  HEAP_GROWABLE, 
  NULL, 
  1000, 
  0, NULL, NULL
);

// asignar un buffer del tamaño de bufferSize
buffer = RtlAllocateHeap(
  memory, 
  HEAP_ZERO_MEMORY, 
  bufferSize
);

// liberar el buffer (en realidad no es necesario porque destruimos el montón en el siguiente paso)
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 el dispositivo abierto, y esperar a que el teclado nos devuelva alguna pulsación. En caso de que se presione la tecla ESC, continuaremos trabajando. Para abrir el dispositivo, necesitaremos llamar a la función NtCreateFile (tendremos que abrir DeviceKeyboardClass0). También llamaremos a NtCreateEvent, para inicializar un objeto para esperar. Declararemos nosotros mismos la estructura KEYBOARD_INPUT_DATA, que representa los datos del teclado. Esto facilitará nuestro trabajo.

El trabajo de la aplicación nativa finaliza con la llamada a la función NtTerminateProcess, porque simplemente estamos matando nuestro propio proceso.

Todo el código de 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);
}

PD: Podemos usar fácilmente en el código la función DbgBreakPoint() para detenernos en el depurador. Sin embargo, será necesario conectar WinDbg a la máquina virtual para la depuración del kernel. La instrucción sobre cómo hacerlo se puede encontrar aquí o simplemente usar VirtualKD.

Compilación y ensamblaje

La forma más sencilla de compilar una aplicación nativa es usar DDK (Driver Development Kit). Necesitamos la antigua séptima versión, ya que las versiones posteriores tienen un enfoque algo diferente y trabajan estrechamente con Visual Studio. Si utilizamos DDK, nuestro proyecto solo necesita Makefile y sources.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

sources:

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

Tu Makefile será exactamente el mismo, sobre los sources vamos a detenernos un poco más. En este archivo se indican los fuentes de tu programa (archivos .c), opciones de compilación y otros parámetros.

  • TARGETNAME – el nombre del archivo ejecutable que debe resultar al final.
  • TARGETTYPE – el tipo de archivo ejecutable, esto puede ser un controlador (.sys), entonces el valor del campo debe ser DRIVER, si es una biblioteca (.lib), el valor LIBRARY. En nuestro caso necesitamos un archivo ejecutable (.exe), por lo que establecemos el valor PROGRAM.
  • UMTYPE – posibles valores de este campo: console para una aplicación de consola, windows para trabajar en modo de ventana. Pero necesitamos especificar nt para obtener una aplicación nativa.
  • BUFFER_OVERFLOW_CHECKS – verificación de la pila para desbordamiento de búfer, desafortunadamente, no es nuestro caso, desactivamos.
  • MINWIN_SDK_LIB_PATH – este valor se refiere a la variable SDK_LIB_PATH, no te preocupes si no tienes esta variable del sistema declarada, en el momento en que ejecutemos una compilación verificada desde el DDK, esta variable se declarará y apuntará a las bibliotecas necesarias.
  • SOURCES – lista de fuentes de tu programa.
  • INCLUDES – archivos de encabezado necesarios para la compilación. Aquí normalmente se indica la ruta a los archivos que vienen con el DDK, pero puedes incluir cualquier otro adicional.
  • TARGETLIBS – lista de bibliotecas que se deben vincular.
  • USE_NTDLL – campo obligatorio que debe establecerse en 1. Por razones bastante obvias.
  • USER_C_FLAGS – cualquier bandera que puedas usar en las directivas del preprocesador al preparar el código de la aplicación.

Entonces, para compilar, necesitamos ejecutar x86 (o x64) Checked Build, cambiar el directorio de trabajo a la carpeta del proyecto y ejecutar el comando Build. El resultado en la captura de pantalla muestra que hemos compilado un archivo ejecutable.

Aplicaciones nativas de Windows y servicio Acronis Active Restore

Este archivo no se puede ejecutar tan fácilmente, el sistema se queja y nos envía a pensar sobre su comportamiento con el siguiente error:

Aplicaciones nativas de Windows y servicio Acronis Active Restore

¿Cómo ejecutar una aplicación nativa?

Al iniciar autochk, la secuencia de lanzamiento de programas se determina por el valor de la clave del registro:

HKLMSistemaCurrentControlSetControlSession ManagerBootExecute

El administrador de sesiones ejecuta secuencialmente los programas de esta lista. Los propios archivos ejecutables son buscados por el administrador de sesiones en el directorio system32. El formato del valor de la clave del registro es el siguiente:

autocheck autochk *MyNative

El valor debe estar en formato hexadecimal, no en el habitual ASCII, por lo tanto, la clave presentada anteriormente 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 nombre, puedes utilizar un servicio en línea, por ejemplo, este.

Aplicaciones nativas de Windows y servicio Acronis Active Restore
Así que, para ejecutar una aplicación nativa, necesitamos:

  1. Copiar el archivo ejecutable en la carpeta system32
  2. Agregar la clave en el registro
  3. Reiniciar la máquina

Para conveniencia, aquí tienes un script listo para instalar la aplicación nativa:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Ejemplo nativo instalado
pause

add.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,00

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

Aplicaciones nativas de Windows y servicio Acronis Active Restore

Summary

En el ejemplo de esta pequeña aplicación, hemos comprobado que es posible ejecutar una aplicación a nivel de Windows Native. Más adelante, junto con los chicos de la Universidad Innopolis, continuaremos construyendo un servicio que iniciará el proceso de interacción con el controlador mucho antes que en la versión anterior de nuestro proyecto. Y con la llegada de la interfaz win32, será lógico transferir el control a un servicio completo que ya ha sido desarrollado (más detalles sobre esto). aquí).

En el próximo artículo tocaremos otro componente del servicio Active Restore, específicamente el controlador UEFI. Suscríbete a nuestro blog para no perderte la próxima publicación.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster