Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore

Sot ne vazhdojmë historinë se si ne, së bashku me djemtë nga Universiteti Innopolis, po zhvillojmë teknologjinë Active Restore për të lejuar përdoruesin të fillojë të punojë në makinën e tij sa më shpejt të jetë e mundur pas një dështimi. Ne do të flasim për aplikacionet amtare të Windows, duke përfshirë veçoritë e krijimit dhe nisjes së tyre. Më poshtë prerjes është pak për projektin tonë, si dhe një udhëzues praktik se si të shkruani aplikacione vendase.

Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore

Në postimet e mëparshme kemi folur tashmë për atë që është Rivendosja aktive, dhe si zhvillohen studentët nga Innopolis shërbim. Sot dua të përqendrohem në aplikacionet vendase, në nivelin e të cilave duam të "varrosim" shërbimin tonë aktiv të rimëkëmbjes. Nëse gjithçka funksionon, atëherë ne do të jemi në gjendje të:

  • Nisni vetë shërbimin shumë më herët
  • Kontaktoni renë ku ndodhet rezervimi shumë më herët
  • Shumë më herët për të kuptuar se në cilën mënyrë është sistemi - nisja normale ose rikuperimi
  • Shumë më pak skedarë për t'u rikuperuar paraprakisht
  • Lejo përdoruesin të fillojë edhe më shpejt.

Çfarë është një aplikacion vendas gjithsesi?

Për t'iu përgjigjur kësaj pyetjeje, le të shohim sekuencën e thirrjeve që bën sistemi, për shembull, nëse një programues në aplikacionin e tij përpiqet të krijojë një skedar.

Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore
Pavel Yosifovich - Programimi i kernelit të Windows (2019)

Programuesi përdor funksionin KrijoFile, i cili deklarohet në skedarin e kokës fileapi.h dhe zbatohet në Kernel32.dll. Sidoqoftë, vetë ky funksion nuk krijon skedarin, ai vetëm kontrollon argumentet hyrëse dhe thërret funksionin NtCreateFile (prefiksi Nt thjesht tregon se funksioni është vendas). Ky funksion deklarohet në skedarin e kokës winternl.h dhe zbatohet në ntdll.dll. Ai përgatitet të hidhet në hapësirën bërthamore, pas së cilës bën një thirrje sistemi për të krijuar një skedar. Në këtë rast, rezulton se Kernel32 është vetëm një mbështjellës për Ntdll. Një nga arsyet pse u bë kjo është se Microsoft kështu ka aftësinë të ndryshojë funksionet e botës amtare, por të mos prekë ndërfaqet standarde. Microsoft nuk rekomandon thirrjen direkte të funksioneve vendase dhe nuk dokumenton shumicën e tyre. Nga rruga, funksionet e padokumentuara mund të gjenden këtu.

Avantazhi kryesor i aplikacioneve vendase është se ntdll ngarkohet në sistem shumë më herët se kernel32. Kjo është logjike, sepse kernel32 kërkon ntdll për të punuar. Si rezultat, aplikacionet që përdorin funksione vendase mund të fillojnë të punojnë shumë më herët.

Kështu, aplikacionet e Windows Native janë programe që mund të fillojnë herët në nisjen e Windows. Ata përdorin VETËM funksione nga ntdll. Një shembull i një aplikacioni të tillë: autochk që kryen chkdisk utility për të kontrolluar diskun për gabime përpara se të nisni shërbimet kryesore. Ky është pikërisht niveli që duam të jetë "Rivendosja jonë aktive".

Çfarë na nevojitet?

  • DDK (Driver Development Kit), tani i njohur edhe si WDK 7 (Windows Driver Kit).
  • Makinë virtuale (për shembull, Windows 7 x64)
  • Nuk është e nevojshme, por skedarët e kokës që mund të shkarkohen mund të ndihmojnë këtu

Çfarë ka në kod?

Le të praktikojmë pak dhe, për shembull, të shkruajmë një aplikacion të vogël që:

  1. Shfaq një mesazh në ekran
  2. Ndan pak memorie
  3. Pret për hyrjen e tastierës
  4. Liron kujtesën e përdorur

Në aplikacionet vendase, pika e hyrjes nuk është kryesore ose winmain, por funksioni NtProcessStartup, pasi ne në fakt nisim drejtpërdrejt procese të reja në sistem.

Le të fillojmë duke shfaqur një mesazh në ekran. Për këtë kemi një funksion amtare NtDisplayString, i cili merr si argument një tregues për një objekt strukture UNICODE_STRING. RtlInitUnicodeString do të na ndihmojë ta inicializojmë atë. Si rezultat, për të shfaqur tekstin në ekran mund të shkruajmë këtë funksion të vogël:

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

Meqenëse vetëm funksionet nga ntdll janë të disponueshme për ne, dhe thjesht nuk ka ende biblioteka të tjera në memorie, ne patjetër do të kemi probleme me mënyrën e ndarjes së kujtesës. Operatori i ri nuk ekziston ende (sepse vjen nga bota e nivelit shumë të lartë të C++), dhe nuk ka asnjë funksion malloc (kërkon biblioteka C në kohëzgjatje). Sigurisht, mund të përdorni vetëm një pirg. Por nëse na duhet të shpërndajmë memorie në mënyrë dinamike, do të duhet ta bëjmë atë në grumbull (d.m.th. në grumbull). Pra, le të krijojmë një grumbull për veten tonë dhe të marrim kujtim prej tij sa herë që kemi nevojë.

Funksioni është i përshtatshëm për këtë detyrë RtlCreateHeap. Më pas, duke përdorur RtlAllocateHeap dhe RtlFreeHeap, ne do të zëmë dhe do të çlirojmë memorie kur të na nevojitet.

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

Le të kalojmë në pritjen e hyrjes në tastierë.

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

Gjithçka që na duhet është të përdorim NtReadFile në një pajisje të hapur dhe prisni derisa tastiera të na kthejë ndonjë shtypje. Nëse shtypet tasti ESC, ne do të vazhdojmë të punojmë. Për të hapur pajisjen, do të na duhet të thërrasim funksionin NtCreateFile (do të duhet të hapim DeviceKeyboardClass0). Ne gjithashtu do të telefonojmë NtCreateEventpër të inicializuar objektin e pritjes. Ne do ta deklarojmë vetë strukturën KEYBOARD_INPUT_DATA, e cila përfaqëson të dhënat e tastierës. Kjo do të na lehtësojë punën.

Aplikacioni origjinal përfundon me një thirrje funksioni NtTerminateProcesssepse ne thjesht po vrasim procesin tonë.

I gjithë kodi për aplikacionin tonë të vogël:

#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: Ne mund të përdorim lehtësisht funksionin DbgBreakPoint() në kodin tonë për ta ndaluar atë në korrigjues. Vërtetë, do t'ju duhet të lidhni WinDbg me një makinë virtuale për korrigjimin e kernelit. Mund të gjenden udhëzime se si ta bëni këtë këtu ose thjesht përdorni KD virtuale.

Kompilimi dhe montimi

Mënyra më e lehtë për të ndërtuar një aplikacion vendas është përdorimi DDK (Kit për zhvillimin e shoferit). Ne kemi nevojë për versionin e shtatë të lashtë, pasi versionet e mëvonshme kanë një qasje paksa të ndryshme dhe punojnë ngushtë me Visual Studio. Nëse përdorim DDK, atëherë projekti ynë ka nevojë vetëm për Makefile dhe burime.

makefile

!INCLUDE $(NTMAKEENV)makefile.def

Burime:

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

Makefile juaj do të jetë saktësisht i njëjtë, por le t'i shikojmë burimet pak më në detaje. Ky skedar specifikon burimet e programit tuaj (skedarët .c), opsionet e ndërtimit dhe parametrat e tjerë.

  • TARGETNAME – emri i skedarit të ekzekutueshëm që duhet të prodhohet në fund.
  • TARGETTYPE – lloji i skedarit të ekzekutueshëm, mund të jetë një drejtues (.sys), atëherë vlera e fushës duhet të jetë DRIVER, nëse një bibliotekë (.lib), atëherë vlera është LIBRARY. Në rastin tonë, ne kemi nevojë për një skedar të ekzekutueshëm (.exe), kështu që ne vendosim vlerën në PROGRAM.
  • UMTYPE - vlerat e mundshme për këtë fushë: tastierë për një aplikacion konsol, dritare për të punuar në modalitetin e dritareve. Por ne duhet të specifikojmë nt për të marrë një aplikacion vendas.
  • BUFFER_OVERFLOW_CHECKS – kontrollimi i pirgut për tejmbushje të tamponit, për fat të keq jo në rastin tonë, ne e fikim atë.
  • MINWIN_SDK_LIB_PATH – kjo vlerë i referohet ndryshores SDK_LIB_PATH, mos u shqetësoni se nuk keni të deklaruar një variabël të tillë sistemi, kur të ekzekutojmë ndërtimin e kontrolluar nga DDK, kjo ndryshore do të deklarohet dhe do të tregojë bibliotekat e nevojshme.
  • BURIMET – një listë e burimeve për programin tuaj.
  • PËRFSHIN – skedarët e kokës që kërkohen për montim. Këtu ata zakonisht tregojnë shtegun për skedarët që vijnë me DDK, por ju mund të specifikoni gjithashtu ndonjë tjetër.
  • TARGETLIBS – lista e bibliotekave që duhet të lidhen.
  • USE_NTDLL është një fushë e detyrueshme që duhet vendosur në 1 për arsye të dukshme.
  • USER_C_FLAGS – çdo flamur që mund të përdorni në direktivat e paraprocesorit kur përgatitni kodin e aplikacionit.

Pra, për të ndërtuar, duhet të ekzekutojmë x86 (ose x64) Checked Build, të ndryshojmë drejtorinë e punës në dosjen e projektit dhe të ekzekutojmë komandën Build. Rezultati në pamjen e ekranit tregon se kemi një skedar të ekzekutueshëm.

Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore

Ky skedar nuk mund të hapet kaq lehtë, sistemi mallkon dhe na dërgon të mendojmë për sjelljen e tij me gabimin e mëposhtëm:

Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore

Si të hapni një aplikacion vendas?

Kur fillon autochk, sekuenca e nisjes së programeve përcaktohet nga vlera e çelësit të regjistrit:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Menaxheri i sesionit ekzekuton programet nga kjo listë një nga një. Menaxheri i sesionit kërkon vetë skedarët e ekzekutueshëm në direktorinë system32. Formati i vlerës së çelësit të regjistrit është si më poshtë:

autocheck autochk *MyNative

Vlera duhet të jetë në format heksadecimal, jo në ASCII të zakonshme, kështu që çelësi i treguar më sipër do të jetë në formatin:

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

Për të kthyer titullin, mund të përdorni një shërbim në internet, për shembull, этот.

Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore
Rezulton se për të nisur një aplikacion vendas, na duhen:

  1. Kopjoni skedarin e ekzekutueshëm në dosjen system32
  2. Shtoni një çelës në regjistër
  3. Rinisni makinën

Për lehtësi, këtu është një skenar i gatshëm për instalimin e një aplikacioni vendas:

instaloj.bat

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

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

Pas instalimit dhe rindezjes, edhe para se të shfaqet ekrani i përzgjedhjes së përdoruesit, do të marrim foton e mëposhtme:

Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore

Total

Duke përdorur shembullin e një aplikacioni kaq të vogël, ne ishim të bindur se është mjaft e mundur të ekzekutohet aplikacioni në nivelin Windows Native. Më pas, djemtë nga Universiteti Innopolis dhe unë do të vazhdojmë të ndërtojmë një shërbim që do të fillojë procesin e ndërveprimit me shoferin shumë më herët sesa në versionin e mëparshëm të projektit tonë. Dhe me ardhjen e guaskës win32, do të ishte logjike të transferohej kontrolli në një shërbim të plotë që është zhvilluar tashmë (më shumë për këtë këtu).

Në artikullin tjetër do të prekim një komponent tjetër të shërbimit Active Restore, përkatësisht shoferin UEFI. Abonohuni në blogun tonë që të mos humbisni postimin e radhës.

Burimi: www.habr.com

Shto një koment