Aplicacións nativas de Windows e servizo Acronis Active Restore

Hoxe continuamos a historia de como nós, xunto cos rapaces da Universidade de Innopolis, estamos a desenvolver a tecnoloxía Active Restore para permitir que o usuario comece a traballar na súa máquina o antes posible despois dun fallo. Falaremos das aplicacións nativas de Windows, incluíndo as características da súa creación e lanzamento. Debaixo do corte hai un pouco sobre o noso proxecto, así como unha guía práctica sobre como escribir aplicacións nativas.

Aplicacións nativas de Windows e servizo Acronis Active Restore

En entradas anteriores xa falamos do que é Restauración activa, e como se desenvolven os estudantes de Innopolis servizo. Hoxe quero centrarme nas aplicacións nativas, a cuxo nivel queremos "enterrar" o noso servizo de recuperación activa. Se todo funciona, poderemos:

  • Inicie o propio servizo moito antes
  • Póñase en contacto coa nube onde se atopa a copia de seguridade moito antes
  • Moito antes para entender en que modo está o sistema: arranque ou recuperación normal
  • Moito menos ficheiros para recuperar con antelación
  • Permite que o usuario comece aínda máis rápido.

De todos os xeitos, que é unha aplicación nativa?

Para responder a esta pregunta, vexamos a secuencia de chamadas que fai o sistema, por exemplo, se un programador da súa aplicación tenta crear un ficheiro.

Aplicacións nativas de Windows e servizo Acronis Active Restore
Pavel Yosifovich - Programación do núcleo de Windows (2019)

O programador usa a función Crear ficheiro, 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ó verifica os argumentos de entrada e chama á función NtCreateFile (o prefixo Nt só indica que a función é nativa). Esta función declárase no ficheiro de cabeceira winternl.h e implícase en ntdll.dll. Prepárase para saltar ao espazo nuclear, despois de que fai unha chamada ao sistema para crear un ficheiro. Neste caso, resulta que Kernel32 é só un envoltorio para Ntdll. Unha das razóns polas que se fixo isto é que Microsoft ten así a capacidade de cambiar as funcións do mundo nativo, pero non tocar 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 aquí.

A principal vantaxe das aplicacións nativas é que ntdll se carga no sistema moito antes que o kernel32. Isto é lóxico, porque o kernel32 require que ntdll funcione. Como resultado, as aplicacións que usan funcións nativas poden comezar a funcionar moito antes.

Así, as aplicacións nativas de Windows son programas que poden iniciarse no inicio de Windows. SÓ usan funcións de ntdll. Un exemplo desta aplicación: autochk quen realiza utilidade chkdisk para comprobar se hai erros no disco antes de iniciar os servizos principais. Este é exactamente o nivel que queremos que sexa a nosa Restauración activa.

Que necesitamos?

  • DDK (Kit de desenvolvemento de controladores), agora tamén coñecido como WDK 7 (Kit de controladores de Windows).
  • Máquina virtual (por exemplo, Windows 7 x64)
  • Non é necesario, pero os ficheiros de cabeceira que se poden descargar poden axudar aquí

Que hai no código?

Practiquemos un pouco e, por exemplo, escribamos unha pequena aplicación que:

  1. Mostra unha mensaxe na pantalla
  2. Asigna algo de memoria
  3. Agarda a entrada do teclado
  4. Libera memoria usada

Nas aplicacións nativas, o punto de entrada non é main ou winmain, senón a función NtProcessStartup, xa que en realidade lanzamos novos procesos directamente no sistema.

Comecemos mostrando unha mensaxe na pantalla. Para iso temos unha función nativa NtDisplayString, que toma como argumento un punteiro a un obxecto de estrutura UNICODE_STRING. RtlInitUnicodeString axudaranos a inicializalo. Como resultado, para mostrar texto na pantalla podemos escribir esta pequena función:

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

Dado que só as funcións de ntdll están dispoñibles para nós, e simplemente non hai outras bibliotecas na memoria aínda, definitivamente teremos problemas con como asignar memoria. O novo operador aínda non existe (porque provén do mundo demasiado alto de C++), e non hai función malloc (require bibliotecas C en tempo de execución). Por suposto, só podes usar unha pila. Pero se necesitamos asignar memoria de forma dinámica, teremos que facelo no montón (é dicir, no montón). Entón, imos crear un montón para nós mesmos e sacarlle memoria sempre que o necesitemos.

A función é adecuada para esta tarefa RtlCreateHeap. A continuación, usando RtlAllocateHeap e RtlFreeHeap, ocuparemos e liberaremos memoria cando o 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 á espera da entrada de 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 o que necesitamos é usar NtReadFile nun dispositivo aberto e agarde ata que o teclado nos devolva calquera presión. Se se preme a tecla ESC, seguiremos traballando. Para abrir o dispositivo, teremos que chamar á función NtCreateFile (teremos que abrir DeviceKeyboardClass0). Tamén chamaremos NtCreateEventpara inicializar o obxecto wait. Nós mesmos declararemos a estrutura KEYBOARD_INPUT_DATA, que representa os datos do teclado. Isto facilitará o noso traballo.

A aplicación nativa remata cunha chamada de función NtTerminateProcessporque simplemente estamos matando o noso propio proceso.

Todo o código 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 detelo no depurador. É certo, terá que conectar WinDbg a unha máquina virtual para a depuración do núcleo. Pódense atopar instrucións sobre como facelo aquí ou simplemente use VirtualKD.

Compilación e montaxe

O xeito máis sinxelo de crear unha aplicación nativa é empregala DDK (Kit de desenvolvemento de controladores). Necesitamos a antiga sétima versión, xa que as versións posteriores teñen un enfoque lixeiramente diferente e traballan en estreita colaboración con Visual Studio. Se usamos o DDK, entón o noso proxecto só necesita Makefile e fontes.

makefile

!INCLUDE $(NTMAKEENV)makefile.def

fontes:

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

O teu Makefile será exactamente o mesmo, pero vexamos as fontes con máis detalle. Este ficheiro especifica as fontes do programa (ficheiros .c), as opcións de compilación e outros parámetros.

  • TARGETNAME: o nome do ficheiro executable que debería producirse ao final.
  • TARGETTYPE: tipo de ficheiro executable, pode ser un controlador (.sys), entón o valor do campo debe ser DRIVER, se é unha biblioteca (.lib), entón o valor é LIBRARY. No noso caso, necesitamos un ficheiro executable (.exe), polo que establecemos o valor en PROGRAMA.
  • UMTYPE: valores posibles para este campo: consola para unha aplicación de consola, fiestras para traballar en modo con fiestras. Pero necesitamos especificar nt para obter unha aplicación nativa.
  • BUFFER_OVERFLOW_CHECKS: comprobando a pila para detectar un desbordamento do búfer, desafortunadamente non é o noso caso, desactivámolo.
  • MINWIN_SDK_LIB_PATH: este valor refírese á variable SDK_LIB_PATH, non te preocupes que non tes unha variable do sistema declarada, cando executamos a compilación comprobada desde o DDK, esta variable será declarada e apuntará ás bibliotecas necesarias.
  • FONTES: unha lista de fontes para o seu programa.
  • INCLÚE: ficheiros de cabeceira que son necesarios para a montaxe. Aquí adoitan indicar o camiño aos ficheiros que veñen co DDK, pero tamén podes especificar outros.
  • TARGETLIBS: lista de bibliotecas que se deben ligar.
  • USE_NTDLL é un campo obrigatorio que debe establecerse en 1 por razóns obvias.
  • USER_C_FLAGS: calquera marca que poida usar nas directivas do preprocesador ao preparar o código da aplicación.

Polo tanto, para construír, necesitamos executar x86 (ou x64) Checked Build, cambiar o directorio de traballo ao cartafol do proxecto e executar o comando Construír. O resultado da captura de pantalla mostra que temos un ficheiro executable.

Aplicacións nativas de Windows e servizo Acronis Active Restore

Este ficheiro non se pode iniciar tan facilmente, o sistema maldice e mándanos a pensar no seu comportamento co seguinte erro:

Aplicacións nativas de Windows e servizo Acronis Active Restore

Como lanzar unha aplicación nativa?

Cando se inicia o autochk, a secuencia de inicio dos programas está determinada polo valor da clave de rexistro:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

O xestor de sesión executa os programas desta lista un por un. O xestor de sesións busca os propios ficheiros executables no directorio system32. O formato do valor da clave de rexistro é o seguinte:

autocheck autochk *MyNative

O valor debe estar en formato hexadecimal, non no ASCII habitual, polo que a clave mostrada arriba terá o 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 converter o título, podes usar un servizo en liña, por exemplo, isto.

Aplicacións nativas de Windows e servizo Acronis Active Restore
Resulta que para lanzar unha aplicación nativa, necesitamos:

  1. Copia o ficheiro executable no cartafol system32
  2. Engade unha clave ao rexistro
  3. Reinicie a máquina

Para comodidade, aquí tes un script preparado para instalar unha aplicación nativa:

instalar.bat

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

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

Despois da instalación e reinicio, mesmo antes de que apareza a pantalla de selección de usuario, obteremos a seguinte imaxe:

Aplicacións nativas de Windows e servizo Acronis Active Restore

Total

Usando o exemplo dunha aplicación tan pequena, estabamos convencidos de que é moi posible executar a aplicación no nivel nativo de Windows. A continuación, os mozos da Universidade de Innopolis e eu seguiremos construíndo un servizo que iniciará o proceso de interacción co condutor moito antes que na versión anterior do noso proxecto. E coa chegada do shell win32, sería lóxico transferir o control a un servizo completo que xa foi desenvolvido (máis sobre isto aquí).

No seguinte artigo imos tocar outro compoñente do servizo Active Restore, a saber, o controlador UEFI. Subscríbete ao noso blog para non perder a seguinte publicación.

Fonte: www.habr.com

Engadir un comentario