Aplicações nativas do Windows e Acronis Active Restore

Hoje continuamos a história de como nós, juntamente com a equipe da Universidade de Innopolis, 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. Sob o corte - um pouco sobre o nosso projeto, bem como um guia prático sobre como escrever aplicativos nativos.



Nas postagens anteriores, já falamos sobre o que é o Active Restore e como os alunos do Innopolis desenvolvem o serviço . Hoje, quero me concentrar em aplicativos nativos, no nível em que queremos “enterrar” nosso serviço de recuperação ativo. Se tudo der certo, podemos:

  • Muito antes para iniciar o próprio serviço
  • Muito antes de entrar em contato com a nuvem na qual o backup está
  • É muito mais cedo para entender em que modo o sistema está - inicialização ou recuperação normal
  • Para restaurar muito menos arquivos com antecedência
  • Permita que o usuário comece ainda mais rápido.

O que é um aplicativo nativo em geral?


Para responder a essa pergunta, vejamos a sequência de chamadas que o sistema faz, por exemplo, se um programador em seu aplicativo tentar criar um arquivo.


Pavel Yosifovich - Programação do Kernel do Windows (2019)

O programador usa a função CreateFile , declarada no arquivo de cabeçalho fileapi.h e implementada no Kernel32.dll. No entanto, essa função em si não cria um arquivo, apenas verifica os argumentos na 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 no ntdll.dll. Ela se prepara para pular para o espaço nuclear, após o que faz uma chamada de sistema para criar um arquivo. Nesse caso, o Kernel32 é apenas um invólucro para o Ntdll. Uma das razões pelas quais isso é feito, a Microsoft tem, portanto, a capacidade de alterar as funções do mundo nativo, mas não toca nas interfaces padrão. A Microsoft não recomenda a chamada direta de funções nativas e não documenta a maioria delas. A propósito, recursos não documentados podem ser encontrados aqui .

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

Portanto, os Aplicativos Nativos do Windows são programas que podem ser executados em um estágio inicial na inicialização do Windows. Eles usam apenas funções do ntdll. Um exemplo desse aplicativo: autochk, que executa o utilitário chkdisk para verificar se há erros no disco antes de iniciar os serviços principais. É nesse nível que queremos ver nossa Restauração Ativa.

Do que precisamos?


  • DDK (Driver Development Kit), agora também conhecido como WDK 7 (Windows Driver Kit).
  • Máquina virtual (por exemplo, Windows 7 x64)
  • Não necessariamente, mas os arquivos de cabeçalho podem ser baixados aqui.

O que está no código?


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

  1. Exibe uma mensagem na tela.
  2. Aloca um pouco de memória
  3. Aguardando entrada do teclado
  4. Libera memória ocupada

Em aplicativos nativos, o ponto de entrada não é o principal ou o domínio principal, mas a função NtProcessStartup, pois na verdade iniciamos diretamente o novo processo no sistema.

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

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

Como apenas as funções do ntdll estão disponíveis e simplesmente não existem outras bibliotecas na memória, definitivamente teremos problemas com a alocação de memória. O novo operador ainda não existe (porque é proveniente de um mundo C ++ de nível muito alto), também não há função malloc (ele precisa de bibliotecas C em tempo de execução). Obviamente, você pode usar apenas a pilha. Porém, se precisarmos alocar memória dinamicamente, teremos que fazer isso no heap (ou seja, heap). Portanto, vamos criar um monte para nós mesmos e tiraremos memória quando precisarmos.

A função RtlCreateHeap é adequada para esta tarefa. Além disso, 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 continuar aguardando 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"\\Device\\KeyboardClass0"); 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 fazer é usar o NtReadFile em um dispositivo aberto e aguardar até o teclado retornar um clique para nós. Caso a tecla ESC seja pressionada, continuaremos a trabalhar. Para abrir o dispositivo, precisamos chamar a função NtCreateFile (você precisará abrir \ Device \ KeyboardClass0). Também chamaremos NtCreateEvent para inicializar o objeto a aguardar. Declararemos independentemente a estrutura KEYBOARD_INPUT_DATA que representa os dados do teclado. Isso facilitará nosso trabalho.

O aplicativo nativo termina com uma chamada para a função NtTerminateProcess , porque acabamos de matar nosso próprio processo.

Todo o código do nosso pequeno aplicativo:

 #include "ntifs.h" // \WinDDK\7600.16385.1\inc\ddk #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"\\Device\\KeyboardClass0"); 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 ready\n"); // create heap in order to allocate memory later memory = RtlCreateHeap( HEAP_GROWABLE, NULL, 1000, 0, NULL, NULL ); WriteLn(L"Heap ready\n"); // allocate buffer of size bufferSize buffer = RtlAllocateHeap( memory, HEAP_ZERO_MEMORY, bufferSize ); WriteLn(L"Buffer allocated\n"); // free buffer (actually not needed because we destroy heap in next step) RtlFreeHeap(memory, 0, buffer); RtlDestroyHeap(memory); WriteLn(L"Heap destroyed\n"); 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 função DbgBreakPoint () no código para parar no depurador. É verdade que você precisará conectar o WinDbg à máquina virtual para depuração do kernel. Instruções sobre como fazer isso podem ser encontradas aqui ou apenas use o VirtualKD .

Compilação e montagem


A maneira mais fácil de criar um aplicativo nativo é usar o DDK (Driver Development Kit). Precisamos exatamente da sétima versão antiga, pois as versões posteriores têm uma abordagem ligeiramente 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:\WinDDK\7600.16385.1\ndk; TARGETLIBS = $(DDK_LIB_PATH)\ntdll.lib \ $(DDK_LIB_PATH)\nt.lib USE_NTDLL = 1 

Seu Makefile será exatamente o mesmo, mas vamos nos concentrar nas fontes com mais detalhes. Este arquivo contém as fontes do seu programa (arquivos .c), opções de compilação e outros parâmetros.

  • TARGETNAME - o nome do arquivo executável, que deve ser o resultado.
  • TARGETTYPE - tipo de arquivo executável, pode ser um driver (.sys); o valor do campo deve ser DRIVER, se a biblioteca (.lib), o valor é LIBRARY. No nosso caso, precisamos de um arquivo executável (.exe), portanto, definimos o valor como PROGRAM.
  • UMTYPE - valores possíveis para este campo: console para um aplicativo de console, janelas para operar no modo de janela. Mas precisamos especificar nt para obter o aplicativo nativo.
  • BUFFER_OVERFLOW_CHECKS - verificando se há excesso de buffer na pilha, infelizmente não é o nosso caso, desative-o.
  • MINWIN_SDK_LIB_PATH - esse valor refere-se à variável SDK_LIB_PATH, não se preocupe por não ter declarado essa variável de sistema; no momento em que executarmos a compilação verificada no DDK, essa variável será declarada e apontará para as bibliotecas necessárias.
  • FONTES - uma lista das fontes do seu programa.
  • INCLUI - arquivos de cabeçalho necessários para montagem. Eles geralmente indicam o caminho para os arquivos que acompanham o DDK, mas você pode opcionalmente especificar outros.
  • TARGETLIBS - uma lista de bibliotecas que precisam ser vinculadas.
  • USE_NTDLL é um campo obrigatório que deve ser definido na posição 1. Por razões óbvias.
  • USER_C_FLAGS - qualquer sinalizador que você possa usar nas diretivas de pré-processador ao preparar o código do aplicativo.

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

Construir

Este arquivo não pode ser executado de maneira simples, o sistema jura e nos envia para pensar sobre seu comportamento com o seguinte erro:

Erro


Como executar um aplicativo nativo?


No início do autochk, a sequência de inicialização dos programas é determinada pelo valor da chave do registro:

 HKLM\System\CurrentControlSet\Control\Session Manager\BootExecute 

O gerenciador de sessões executa os programas dessa lista, um por um. O próprio gerenciador de sessões procura por arquivos executáveis ​​no diretório system32. O formato do valor da chave do Registro é o seguinte:

 autocheck autochk *MyNative 

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


Acontece que, para executar o aplicativo nativo, precisamos:

  1. Copiar arquivo executável para a pasta system32
  2. Adicione uma chave ao registro
  3. Reiniciar 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 

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 

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

resultado

Sumário


Usando o exemplo de um aplicativo tão pequeno, garantimos que é possível executar o aplicativo no nível nativo do Windows. Além disso, os funcionários da Universidade de Innopolis continuarão construindo 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, será lógico transferir o controle para um serviço completo que já foi desenvolvido (mais sobre isso aqui ).

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

Source: https://habr.com/ru/post/pt481382/


All Articles