ProHoster > Blog > administratë > Aplikacionet Native të Windows dhe shërbimi Acronis Active Restore
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.
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.
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ë:
Shfaq një mesazh në ekran
Ndan pak memorie
Pret për hyrjen e tastierës
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);
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.
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:
Si të hapni një aplikacion vendas?
Kur fillon autochk, sekuenca e nisjes së programeve përcaktohet nga vlera e çelësit të regjistrit:
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:
Pas instalimit dhe rindezjes, edhe para se të shfaqet ekrani i përzgjedhjes së përdoruesit, do të marrim foton e mëposhtme:
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.