Aplicativos nativos do Windows e serviço Acronis Active Restore

Hoje continuamos a história de como nós, junto com o pessoal da Innopolis University, estamos desenvolvendo a tecnologia Active Restore para permitir que o usuário comece a trabalhar em sua máquina o mais rápido possível após uma falha. Falaremos sobre aplicativos nativos do Windows, incluindo os recursos de sua criação e lançamento. Abaixo do recorte está um pouco sobre nosso projeto, além de um guia prático de como escrever aplicações nativas.

Aplicativos nativos do Windows e serviço Acronis Active Restore

Em posts anteriores já falamos sobre o que é Restauração Ativae como os estudantes de Innopolis se desenvolvem serviço. Hoje quero focar nas aplicações nativas, ao nível das quais queremos “enterrar” o nosso serviço de recuperação ativo. Se tudo der certo, seremos capazes de:

  • Inicie o próprio serviço muito mais cedo
  • Entre em contato com a nuvem onde o backup está localizado muito antes
  • Muito antes para entender em que modo o sistema está - inicialização normal ou recuperação
  • Muito menos arquivos para recuperar antecipadamente
  • Permita que o usuário comece ainda mais rápido.

Afinal, o que é um aplicativo nativo?

Para responder a essa pergunta, vejamos a sequência de chamadas que o sistema faz, por exemplo, se um programador em sua aplicação tenta criar um arquivo.

Aplicativos nativos do Windows e serviço Acronis Active Restore
Pavel Yosifovich - Programação do Kernel do Windows (2019)

O programador usa a função CreateFile, que é declarado no arquivo de cabeçalho fileapi.h e implementado em Kernel32.dll. No entanto, esta função em si não cria o arquivo, apenas verifica os argumentos de entrada e chama a função NtCreateFile (o prefixo Nt indica apenas que a função é nativa). Esta função é declarada no arquivo de cabeçalho Winternl.h e implementada em ntdll.dll. Ele se prepara para saltar para o espaço nuclear e depois faz uma chamada de sistema para criar um arquivo. Nesse caso, Kernel32 é apenas um wrapper para Ntdll. Uma das razões pelas quais isso foi feito é que a Microsoft tem assim a capacidade de alterar as funções do mundo nativo, mas não mexer nas interfaces padrão. A Microsoft não recomenda chamar funções nativas diretamente e não documenta a maioria delas. A propósito, funções não documentadas podem ser encontradas aqui.

A principal vantagem dos aplicativos nativos é que o ntdll é carregado no sistema muito antes do kernel32. Isso é lógico, porque o kernel32 requer ntdll para funcionar. Como resultado, os aplicativos que utilizam funções nativas podem começar a funcionar muito mais cedo.

Assim, os aplicativos nativos do Windows são programas que podem ser iniciados no início da inicialização do Windows. Eles usam SOMENTE funções do ntdll. Um exemplo de tal aplicação: autochk quem atua utilitário chkdisk para verificar se há erros no disco antes de iniciar os serviços principais. Este é exatamente o nível que queremos que nosso Active Restore esteja.

O que precisamos?

  • DDK (Kit de desenvolvimento de driver), agora também conhecido como WDK 7 (Kit de driver do Windows).
  • Máquina virtual (por exemplo, Windows 7 x64)
  • Não é necessário, mas os arquivos de cabeçalho que podem ser baixados podem ajudar aqui

O que há no código?

Vamos praticar um pouco e, por exemplo, escrever um pequeno aplicativo que:

  1. Exibe uma mensagem na tela
  2. Aloca alguma memória
  3. Aguarda a entrada do teclado
  4. Libera memória usada

Em aplicações nativas, o ponto de entrada não é main ou winmain, mas sim a função NtProcessStartup, já que na verdade lançamos diretamente novos processos no sistema.

Vamos começar exibindo uma mensagem na tela. Para isso temos uma função nativa NtDisplayString, que toma como argumento um ponteiro para um objeto de estrutura UNICODE_STRING. RtlInitUnicodeString nos ajudará a inicializá-lo. Como resultado, para exibir texto na tela podemos escrever esta pequena função:

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

Como apenas funções do ntdll estão disponíveis para nós e simplesmente não há outras bibliotecas na memória ainda, definitivamente teremos problemas em como alocar memória. O novo operador ainda não existe (porque vem do mundo de alto nível do C++) e não há função malloc (requer bibliotecas C de tempo de execução). Claro, você só pode usar uma pilha. Mas se precisarmos alocar memória dinamicamente, teremos que fazer isso no heap (ou seja, heap). Então, vamos criar uma pilha para nós mesmos e tirar memória dela sempre que precisarmos.

A função é adequada para esta tarefa RtlCreateHeap. A seguir, usando RtlAllocateHeap e RtlFreeHeap, ocuparemos e liberaremos memória quando precisarmos.

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);

Vamos esperar pela 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;
	}
}

Tudo o que precisamos é usar NtReadFile em um dispositivo aberto e espere até que o teclado retorne qualquer pressão para nós. Se a tecla ESC for pressionada, continuaremos trabalhando. Para abrir o dispositivo, precisaremos chamar a função NtCreateFile (precisaremos abrir DeviceKeyboardClass0). Também ligaremos NtCreateEventpara inicializar o objeto de espera. Declararemos nós mesmos a estrutura KEYBOARD_INPUT_DATA, que representa os dados do teclado. Isso facilitará nosso trabalho.

A aplicação nativa termina com uma chamada de função NtTerminateProcessporque estamos simplesmente matando nosso próprio processo.

Todo o código do nosso pequeno aplicativo:

#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 facilmente usar a função DbgBreakPoint() em nosso código para interrompê-lo no depurador. É verdade que você precisará conectar o WinDbg a uma máquina virtual para depuração do kernel. Instruções sobre como fazer isso podem ser encontradas aqui ou apenas use VirtualKDGenericName.

Compilação e montagem

A maneira mais fácil de construir um aplicativo nativo é usar DDK (Kit de Desenvolvimento de Driver). Precisamos da sétima versão antiga, já que as versões posteriores têm uma abordagem um pouco diferente e trabalham em estreita colaboração com o Visual Studio. Se usarmos o DDK, nosso projeto precisará apenas de 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

Seu Makefile será exatamente o mesmo, mas vamos examinar as fontes com um pouco mais de detalhes. Este arquivo especifica as fontes do seu programa (arquivos .c), opções de construção e outros parâmetros.

  • TARGETNAME – o nome do arquivo executável que deverá ser produzido no final.
  • TARGETTYPE – tipo de arquivo executável, pode ser um driver (.sys), então o valor do campo deve ser DRIVER, se for uma biblioteca (.lib), então o valor é LIBRARY. No nosso caso, precisamos de um arquivo executável (.exe), por isso definimos o valor como PROGRAM.
  • UMTYPE – valores possíveis para este campo: console para um aplicativo de console, janelas para trabalhar em modo de janela. Mas precisamos especificar nt para obter uma aplicação nativa.
  • BUFFER_OVERFLOW_CHECKS – verificando se há buffer overflow na pilha, infelizmente não é o nosso caso, nós o desligamos.
  • MINWIN_SDK_LIB_PATH – este valor refere-se à variável SDK_LIB_PATH, não se preocupe se você não tiver tal variável de sistema declarada, quando executarmos o build verificado a partir do DDK, esta variável será declarada e apontará para as bibliotecas necessárias.
  • FONTES – uma lista de fontes para o seu programa.
  • INCLUDES – arquivos de cabeçalho necessários para montagem. Aqui eles geralmente indicam o caminho para os arquivos que acompanham o DDK, mas você também pode especificar quaisquer outros.
  • TARGETLIBS – lista de bibliotecas que precisam ser vinculadas.
  • USE_NTDLL é um campo obrigatório que deve ser definido como 1 por motivos óbvios.
  • USER_C_FLAGS – quaisquer sinalizadores que você possa usar nas diretivas do pré-processador ao preparar o código do aplicativo.

Portanto, para construir, precisamos executar o Build verificado x86 (ou x64), alterar o diretório de trabalho para a pasta do projeto e executar o comando Build. O resultado na captura de tela mostra que temos um arquivo executável.

Aplicativos nativos do Windows e serviço Acronis Active Restore

Este arquivo não pode ser iniciado tão facilmente, o sistema amaldiçoa e nos faz pensar em seu comportamento com o seguinte erro:

Aplicativos nativos do Windows e serviço Acronis Active Restore

Como iniciar um aplicativo nativo?

Quando o autochk é iniciado, a sequência de inicialização dos programas é determinada pelo valor da chave de registro:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

O gerenciador de sessão executa programas desta lista um por um. O gerenciador de sessão procura os próprios arquivos executáveis ​​no diretório system32. O formato do valor da chave de registro é o seguinte:

autocheck autochk *MyNative

O valor deve estar no formato hexadecimal e não no ASCII usual, portanto a chave mostrada acima estará no 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, você pode usar um serviço online, por exemplo, este.

Aplicativos nativos do Windows e serviço Acronis Active Restore
Acontece que para lançar um aplicativo nativo, precisamos:

  1. Copie o arquivo executável para a pasta system32
  2. Adicione uma chave ao registro
  3. Reinicie a máquina

Por conveniência, aqui está um script pronto para instalar um aplicativo nativo:

Install.bat

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

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

Após a instalação e reinicialização, antes mesmo de aparecer a tela de seleção do usuário, obteremos a seguinte imagem:

Aplicativos nativos do Windows e serviço Acronis Active Restore

Total

Usando o exemplo de um aplicativo tão pequeno, ficamos convencidos de que é perfeitamente possível executar o aplicativo no nível nativo do Windows. A seguir, eu e o pessoal da Universidade de Innopolis continuaremos a construir um serviço que iniciará o processo de interação com o motorista muito mais cedo do que na versão anterior do nosso projeto. E com o advento do shell win32, seria lógico transferir o controle para um serviço completo já desenvolvido (mais sobre isso aqui).

No próximo artigo abordaremos outro componente do serviço Active Restore, ou seja, o driver UEFI. Assine nosso blog para não perder o próximo post.

Fonte: habr.com

Adicionar um comentário