Windows Native Applications kaj Acronis Active Restore servo

Hodiaŭ ni daŭrigas la rakonton pri kiel ni, kune kun la infanoj de Innopolis University, disvolvas Active Restore-teknologion por permesi al la uzanto komenci labori sur sia maŝino kiel eble plej baldaŭ post fiasko. Ni parolos pri denaskaj Vindozaj aplikoj, inkluzive de la funkcioj de ilia kreado kaj lanĉo. Sub la tranĉo estas iom pri nia projekto, kaj ankaŭ praktika gvidilo pri kiel verki indiĝenajn aplikojn.

Windows Native Applications kaj Acronis Active Restore servo

En antaŭaj afiŝoj ni jam parolis pri kio ĝi estas Aktiva Restarigo, kaj kiel studentoj de Innopolis evoluas servo. Hodiaŭ mi volas koncentriĝi pri indiĝenaj aplikoj, al kies nivelo ni volas "entombigi" nian aktivan reakivan servon. Se ĉio funkcias, tiam ni povos:

  • Lanĉu la servon mem multe pli frue
  • Kontaktu la nubon, kie troviĝas la sekurkopio, multe pli frue
  • Multe pli frue por kompreni en kia reĝimo estas la sistemo - normala lanĉo aŭ reakiro
  • Multe malpli da dosieroj por retrovi anticipe
  • Permesu al la uzanto komenci eĉ pli rapide.

Kio estas denaska aplikaĵo ĉiukaze?

Por respondi ĉi tiun demandon, ni rigardu la sinsekvon de alvokoj, kiujn la sistemo faras, ekzemple, se programisto en sia aplikaĵo provas krei dosieron.

Windows Native Applications kaj Acronis Active Restore servo
Pavel Yosifovich - Vindoza Kerna Programado (2019)

La programisto uzas la funkcion Krei dosieron, kiu estas deklarita en la kapdosiero fileapi.h kaj efektivigita en Kernel32.dll. Tamen, ĉi tiu funkcio mem ne kreas la dosieron, ĝi nur kontrolas la enigajn argumentojn kaj vokas la funkcion NtCreateFile (la prefikso Nt nur indikas, ke la funkcio estas denaska). Ĉi tiu funkcio estas deklarita en la kapa dosiero winternl.h kaj efektivigita en ntdll.dll. Ĝi prepariĝas por salti en nuklean spacon, post kio ĝi faras sistemvokon por krei dosieron. En ĉi tiu kazo, rezultas, ke Kernel32 estas nur envolvaĵo por Ntdll. Unu el la kialoj kial tio estis farita estas ke Microsoft tiel havas la kapablon ŝanĝi la funkciojn de la denaska mondo, sed ne tuŝi la normajn interfacojn. Mikrosofto ne rekomendas voki denaskajn funkciojn rekte kaj ne dokumentas la plej multajn el ili. Cetere, nedokumentitaj funkcioj troveblas tie.

La ĉefa avantaĝo de indiĝenaj aplikoj estas, ke ntdll estas ŝarĝita en la sistemon multe pli frue ol kernel32. Ĉi tio estas logika, ĉar kernel32 postulas ntdll por funkcii. Kiel rezulto, aplikoj kiuj uzas denaskajn funkciojn povas komenci labori multe pli frue.

Tiel, Windows Native Applications estas programoj kiuj povas komenci frue en Vindoza ekkuro. Ili NUR uzas funkciojn de ntdll. Ekzemplo de tia aplikaĵo: autochk kiu plenumas chkdisk ilo por kontroli la diskon por eraroj antaŭ ol komenci la ĉefajn servojn. Ĉi tio estas ĝuste la nivelo, kiun ni volas, ke nia Aktiva Restarigo estu.

Kion ni bezonas?

  • DDK (Driver Development Kit), nun ankaŭ konata kiel WDK 7 (Windows Driver Kit).
  • Virtuala maŝino (ekzemple Windows 7 x64)
  • Ne necesas, sed kapdosieroj elŝuteblaj povas helpi tie

Kio estas en la kodo?

Ni ekzercu iomete kaj, ekzemple, skribu malgrandan aplikaĵon, kiu:

  1. Montras mesaĝon sur la ekrano
  2. Asignas iom da memoro
  3. Atendas klavaran enigon
  4. Liberigas uzitan memoron

En indiĝenaj aplikoj, la enirpunkto ne estas ĉefa aŭ winmain, sed la funkcio NtProcessStartup, ĉar ni fakte rekte lanĉas novajn procezojn en la sistemo.

Ni komencu montrante mesaĝon sur la ekrano. Por tio ni havas denaskan funkcion NtDisplayString, kiu prenas kiel argumenton montrilon al UNICODE_STRING-strukturobjekto. RtlInitUnicodeString helpos nin pravalorigi ĝin. Kiel rezulto, por montri tekston sur la ekrano ni povas skribi ĉi tiun malgrandan funkcion:

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

Ĉar nur funkcioj de ntdll disponeblas al ni, kaj simple ankoraŭ ne estas aliaj bibliotekoj en memoro, ni certe havos problemojn pri kiel asigni memoron. La nova funkciigisto ankoraŭ ne ekzistas (ĉar ĝi venas de la tro altnivela mondo de C++), kaj ne ekzistas malloc-funkcio (ĝi postulas rultempajn C-bibliotekojn). Kompreneble, vi povas nur uzi stakon. Sed se ni bezonas dinamike asigni memoron, ni devos fari ĝin sur la amaso (t.e. amaso). Do ni kreu amason por ni mem kaj prenu memoron de ĝi kiam ajn ni bezonas ĝin.

La funkcio taŭgas por ĉi tiu tasko RtlCreateHeap. Poste, uzante RtlAllocateHeap kaj RtlFreeHeap, ni okupos kaj liberigos memoron kiam ni bezonos ĝin.

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

Ni daŭrigu atendi la klavarenigon.

// 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;
	}
}

Ĉio, kion ni bezonas, estas uzi NtReadFile sur malfermita aparato, kaj atendu ĝis la klavaro resendas iun ajn premon al ni. Se la ESC-klavo estas premata, ni daŭre laboros. Por malfermi la aparaton, ni devos voki la funkcion NtCreateFile (ni bezonos malfermi DeviceKeyboardClass0). Ni ankaŭ vokos NtCreateEventpravalorigi la atendan objekton. Ni mem deklaros la strukturon KEYBOARD_INPUT_DATA, kiu reprezentas la klavardatumojn. Ĉi tio faciligos nian laboron.

La indiĝena aplikaĵo finiĝas per funkciovoko NtTerminateProcessĉar ni simple mortigas nian propran procezon.

La tuta kodo por nia malgranda aplikaĵo:

#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: Ni povas facile uzi la funkcion DbgBreakPoint() en nia kodo por haltigi ĝin en la erarserĉilo. Vere, vi devos konekti WinDbg al virtuala maŝino por kerna senararigado. Instrukcioj pri kiel fari tion troveblas tie aŭ simple uzi VirtualKD.

Kompilo kaj muntado

La plej facila maniero por konstrui indiĝenan aplikaĵon estas uzi DDK (Driver Development Kit). Ni bezonas la antikvan sepan version, ĉar pli postaj versioj havas iomete malsaman aliron kaj laboras proksime kun Visual Studio. Se ni uzas la DDK, tiam nia projekto bezonas nur Makefile kaj fontojn.

Faru dosieron

!INCLUDE $(NTMAKEENV)makefile.def

fontoj:

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

Via Makefile estos ĝuste la sama, sed ni rigardu fontojn iom pli detale. Ĉi tiu dosiero specifas la fontojn de via programo (.c-dosieroj), konstruopciojn kaj aliajn parametrojn.

  • TARGETNAME - la nomo de la plenumebla dosiero kiu devus esti produktita finfine.
  • TARGETTYPE - tipo de rulebla dosiero, ĝi povas esti pelilo (.sys), tiam la kampovaloro devus esti DRIVER, se biblioteko (.lib), tiam la valoro estas LIBRARY. En nia kazo, ni bezonas ruleblan dosieron (.exe), do ni fiksas la valoron al PROGRAMO.
  • UMTYPE - eblaj valoroj por ĉi tiu kampo: konzolo por konzola aplikaĵo, fenestroj por labori en fenestra reĝimo. Sed ni devas specifi nt por ricevi denaskan aplikaĵon.
  • BUFFER_OVERFLOW_CHECKS - kontrolante la stakon por bufro superfluo, bedaŭrinde ne nia kazo, ni malŝaltas ĝin.
  • MINWIN_SDK_LIB_PATH - ĉi tiu valoro rilatas al la variablo SDK_LIB_PATH, ne zorgu, ke vi ne havas tian sisteman variablon deklarita, kiam ni rulas kontrolitan konstruon de la DDK, ĉi tiu variablo estos deklarita kaj indikos al la necesaj bibliotekoj.
  • FONTOJ - listo de fontoj por via programo.
  • INKLUAS - kapdosierojn kiuj estas bezonataj por kunigo. Ĉi tie ili kutime indikas la vojon al la dosieroj kiuj venas kun la DDK, sed vi povas aldone specifi iujn ajn aliajn.
  • TARGETLIBS - listo de bibliotekoj kiuj devas esti ligitaj.
  • USE_NTDLL estas postulata kampo kiu devas esti agordita al 1 pro evidentaj kialoj.
  • USER_C_FLAGS - ajnaj flagoj, kiujn vi povas uzi en antaŭprocesoraj direktivoj kiam vi preparas aplikan kodon.

Do por konstrui, ni devas ruli x86 (aŭ x64) Checked Build, ŝanĝi la labordosierujon al la projekta dosierujo kaj ruli la Build-komandon. La rezulto en la ekrankopio montras, ke ni havas unu plenumeblan dosieron.

Windows Native Applications kaj Acronis Active Restore servo

Ĉi tiu dosiero ne povas esti lanĉita tiel facile, la sistemo malbenas kaj sendas nin pensi pri ĝia konduto kun la jena eraro:

Windows Native Applications kaj Acronis Active Restore servo

Kiel lanĉi denaskan aplikaĵon?

Kiam aŭtochk komenciĝas, la startsekvenco de programoj estas determinita de la valoro de la registra ŝlosilo:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

La seanca administranto efektivigas programojn el ĉi tiu listo unu post la alia. La seanca administranto serĉas la ruleblajn dosierojn mem en la dosierujo system32. La registr-ŝlosila valorformato estas jena:

autocheck autochk *MyNative

La valoro devas esti en deksesuma formato, ne la kutima ASCII, do la ŝlosilo montrita supre estos en la 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

Por konverti la titolon, vi povas uzi interretan servon, ekzemple, ĉi tio.

Windows Native Applications kaj Acronis Active Restore servo
Rezultas, ke por lanĉi denaskan aplikaĵon, ni bezonas:

  1. Kopiu la plenumeblan dosieron al la dosierujo system32
  2. Aldonu ŝlosilon al la registro
  3. Rekomencu la maŝinon

Por komforto, jen preta skripto por instali denaskan aplikaĵon:

instali.bat

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

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

Post instalado kaj rekomenco, eĉ antaŭ ol la ekrano de elekto de uzantoj aperos, ni ricevos la jenan bildon:

Windows Native Applications kaj Acronis Active Restore servo

La rezulto

Uzante la ekzemplon de tia malgranda aplikaĵo, ni estis konvinkitaj, ke estas tute eble ruli la aplikaĵon je la Windows Native-nivelo. Poste, la infanoj de Innopolis University kaj mi daŭre konstruos servon, kiu komencos la procezon de interago kun la ŝoforo multe pli frue ol en la antaŭa versio de nia projekto. Kaj kun la apero de la win32-ŝelo, estus logike transdoni kontrolon al plenrajta servo, kiu jam estis disvolvita (pli pri ĉi tio tie).

En la sekva artikolo ni tuŝos alian komponanton de la Aktiva Restariga servo, nome la UEFI-ŝoforo. Abonu nian blogon por ke vi ne maltrafu la sekvan afiŝon.

fonto: www.habr.com

Aldoni komenton