Hoje continuamos a contar como estamos desenvolvendo a tecnologia Active Restore em conjunto com a equipe da Universidade Innopolis, para permitir que o usuário retome o trabalho em sua máquina o mais cedo possível após uma falha. Vamos falar sobre aplicativos nativos do Windows, incluindo as particularidades de sua criação e execução. Abaixo, um pouco sobre nosso projeto, além de um guia prático sobre como escrever aplicativos nativos.

Nos posts anteriores, já falamos sobre o que é , e como os estudantes de Innopolis estão desenvolvendo . Hoje, quero focar nos aplicativos nativos, até onde queremos "enterrar" nosso serviço de recuperação ativa. Se tudo der certo, poderemos:
- Iniciar o próprio serviço muito mais cedo
- Conectar-se ao cloud onde o backup está armazenado muito mais cedo
- Entender muito mais cedo em que modo o sistema está – de inicialização normal ou de recuperação
- Restaurar muito menos arquivos antecipadamente
- Permitir que o usuário comece a trabalhar ainda mais rápido.
O que é, afinal, um aplicativo nativo?
Para responder a essa pergunta, vamos dar uma olhada na sequência de chamadas que o sistema realiza, por exemplo, se um programador em seu aplicativo tenta criar um arquivo.

Pavel Yosifovich – Programação do Kernel Windows (2019)
O programador utiliza a função , que é declarada no arquivo de cabeçalho fileapi.h e implementada no Kernel32.dll. No entanto, essa função não se ocupa da criação do arquivo; ela apenas verifica os argumentos de entrada e chama a função (o prefixo Nt indica que a função é nativa). Essa função é declarada no arquivo de cabeçalho winternl.h e implementada no ntdll.dll. Ela se prepara para um salto ao espaço do kernel e, em seguida, realiza uma chamada de sistema para criar o arquivo. Nesse caso, podemos dizer que Kernel32 é apenas uma camada sobre Ntdll. Uma das razões para isso é que a Microsoft, dessa forma, tem a capacidade de modificar funções do mundo nativo sem alterar as interfaces padrão. A Microsoft não recomenda chamar funções nativas diretamente e não documenta a maior parte delas. Aliás, funções não documentadas podem ser encontradas. .
A principal vantagem dos aplicativos nativos é que o ntdll é carregado no sistema significativamente antes do kernel32. Isso faz sentido, já que o kernel32 requer a presença do ntdll para funcionar. Como resultado, aplicativos que utilizam funções nativas podem iniciar sua operação muito mais cedo.
Assim, os Aplicativos Nativos do Windows são programas capazes de serem executados em uma fase inicial de carregamento do Windows. Eles usam APENAS funções do ntdll. Um exemplo desse tipo de aplicativo: que executa para verificar se há erros no disco ainda antes do lançamento dos serviços principais. É exatamente nesse nível que queremos ver nosso Active Restore.
O que precisamos?
- (Driver Development Kit), atualmente também conhecido como WDK 7 (Windows Driver Kit).
- Uma máquina virtual (por exemplo, Windows 7 x64)
- Não é obrigatório, mas cabeçalhos podem ajudar e estão disponíveis para download
E quanto ao código?
Vamos praticar um pouco e, como exemplo, escrever um pequeno aplicativo que:
- Exibe uma mensagem na tela
- Aloca um pouco de memória
- Aguarda a entrada do teclado
- Libera a memória alocada
Em aplicativos nativos, o ponto de entrada não é main ou winmain, mas sim a função NtProcessStartup, já que estamos efetivamente iniciando novos processos no sistema diretamente.
Vamos começar exibindo uma mensagem na tela. Para isso, temos a função nativa , que aceita um ponteiro para um objeto da estrutura UNICODE_STRING como argumento. Podemos inicializá-lo com RtlInitUnicodeString. Como resultado, para exibir texto na tela, podemos escrever uma pequena função como esta:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}Como temos acesso apenas às funções do ntdll, e outras bibliotecas ainda não estão carregadas na memória, teremos problemas para alocar memória. O operador new ainda não existe (pois ele é do mundo C++ de alto nível), e também não há a função malloc (que requer bibliotecas de tempo de execução C). Podemos, claro, usar apenas a pilha. Mas se precisarmos alocar memória dinamicamente, teremos que fazer isso na pilha (ou seja, heap). Portanto, vamos criar um heap para nós e alocar memória dele quando necessário.
Para essa tarefa, a função será adequada. Em seguida, usando RtlAllocateHeap e RtlFreeHeap, nós iremos alocar e liberar memória quando necessário.
PVOID memória = NULL;
PVOID buffer = NULL;
ULONG tamanhoBuffer = 42;
// criar heap para alocar memória depois
memória = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// alocar buffer do tamanho tamanhoBuffer
buffer = RtlAllocateHeap(
memória,
HEAP_ZERO_MEMORY,
tamanhoBuffer
);
// liberar buffer (na verdade não é necessário porque destruímos o heap no próximo passo)
RtlFreeHeap(memória, 0, buffer);
RtlDestroyHeap(memória);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 que precisamos fazer é usar no dispositivo aberto e esperar até que o teclado nos retorne alguma tecla pressionada. No caso de a tecla ESC ser pressionada, nós continuaremos a execução. Para abrir o dispositivo, precisaremos chamar a função NtCreateFile (precisamos abrir o DeviceKeyboardClass0). Também chamaremos , para inicializar um objeto para espera. Nós declararemos a estrutura KEYBOARD_INPUT_DATA, que representa os dados do teclado. Isso facilitará nosso trabalho.
A execução do aplicativo nativo termina chamando a função , porque simplesmente estamos encerrando 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() no código para parar no depurador. É preciso conectar o WinDbg à máquina virtual para depuração do kernel. A instrução de como fazer isso pode ser encontrada ou simplesmente usar .
Compilação e montagem
A maneira mais simples de compilar um aplicativo nativo é usar (Driver Development Kit). Precisamos exatamente da versão antiga sete, pois versões mais recentes têm uma abordagem um pouco diferente e trabalham estreitamente com o Visual Studio. Se usarmos o DDK, nosso projeto precisa apenas de um Makefile e sources.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defsources:
NOME_DO_ALVO = MeuNativo
TIPO_DO_ALVO = PROGRAMA
UMTIPO = nt
VERIFICAÇÕES_DE_ESTOURO_DE_BUFFER = 0
Caminho_BIBLIOTECA_MINWIN_SDK = $(SDK_LIB_PATH)
FONTES = source.c
INCLUI = $(DDK_INC_PATH);
C:WinDDK7600.16385.1ndk;
BIBLIOTECAS_DO_ALVO = $(DDK_LIB_PATH)ntdll.lib
$(DDK_LIB_PATH)nt.lib
USE_NTDLL = 1Seu Makefile será exatamente igual, mas vamos parar um pouco mais com as sources. Neste arquivo, você especifica os fontes do seu programa (arquivos .c), opções de compilação e outros parâmetros.
- NOME_DO_ALVO – o nome do arquivo executável que deve ser gerado no final.
- TIPO_DO_ALVO – o tipo do arquivo executável, pode ser um driver (.sys), então o valor deve ser DRIVER, se for uma biblioteca (.lib), o valor deve ser LIBRARY. No nosso caso, precisamos de um arquivo executável (.exe), então definimos o valor como PROGRAM.
- UMTYPE – os possíveis valores deste campo: console para aplicativo de console, windows para operação em modo de janela. Mas precisamos especificar nt para obter um aplicativo nativo.
- BUFFER_OVERFLOW_CHECKS – verificação de pilha para estouro de buffer, infelizmente não é o nosso caso, desativamos.
- MINWIN_SDK_LIB_PATH – esse valor se refere à variável SDK_LIB_PATH, não se preocupe se você não declarou tal variável de sistema, no momento em que iniciarmos a compilação verificada do DDK, essa variável será declarada e apontará para as bibliotecas necessárias.
- SOURCES – lista de fontes do seu programa.
- INCLUDES – arquivos de cabeçalho necessários para a compilação. Aqui normalmente se indica o caminho para os arquivos que acompanham o DDK, mas você pode incluir qualquer outro que desejar.
- TARGETLIBS – lista de bibliotecas que precisam ser vinculadas.
- USE_NTDLL – campo obrigatório que deve ser definido como 1. Por razões bem óbvias.
- USER_C_FLAGS – quaisquer flags que você possa usar nas diretivas do pré-processador ao preparar o código do aplicativo.
Portanto, para a compilação, precisamos iniciar a compilação verificada x86 (ou x64), mudar o diretório de trabalho para a pasta do projeto e executar o comando Build. O resultado na captura de tela mostra que conseguimos compilar um arquivo executável.

Este arquivo não pode ser simplesmente executado, o sistema reclama e nos envia a pensar sobre seu comportamento com o seguinte erro:

Como executar um aplicativo nativo?
No momento da inicialização do autochk, a sequência de inicialização dos programas é determinada pelo valor da chave do registro:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteO gerenciador de sessão executa os programas desta lista em ordem. Os próprios arquivos executáveis são procurados pelo gerenciador de sessão no diretório system32. O formato do valor da chave do registro é o seguinte:
autocheck autochk *MyNativeO valor deve estar em formato hexadecimal, e não no ASCII habitual, 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,00Para converter o nome, você pode usar um serviço online, por exemplo, .

Portanto, para executar um aplicativo nativo, precisamos:
- Copiar o arquivo executável para a pasta system32
- Adicionar a chave ao registro
- Reiniciar a máquina
Para facilitar, aqui está um script pronto para instalar o aplicativo nativo:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Exemplo Nativo Instalado
pauseadd.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,00Após a instalação e reinicialização, antes mesmo de aparecer a tela de seleção de usuários, teremos a seguinte cena:

Resultado
No exemplo de um pequeno aplicativo, confirmamos que é totalmente possível executar um aplicativo no nível Windows Native. Posteriormente, junto com o pessoal da Universidade de Innopolis, continuaremos a desenvolver um serviço que iniciará o processo de interação com o driver muito mais cedo do que na versão anterior do nosso projeto. Com a introdução da interface win32, será lógico transferir o controle para um serviço completo que já foi desenvolvido (mais sobre isso) ).
Em nosso próximo artigo, abordaremos mais um componente do serviço Active Restore, que é o driver UEFI. Siga nosso blog para não perder o próximo post.
Fonte: habr.com
