امروز، ما داستان خود را در مورد چگونگی همکاری با دانشگاه اینوپولیس برای توسعه فناوری بازیابی فعال (Active Restore) ادامه خواهیم داد تا کاربران بتوانند پس از خرابی، در اسرع وقت کار خود را روی دستگاههایشان از سر بگیرند. ما در مورد برنامههای بومی صحبت خواهیم کرد. Windows، از جمله جزئیات ایجاد و راهاندازی آنها. در زیر پیشزمینهای مختصر از پروژه ما و همچنین یک راهنمای عملی در مورد نحوه نوشتن برنامههای بومی ارائه شده است.

در پست های قبلی قبلاً در مورد چیستی آن صحبت کرده ایم ، و چگونگی رشد دانش آموزان از Innopolis . امروز میخواهم روی برنامههای بومی تمرکز کنم، که میخواهیم سرویس بازیابی فعال خود را تا حدی «دفن» کنیم. اگر همه چیز درست شود، ما قادر خواهیم بود:
- خود سرویس را خیلی زودتر راه اندازی کنید
- خیلی زودتر با ابری که پشتیبان در آن قرار دارد تماس بگیرید
- خیلی زودتر برای درک اینکه سیستم در چه حالتی است - بوت معمولی یا بازیابی
- فایل های بسیار کمتری برای بازیابی از قبل
- به کاربر اجازه دهید حتی سریعتر شروع به کار کند.
به هر حال یک برنامه بومی چیست؟
برای پاسخ به این سوال، بیایید به دنباله تماس هایی که سیستم انجام می دهد نگاهی بیندازیم، به عنوان مثال، اگر یک برنامه نویس در برنامه خود سعی در ایجاد یک فایل داشته باشد.

پاول یوسیفوویچ - Windows برنامهنویسی هسته (۲۰۱۹)
برنامه نویس از تابع استفاده می کند که در فایل هدر fileapi.h اعلام شده و در Kernel32.dll پیاده سازی شده است. با این حال، این تابع به خودی خود فایلی را ایجاد نمی کند، فقط آرگومان های ورودی را بررسی می کند و تابع را فراخوانی می کند (پیشوند Nt فقط نشان می دهد که تابع بومی است). این تابع در فایل هدر winternl.h اعلام شده و در ntdll.dll پیاده سازی شده است. برای پرش به فضای هسته ای آماده می شود و پس از آن یک فراخوانی سیستمی برای ایجاد یک فایل برقرار می کند. در این مورد، معلوم می شود که Kernel32 فقط یک پوشش برای Ntdll است. یکی از دلایل انجام این کار این است که مایکروسافت بنابراین توانایی تغییر عملکردهای دنیای بومی را دارد، اما رابط های استاندارد را لمس نمی کند. مایکروسافت فراخوانی مستقیم توابع بومی را توصیه نمی کند و اکثر آنها را مستند نمی کند. به هر حال، توابع غیر مستند را می توان یافت .
مزیت اصلی برنامه های بومی این است که ntdll خیلی زودتر از kernel32 در سیستم بارگذاری می شود. این منطقی است، زیرا kernel32 برای کار کردن به ntdll نیاز دارد. در نتیجه، برنامه هایی که از توابع بومی استفاده می کنند می توانند خیلی زودتر شروع به کار کنند.
به این ترتیب، Windows برنامههای بومی، برنامههایی هستند که میتوانند در اوایل بوت اجرا شوند. Windowsآنها فقط از توابع ntdll استفاده میکنند. نمونهای از چنین کاربردی: که اجرا می کند برای بررسی دیسک برای وجود خطا قبل از شروع سرویس های اصلی. این دقیقا همان سطحی است که ما می خواهیم Active Restore ما باشد.
چه چیزی نیاز داریم؟
- (کیت توسعه درایور)، که اکنون با نام WDK 7 نیز شناخته میشود (Windows کیت درایور).
- ماشین مجازی (مثلاً Windows ۷x۶۴)
- لازم نیست، اما فایل های هدری که می توان دانلود کرد ممکن است کمک کند
در کد چیست؟
بیایید کمی تمرین کنیم و مثلاً یک برنامه کوچک بنویسیم که:
- پیامی را روی صفحه نمایش می دهد
- مقداری حافظه را اختصاص می دهد
- منتظر ورودی صفحه کلید است
- حافظه مصرف شده را آزاد می کند
در برنامه های بومی، نقطه ورودی اصلی یا winmain نیست، بلکه تابع NtProcessStartup است، زیرا ما در واقع به طور مستقیم فرآیندهای جدید را در سیستم راه اندازی می کنیم.
بیایید با نمایش یک پیام روی صفحه شروع کنیم. برای این ما یک تابع بومی داریم ، که به عنوان آرگومان یک اشاره گر به یک شی ساختار UNICODE_STRING می گیرد. RtlInitUnicodeString به ما کمک می کند تا آن را مقداردهی کنیم. در نتیجه، برای نمایش متن روی صفحه، می توانیم این تابع کوچک را بنویسیم:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}از آنجایی که فقط توابع ntdll در دسترس ما هستند و به سادگی هنوز هیچ کتابخانه دیگری در حافظه وجود ندارد، قطعاً در نحوه تخصیص حافظه با مشکل مواجه خواهیم شد. اپراتور جدید هنوز وجود ندارد (زیرا از دنیای بسیار سطح بالای C++ می آید)، و هیچ تابع malloc وجود ندارد (به کتابخانه های C زمان اجرا نیاز دارد). البته، شما فقط می توانید از یک پشته استفاده کنید. اما اگر نیاز به تخصیص پویا حافظه داشته باشیم، باید این کار را روی پشته (یعنی heap) انجام دهیم. پس بیایید یک پشته برای خود بسازیم و هر زمان که به آن نیاز داشتیم از آن خاطره برداریم.
عملکرد برای این کار مناسب است . در مرحله بعد، با استفاده از RtlAllocateHeap و RtlFreeHeap، زمانی که به آن نیاز داشته باشیم، حافظه را اشغال کرده و آزاد می کنیم.
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);بیایید به انتظار برای ورودی صفحه کلید برویم.
// 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;
}
}تنها چیزی که نیاز داریم استفاده از آن است در یک دستگاه باز، و صبر کنید تا صفحه کلید فشار را به ما بازگرداند. اگر کلید ESC فشار داده شود، کار را ادامه می دهیم. برای باز کردن دستگاه، باید تابع NtCreateFile را فراخوانی کنیم (باید DeviceKeyboardClass0 را باز کنیم). ما نیز تماس خواهیم گرفت برای مقداردهی اولیه شی انتظار. ساختار KEYBOARD_INPUT_DATA را خودمان اعلام می کنیم که نشان دهنده داده های صفحه کلید است. این کار ما را راحت تر می کند.
برنامه بومی با یک فراخوانی تابع به پایان می رسد زیرا ما به سادگی روند خودمان را می کشیم.
همه کدهای برنامه کوچک ما:
#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: ما به راحتی می توانیم از تابع DbgBreakPoint() در کد خود برای توقف آن در دیباگر استفاده کنیم. درست است، برای اشکال زدایی هسته باید WinDbg را به یک ماشین مجازی متصل کنید. دستورالعمل نحوه انجام این کار را می توان یافت یا فقط استفاده کنید .
تدوین و مونتاژ
ساده ترین راه برای ساختن یک اپلیکیشن بومی استفاده از آن است (کیت توسعه درایور). ما به نسخه هفتم باستانی نیاز داریم، زیرا نسخه های بعدی رویکرد کمی متفاوت دارند و با ویژوال استودیو همکاری نزدیک دارند. اگر از DDK استفاده کنیم، پروژه ما فقط به Makefile و منابع نیاز دارد.
makefile
!INCLUDE $(NTMAKEENV)makefile.defمنابع:
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 شما دقیقاً یکسان خواهد بود، اما بیایید منابع را با جزئیات بیشتری بررسی کنیم. این فایل منابع برنامه شما (فایل های c)، گزینه های ساخت و سایر پارامترها را مشخص می کند.
- TARGETNAME – نام فایل اجرایی که باید در پایان تولید شود.
- TARGETTYPE - نوع فایل اجرایی، می تواند یک درایور (.sys) باشد، سپس مقدار فیلد باید DRIVER باشد، اگر کتابخانه (.lib) باشد، مقدار آن LIBRARY است. در مورد ما، ما به یک فایل اجرایی (exe.) نیاز داریم، بنابراین مقدار را روی PROGRAM قرار می دهیم.
- UMTYPE - مقادیر ممکن برای این فیلد: کنسول برای یک برنامه کنسول، ویندوز برای کار در حالت پنجره. اما برای دریافت یک برنامه بومی باید nt را مشخص کنیم.
- BUFFER_OVERFLOW_CHECKS - بررسی پشته برای سرریز بافر، متأسفانه مورد ما نیست، ما آن را خاموش می کنیم.
- MINWIN_SDK_LIB_PATH – این مقدار به متغیر SDK_LIB_PATH اشاره دارد، نگران نباشید که چنین متغیر سیستمی را اعلان نکردهاید، زمانی که build بررسی شده را از DDK اجرا میکنیم، این متغیر اعلام میشود و به کتابخانههای لازم اشاره میکند.
- منابع - فهرستی از منابع برای برنامه شما.
- شامل – فایل های هدر است که برای مونتاژ مورد نیاز است. در اینجا آنها معمولاً مسیر فایلهای همراه DDK را نشان میدهند، اما میتوانید سایر موارد را نیز مشخص کنید.
- TARGETLIBS - فهرست کتابخانه هایی که باید پیوند داده شوند.
- USE_NTDLL یک فیلد الزامی است که به دلایل واضح باید روی 1 تنظیم شود.
- USER_C_FLAGS - هر پرچمی که می توانید در دستورالعمل های پیش پردازنده هنگام تهیه کد برنامه استفاده کنید.
بنابراین برای ساخت، باید x86 (یا x64) Checked Build را اجرا کنیم، دایرکتوری کاری را به پوشه پروژه تغییر دهیم و دستور Build را اجرا کنیم. نتیجه در اسکرین شات نشان می دهد که ما یک فایل اجرایی داریم.

این فایل به این راحتی راه اندازی نمی شود، سیستم فحش می دهد و با خطای زیر ما را به فکر رفتار آن می فرستد:

چگونه یک برنامه بومی راه اندازی کنیم؟
هنگامی که autochk شروع می شود، دنباله راه اندازی برنامه ها با مقدار کلید رجیستری تعیین می شود:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteSession manager برنامه های این لیست را یکی یکی اجرا می کند. مدیر جلسه به دنبال فایل های اجرایی خود در فهرست system32 می گردد. فرمت مقدار کلید رجیستری به شرح زیر است:
autocheck autochk *MyNativeمقدار باید در قالب هگزادسیمال باشد، نه ASCII معمول، بنابراین کلید نشان داده شده در بالا در قالب خواهد بود:
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برای تبدیل عنوان، می توانید از یک سرویس آنلاین استفاده کنید، به عنوان مثال، .

به نظر می رسد که برای راه اندازی یک برنامه بومی، ما نیاز داریم:
- فایل اجرایی را در پوشه system32 کپی کنید
- یک کلید به رجیستری اضافه کنید
- دستگاه را مجدد راه اندازی کنید
برای راحتی، در اینجا یک اسکریپت آماده برای نصب یک برنامه بومی وجود دارد:
install.bat
@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pauseadd.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پس از نصب و راه اندازی مجدد، حتی قبل از اینکه صفحه انتخاب کاربر ظاهر شود، تصویر زیر را دریافت خواهیم کرد:

مجموع
با استفاده از این برنامه کوچک به عنوان مثال، ما متقاعد شدیم که راهاندازی یک برنامه در سطح Windows بومیسازی کاملاً امکانپذیر است. در مرحله بعد، من و بچههای دانشگاه اینوپولیس به ساخت سرویسی ادامه خواهیم داد که تعامل با درایور را خیلی زودتر از نسخه قبلی پروژه ما آغاز کند. و با ظهور پوسته Win32، منطقی خواهد بود که کنترل را به یک سرویس کامل که قبلاً توسعه داده شده است، منتقل کنیم (بعداً بیشتر در مورد آن صحبت خواهیم کرد). ).
در مقاله بعدی به یکی دیگر از اجزای سرویس Active Restore یعنی درایور UEFI خواهیم پرداخت. در وبلاگ ما عضو شوید تا پست بعدی را از دست ندهید.
منبع: www.habr.com
