Windows Aplicacións nativas e servizo Acronis Active Restore

Hoxe, continuaremos a nosa historia sobre como estamos a traballar coa Universidade de Innopolis para desenvolver a tecnoloxía Active Restore que permita aos usuarios retomar o traballo nas súas máquinas o máis rápido posible despois dun fallo. Falaremos de aplicacións nativas. Windows, incluíndo os detalles da súa creación e lanzamento. A continuación, ofrécese un pouco de información sobre o noso proxecto, así como unha guía práctica sobre como escribir aplicacións nativas.

Windows Aplicacións nativas e servizo Acronis Active Restore

En publicacións anteriores xa falamos do que é Restauración activae como se están desenvolvendo os estudantes de Innopolis servizoHoxe gustaríame centrarme nas aplicacións nativas, o nivel no que queremos "enterrar" o noso servizo de recuperación activo. Se todo sae ben, poderemos:

  • Lanzar o propio servizo moito antes
  • Contacta coa nube onde se atopa a copia de seguridade moito antes
  • Moito antes é posible comprender en que modo se atopa o sistema: arranque normal ou recuperación
  • Moitos menos ficheiros para recuperar con antelación
  • Permite que o usuario comece aínda máis rápido.

Que é, en realidade, unha aplicación nativa?

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

Windows Aplicacións nativas e servizo Acronis Active Restore
Pavel Yosifovich — Windows Programación do núcleo (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ó comproba os argumentos de entrada e chama á función. NtCreateFile (O prefixo Nt indica que a función é nativa). Esta función declárase no ficheiro de cabeceira winternl.h e impleméntase en ntdll.dll. Prepárase para o salto ao espazo do kernel, despois do cal realiza unha chamada ao sistema para crear o ficheiro. Neste caso, Kernel32 é simplemente un envoltorio para Ntdll. Unha das razóns disto é para que Microsoft poida modificar as funcións nativas sen afectar 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 significativamente antes que kernel32. Isto é lóxico, xa que kernel32 require ntdll para funcionar. Como resultado, as aplicacións que usan funcións nativas poden comezar a funcionar significativamente antes.

Así, o Windows As aplicacións nativas son programas que se poden executar cedo durante o arranque. WindowsUsan SÓ funcións de ntdll. Un exemplo dunha aplicación deste tipo: autochk que realiza utilidade chkdisk Para comprobar se hai erros no disco antes de iniciar os servizos principais. Este é exactamente o nivel no que queremos que funcione a nosa Restauración activa.

Que necesitamos?

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

Que hai no código?

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

  1. Mostra unha mensaxe na pantalla
  2. Asigna algo de memoria
  3. Agardando pola entrada do teclado
  4. Libera memoria ocupada

Nas aplicacións nativas, o punto de entrada non é main nin winmain, senón a función NtProcessStartup, xa que en realidade estamos a iniciar directamente un novo proceso no sistema.

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

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

Dado que só temos acceso ás funcións desde ntdll e aínda non hai outras bibliotecas na memoria, seguro que teremos problemas para asignar memoria. O operador new aínda non existe (porque provén do mundo de nivel demasiado alto de C++) e non hai ningunha función malloc (require bibliotecas C en tempo de execución). Por suposto, poderiamos usar simplemente a pila. Pero se precisamos asignar memoria dinamicamente, teremos que facelo no heap. Así que imos crear un heap para nós mesmos e coller memoria del cando a precisemos.

A función axeitada para esta tarefa é RtlCreateHeapA continuación, usando RtlAllocateHeap e RtlFreeHeap, asignaremos e liberaremos memoria cando a precisemos.

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 a esperar 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"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;
	}
}

O único que temos que facer é usalo NtReadFile no dispositivo aberto e agardar a que o teclado devolva unha pulsación de tecla. Se se preme a tecla ESC, continuaremos traballando. Para abrir o dispositivo, teremos que chamar á función NtCreateFile (DeviceKeyboardClass0 debe estar aberta). Tamén chamaremos NtCreateEventpara inicializar o obxecto wait. Declararemos manualmente a estrutura KEYBOARD_INPUT_DATA, que representa os datos do teclado. Isto facilitaranos o traballo.

A aplicación nativa remata cunha chamada a función NtTerminateProcess, porque simplemente estamos matando o noso propio proceso.

O código completo 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 deter o depurador. Non obstante, teremos que conectar WinDbg á máquina virtual para a depuración do kernel. As instrucións sobre como facelo pódense atopar aquí. aquí ou simplemente usar VirtualKD.

Compilación e montaxe

A forma máis sinxela de crear unha aplicación nativa é usar DDK (Kit de desenvolvemento de controladores). Necesitamos especificamente a sétima versión antiga, xa que as versións posteriores teñen unha abordaxe lixeiramente diferente e funcionan en estreita colaboración con Visual Studio. Se usamos o DDK, o noso proxecto só precisa un Makefile e os códigos fonte.

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 os códigos fonte con máis detalle. Este ficheiro especifica o código fonte do teu programa (ficheiros .c), as opcións de compilación e outros parámetros.

  • TARGETNAME: o nome do ficheiro executábel que se debería producir como resultado.
  • TIPO DE OBXECTIVO: o tipo de ficheiro executábel. Podería ser un controlador (.sys), nese caso o valor do campo debería ser CONTROLADOR; se é unha biblioteca (.lib), o valor debería ser BIBLIOTECA. No noso caso, necesitamos un ficheiro executábel (.exe), polo que definimos o valor como PROGRAMA.
  • UMTYPE: valores posibles para este campo: console para unha aplicación de consola, windows para executarse en modo de xanela. Non obstante, precisamos especificar nt para obter unha aplicación nativa.
  • BUFFER_OVERFLOW_CHECKS: comprobación do desbordamento do búfer na pila, por desgraza non é o noso caso, deshabilitámolo.
  • MINWIN_SDK_LIB_PATH: este valor fai referencia á variable SDK_LIB_PATH. Non te preocupes se non tes unha variable de sistema similar declarada. Cando executamos unha compilación comprobada desde DDK, esta variable declararase e apuntará ás bibliotecas necesarias.
  • FONTES – unha lista das fontes do teu programa.
  • INCLUDES – ficheiros de cabeceira necesarios para a compilación. Isto normalmente especifica a ruta aos ficheiros incluídos no DDK, pero podes especificar calquera outro adicional.
  • TARGETLIBS – lista de bibliotecas para enlazar.
  • USE_NTDLL é un campo obrigatorio que debe definirse como 1. Por razóns obvias.
  • USER_C_FLAGS: calquera indicador que se poida usar nas directivas do preprocesador ao preparar o código da aplicación.

Entón, para compilar, precisamos executar a versión marcada con x86 (ou x64), cambiar o directorio de traballo ao cartafol do proxecto e executar o comando Build. A captura de pantalla mostra que compilamos un único ficheiro executábel.

Windows Aplicacións nativas e servizo Acronis Active Restore

Este ficheiro non se executará tan facilmente; o sistema quéixase e pídenos que pensemos no seu comportamento co seguinte erro:

Windows Aplicacións nativas e servizo Acronis Active Restore

Como lanzar unha aplicación nativa?

Cando se inicia autochk, a secuencia de inicio do programa está determinada polo valor da clave do rexistro:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

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

autocheck autochk *MyNative

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

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

  1. Copia o ficheiro executábel ao cartafol system32
  2. Engadir unha clave ao rexistro
  3. Reiniciar a máquina

Para a súa comodidade, aquí ten un script xa feito 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 o reinicio, antes de que apareza a pantalla de selección de usuario, veremos a seguinte imaxe:

Windows Aplicacións nativas e servizo Acronis Active Restore

Total

Usando esta pequena aplicación como exemplo, estabamos convencidos de que lanzar unha aplicación ao nivel Windows O nativo é totalmente posible. A continuación, os rapaces da Universidade de Innopolis e eu continuaremos a construír un servizo que iniciará a interacción co controlador moito antes que na versión anterior do noso proxecto. E coa chegada do shell de Win32, será lóxico transferir o control a un servizo completo que xa foi desenvolvido (máis sobre iso máis tarde). aquí).

No noso seguinte artigo, trataremos outro compoñente do servizo Active Restore: o controlador UEFI. Subscríbete ao noso blog para manterte ao día da seguinte publicación.

Fonte: www.habr.com

Compre hospedaxe fiable para sitios con protección DDoS, servidores VPS VDS 🔥 Compra aloxamento web fiable con protección DDoS, servidores VPS VDS | ProHoster