Sot vazhdojmë tregimin për mënyrën se si ne, së bashku me djemtë nga Universiteti Innopolis, po zhvillojmë teknologjinë Active Restore për të lejuar përdoruesin të fillojë punën sa më shpejt që të jetë e mundur pas një dështimi. Do të flasim për aplikacionet native të Windows, duke përfshirë karakteristikat e ndërtimit dhe ekzekutimit të tyre. Nën kat - pak rreth projektit tonë, si dhe një udhëzues praktik për shkrimin e aplikacioneve native.

NĂ« postimet e kaluara, ne kemi folur pĂ«r se çfarĂ« Ă«shtĂ« , dhe si studentĂ«t nga Innopolis po zhvillojnĂ« . Sot dua tĂ« ndalem nĂ« aplikacionet native, nĂ« nivelin e tĂ« cilave dĂ«shirojmĂ« tĂ« âzhbimâ shĂ«rbimin tonĂ« tĂ« rikthimit aktiv. NĂ«se gjithçka shkon mirĂ«, ne do tĂ« mundĂ«m:
- Të nisim shërbimin shumë më herët
- Të lidhemi me cloud-in, ku ndodhet backup-i, shumë më herët
- TĂ« kuptojmĂ« shumĂ« mĂ« herĂ«t nĂ« cilin mod Ă«shtĂ« sistemi â ngarkesa normale apo rikuperimi
- Të rikuperojmë shumë më pak skedarë paraprakisht
- Të lejojmë përdoruesin të fillojë punën akoma më shpejt.
ĂfarĂ« Ă«shtĂ« nĂ« tĂ«rĂ«si njĂ« aplikacion native?
Për të përgjigjur në këtë pyetje, le të shikojmë sekuencën e thirrjeve që sistemi i bën, për shembull, nëse programuesi në aplikacionin e tij përpiqet të krijojë një skedar.

Pavel Yosifovich â Programimi i Kernelit Windows (2019)
Programuesi përdor funksionin , i cili shpallet në skedarin e përkufizimit fileapi.h dhe realizohet në Kernel32.dll. Megjithatë, ky funksion nuk merret me krijimin e skedarëve, ai vetëm kontrollon argumentet në hyrje dhe thërret funksionin (prefiksi Nt tregon se funksioni është natyror). Ky funksion shpallet në skedarin e përkufizimit winternl.h dhe realizohet në ntdll.dll. Ai përgatit për një kërcim në hapësirën bërthamore, pas së cilës bën një thirrje sistemike për krijimin e skedarit. Në këtë rast, del se Kernel32 është vetëm një mbështjellëse për Ntdll. Një nga arsyet për këtë është se Microsoft ka mundësinë të modifikojë funksionet e botës natyrale pa prekur ndërfaqet standarde. Microsoft nuk rekomandon thirrjen e funksioneve natyrale direkt dhe nuk dokumenton shumicën e tyre. Dhe për diçka, funksionet e pa dokumentuara mund të gjenden. .
Avantazhi kryesor i aplikacioneve natyrore është se ntdll ngarkohet në sistem shumë më herët se kernel32. Kjo ka kuptim, pasi kernel32 kërkon prania e ntdll për të funksionuar. Si pasojë, aplikacionet që përdorin funksione natyrore mund të fillojnë të punojnë shumë më herët.
KĂ«shtu, Aplikacionet Natyrore tĂ« Windows janĂ« programe qĂ« mund tĂ« ekzekutohen nĂ« fazĂ«n e hershme tĂ« ngarkimit tĂ« Windows. Ato pĂ«rdorin VETĂM funksione nga ntdll. NjĂ« shembull i tillĂ« aplikacioni Ă«shtĂ«: i cili ekzekuton pĂ«r tĂ« kontrolluar disqet pĂ«r gabime akoma para se tĂ« fillojnĂ« shĂ«rbimet kryesore. NĂ« kĂ«tĂ« nivel dĂ«shirojmĂ« tĂ« shohim aktivimin tonĂ« tĂ« rikuperimit.
ĂfarĂ« na nevojitet?
- (Pako për Zhvillimin e Drejtorive), tani gjithashtu e njohur si WDK 7 (Pako për Drejtorët e Windows).
- Një makinë virtuale (p.sh., Windows 7 x64)
- Nuk është e domosdoshme, por mund të ndihmojnë skedarët e titujve të cilat mund të shkarkohen.
E çfarë ndodhi në kod?
Le të ushtrojmë pak dhe për shembull të shkruajmë një aplikacion të vogël që:
- Shfaq një mesazh në ekran
- Alokuan pak hapësirë në memorje
- Pret për hyrje nga tastiera
- Ăliron hapĂ«sirĂ«n e zĂ«nĂ«
Në aplikacionet natyrore, pika e hyrjes nuk është main ose winmain, por funksioni NtProcessStartup, pasi ne përfaqësisht fillojmë një proces të ri direkt në sistem.
Le të fillojmë me shfaqjen e një mesazhi në ekran. Për këtë, kemi funksionin natyror , i cili merr si argument një tregues në objektin e strukturës UNICODE_STRING. RtlInitUnicodeString do të na ndihmojë për ta inicializuar 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);
}Duke qenë se kemi qasje vetëm në funksionet nga ntdll dhe nuk ka bibliotekave të tjera në memorie, patjetër do të përballemi me probleme në alokimin e memories. Operatorët new nuk ekzistojnë akoma (sepse ata vijnë nga një botë shumë të lartë të C++), as ka funksionin malloc (për të cilin nevojiten bibliotekat runtime të C). Natyrisht, mund të përdorim vetëm stack-in. Por nëse na nevojitet të alokojmë dinamikisht memorie, do të duhet ta bëjmë këtë në heap. Prandaj, le të krijojmë një heap për veten tonë dhe të marrim memorie nga aty kur t'na nevojitet.
Për këtë detyrë do të përshtatet funksioni . Më pas, duke përdorur RtlAllocateHeap dhe RtlFreeHeap, ne do të alokojmë dhe lirojmë memorien kur na nevojitet.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// krijo heap për të alokuar memorie më vonë
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// aloko buffer me madhësi bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// liro buffer (në të vërtetë nuk është e nevojshme sepse ne shkatërrojmë heap-in në hapin tjetër)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);Të kalojmë në pritjen e hyrjes nga tastiera.
// 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 nevojitet â Ă«shtĂ« tĂ« pĂ«rdorim nĂ« pajisjen e hapur, dhe presim derisa tastiera tĂ« na kthejĂ« ndonjĂ« shtypje. NĂ« rast se shtypet çelĂ«si ESC, ne do tĂ« vazhdojmĂ« punĂ«n. PĂ«r tĂ« hapur pajisjen, na nevojitet tĂ« thĂ«rrasim funksionin NtCreateFile (duhet tĂ« hapim DeviceKeyboardClass0). PĂ«r mĂ« tepĂ«r, do tĂ« thĂ«rrasim , pĂ«r tĂ« inicializuar njĂ« objekt pĂ«r pritjen. Do tĂ« shpallim vetĂ« strukturĂ«n KEYBOARD_INPUT_DATA, e cila pĂ«rfaqĂ«son tĂ« dhĂ«nat e tastierĂ«s. Kjo do ta lehtĂ«sojĂ« punĂ«n tonĂ«.
Funksioni i aplikacionit natyror përfundohet me thirrjen e funksionit , sepse ne thjesht po vrasim procesin tonë.
I gjithë kodi i aplikacionit 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ë kod për të ndaluar në debugger. E vërteta është se do të duhej të lidhnim WinDbg me makinën virtuale për debagimin e kernelit. Udhëzimet se si ta bëni këtë mund të gjenden ose thjesht përdorni .
Kompilimi dhe ndërtimi
Mënyra më e thjeshtë për të ndërtuar një aplikacion natyror është të përdorni (Driver Development Kit). Na nevojitet versioni i vjetër i shtatë, pasi versionet më të reja kanë një qasje pak më ndryshe dhe punojnë ngushtë me Visual Studio. Nëse përdorim DDK, projekti ynë ka nevojë vetëm për Makefile dhe burimet.
Makefile
!INCLUDE $(NTMAKEENV)makefile.defburimet:
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 = 1Makefile juaj do të jetë saktësisht si ky, ndërsa për burimet do të ndalemi pak më shumë. Në këtë skedar specifikohen burimet e programit tuaj (skedarët .c), opsionet e ndërtimit dhe parametrat e tjera.
- TARGETNAME â emri i skedarit ekzekutues qĂ« duhet tĂ« rezultojĂ« nĂ« fund.
- TARGETTYPE â lloji i skedarit ekzekutues, mund tĂ« jetĂ« njĂ« driver (.sys), atĂ«herĂ« vlera e fushĂ«s duhet tĂ« jetĂ« DRIVER, nĂ«se Ă«shtĂ« njĂ« bibliotekĂ« (.lib), vlera LIBRARY. NĂ« rastin tonĂ«, na nevojitet njĂ« skedar ekzekutues (.exe), prandaj vendosim vlerĂ«n PROGRAM.
- UMTYPE â vlerat e mundshme tĂ« kĂ«saj fushe: console pĂ«r aplikacionin e konsolĂ«s, windows pĂ«r funksionimin nĂ« modalitetin e dritares. Por ne duhet tĂ« caktosh nt pĂ«r tĂ« marrĂ« njĂ« aplikacion natyror.
- BUFFER_OVERFLOW_CHECKS â kontrolli i stack-ut pĂ«r mbushjen e tamponit, fatkeqĂ«sisht nuk Ă«shtĂ« rasti ynĂ«, ne e çaktivizojmĂ«.
- MINWIN_SDK_LIB_PATH â kjo vlerĂ« i referohet variablit SDK_LIB_PATH, nuk ka nevojĂ« tĂ« shqetĂ«soheni qĂ« njĂ« variablĂ« tĂ« tillĂ« sistemore nuk Ă«shtĂ« shpallur, nĂ« momentin kur tĂ« nisĂ«m ndĂ«rtimin e kontrolluar nga DDK, ky variabli do tĂ« shpallet dhe do tĂ« tregojĂ« pĂ«r bibliotekat e nevojshme.
- SOURCES â lista e burimeve tĂ« programit tuaj.
- INCLUDES â skedarĂ«t e titulluara qĂ« janĂ« tĂ« nevojshme pĂ«r ndĂ«rtimin. KĂ«tu zakonisht jepni rrugĂ«n pĂ«r skedarĂ«t qĂ« vijnĂ« me DDK, por mund tĂ« jepni gjithashtu çdo tjetĂ«r.
- TARGETLIBS â lista e bibliotekave qĂ« duhet tĂ« linkohen.
- USE_NTDLL â njĂ« fushĂ« e detyrueshme qĂ« duhet tĂ« vendoset nĂ« pozitat 1. PĂ«r arsye kuptimplote.
- USER_C_FLAGS â çdo flamur qĂ« mund tĂ« pĂ«rdorni nĂ« drejtoritĂ« e para-procesit nĂ« pĂ«rgatitjen e kodit tĂ« aplikacionit.
Pra, për ndërtimin na nevojitet të startojmë nga x86 (ose x64) Checked Build, të ndryshojmë katalogun e punës në dosjen e projektit dhe të ekzekutojmë komandën Build. Rezultati në screenshot tregon se kemi ndërtuar një skedar ekzekutiv.

Ky skedar nuk mund të startohet lehtësisht, sistemi ankohet dhe na dërgon të mendojmë për sjelljen e tij me këtë gabim:

Si tĂ« ŃŃаŃŃoni njĂ« aplikacion tĂ« natyrshĂ«m?
Në momentin e fillimit autochk, sekuenca e fillimit të programeve përcaktohet nga vlera e çelësit të regjistrit:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteMenaxheri i seancave ekzekuton një pas një programet nga ky listë. Vetë skedarët ekzekutivë i kërkon menaxheri i seancave në drejtorinë system32. Formati i vlerës së çelësit të regjistrit është si vijon:
autocheck autochk *MyNativeVlera duhet të jetë në formatin heksadecimal, dhe jo në ASCII të zakonshëm, prandaj çelësi i paraqitur më lart do të ketë 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,00Për të konvertuar emrin, mund të përdorni një shërbim online, për shembull, .

Duket se për të nisur aplikacionin natyror, na nevojitet:
- Të kopjojmë skedarin ekzekutiv në dosjen system32
- Të shtojmë një çelës në regjistër
- Të rindezim makinën
Për lehtësi, ja një skenar i gatshëm për instalimin e aplikacionit natyror:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
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,00Pas instalimit dhe rindezjes, edhe para shfaqjes së ekranit të zgjedhjes së përdoruesve do të kemi këtë pamje:

Përfundimi
Me shembullin e një aplikacioni të tillë të vogël, ne u bindëm se është krejtësisht e mundur të nisësh aplikacionin në nivelin Windows Native. Më pas, ne do të vazhdojmë me djemtë nga Universiteti Innopolis për të ndërtuar një shërbim që do të niste procesin e ndërveprimit me driverin shumë më herët se në versionin e mëparshëm të projektit tonë. Dhe me shfaqjen e mbulesës win32, do të kishte kuptim që të kalonim menaxhimin në një shërbim të plotë që tashmë ishte zhvilluar (për këtë më shumë) ).
Në këtë artikull, do të flasim për një komponent tjetër të shërbimit Active Restore, konkretisht për driverin UEFI. Abonohuni në blogun tonë për të mos e humbur postimin e ardhshëm.
Burimi: habr.com
