سنواصل اليوم حديثنا عن تعاوننا مع جامعة إينوبوليس لتطوير تقنية الاستعادة النشطة التي تُمكّن المستخدمين من استئناف العمل على أجهزتهم بأسرع وقت ممكن بعد حدوث عطل. وسنتناول في هذا الجزء تطبيقات النظام الأصلية. Windowsبما في ذلك تفاصيل إنشائها وإطلاقها. فيما يلي نبذة مختصرة عن مشروعنا، بالإضافة إلى دليل عملي حول كيفية كتابة تطبيقات أصلية.

في المشاركات السابقة تحدثنا بالفعل عن ما هو عليه وكيف يتطور الطلاب من إنوبوليس . اليوم أريد التركيز على التطبيقات المحلية، إلى المستوى الذي نريد "دفن" خدمة الاسترداد النشطة لدينا. إذا سارت الأمور على ما يرام، فسنكون قادرين على:
- قم بتشغيل الخدمة نفسها قبل ذلك بكثير
- اتصل بالسحابة حيث توجد النسخة الاحتياطية قبل ذلك بكثير
- قبل ذلك بكثير، لفهم الوضع الذي يوجد فيه النظام - التمهيد العادي أو الاسترداد
- عدد أقل بكثير من الملفات التي يمكن استعادتها مسبقًا
- السماح للمستخدم بالبدء بشكل أسرع.
ما هو التطبيق الأصلي على أي حال؟
للإجابة على هذا السؤال، دعونا ننظر إلى تسلسل الاستدعاءات التي يقوم بها النظام، على سبيل المثال، إذا حاول أحد المبرمجين في تطبيقه إنشاء ملف.

بافيل يوسيفوفيتش — Windows برمجة النواة (2019)
يستخدم المبرمج الوظيفة ، والذي تم الإعلان عنه في الملف الرأسي fileapi.h وتم تنفيذه في Kernel32.dll. ومع ذلك، هذه الوظيفة نفسها لا تقوم بإنشاء الملف، بل تتحقق فقط من وسائط الإدخال وتستدعي الوظيفة (تشير البادئة Nt فقط إلى أن الوظيفة أصلية). تم الإعلان عن هذه الوظيفة في ملف رأس Winternl.h وتنفيذها في ntdll.dll. يستعد للقفز إلى الفضاء النووي، وبعد ذلك يقوم باستدعاء النظام لإنشاء ملف. في هذه الحالة، اتضح أن Kernel32 هو مجرد غلاف لـ Ntdll. أحد أسباب القيام بذلك هو أن Microsoft لديها القدرة على تغيير وظائف العالم الأصلي، ولكن دون لمس الواجهات القياسية. لا توصي Microsoft باستدعاء الوظائف الأصلية مباشرة ولا تقوم بتوثيق معظمها. بالمناسبة، يمكن العثور على وظائف غير موثقة .
الميزة الرئيسية للتطبيقات الأصلية هي أن ntdll يتم تحميله في النظام في وقت أبكر بكثير من kernel32. وهذا أمر منطقي، لأن kernel32 يتطلب ntdll للعمل. ونتيجة لذلك، يمكن للتطبيقات التي تستخدم الوظائف الأصلية أن تبدأ العمل في وقت أبكر بكثير.
وهكذا، Windows التطبيقات الأصلية هي برامج يمكن تشغيلها مبكراً أثناء بدء التشغيل. Windowsتستخدم هذه التطبيقات وظائف من مكتبة ntdll فقط. مثال على ذلك: من يؤدي لفحص القرص بحثًا عن الأخطاء قبل بدء الخدمات الرئيسية. هذا هو بالضبط المستوى الذي نريد أن تكون عليه الاستعادة النشطة.
ماذا نحتاج؟
- (مجموعة تطوير برامج التشغيل)، والمعروفة الآن أيضًا باسم WDK 7 (Windows (مجموعة أدوات السائق).
- آلة افتراضية (مثلاً) Windows 7 × 64)
- ليس ضروريًا، لكن ملفات الرأس التي يمكن تنزيلها قد تساعد
ماذا يوجد في الكود؟
دعونا نتدرب قليلًا، وعلى سبيل المثال، نكتب تطبيقًا صغيرًا:
- يعرض رسالة على الشاشة
- يخصص بعض الذاكرة
- ينتظر إدخال لوحة المفاتيح
- يحرر الذاكرة المستخدمة
في التطبيقات الأصلية، نقطة الإدخال ليست main أو 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 في وقت التشغيل). وبطبيعة الحال، يمكنك فقط استخدام المكدس. ولكن إذا كنا بحاجة إلى تخصيص الذاكرة ديناميكيًا، فسيتعين علينا القيام بذلك على الكومة (أي الكومة). لذلك دعونا ننشئ كومة لأنفسنا ونأخذ الذاكرة منها عندما نحتاج إليها.
الوظيفة مناسبة لهذه المهمة . بعد ذلك، باستخدام 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 بجهاز افتراضي لتصحيح أخطاء kernel. يمكن العثور على تعليمات حول كيفية القيام بذلك أو مجرد استخدام .
التجميع والتجميع
أسهل طريقة لإنشاء تطبيق أصلي هي استخدام (مجموعة تطوير السائق). نحن بحاجة إلى الإصدار السابع القديم، حيث أن الإصدارات الأحدث لها نهج مختلف قليلاً وتعمل بشكل وثيق مع Visual Studio. إذا استخدمنا DDK، فإن مشروعنا يحتاج فقط إلى 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 = 1سيكون ملف Makefile الخاص بك هو نفسه تمامًا، ولكن دعونا نلقي نظرة على المصادر بمزيد من التفاصيل. يحدد هذا الملف مصادر برنامجك (ملفات .c)، وخيارات البناء، والمعلمات الأخرى.
- TARGETNAME – اسم الملف القابل للتنفيذ الذي ينبغي إنتاجه في النهاية.
- TARGETTYPE - نوع الملف القابل للتنفيذ، يمكن أن يكون برنامج تشغيل (.sys)، ثم يجب أن تكون قيمة الحقل هي DRIVER، وإذا كانت مكتبة (.lib)، فإن القيمة هي LIBRARY. في حالتنا، نحتاج إلى ملف قابل للتنفيذ (.exe)، لذلك قمنا بتعيين القيمة على PROGRAM.
- UMTYPE – القيم المحتملة لهذا الحقل: وحدة التحكم لتطبيق وحدة التحكم، ونوافذ العمل في وضع الإطارات. لكننا بحاجة إلى تحديد nt للحصول على تطبيق أصلي.
- BUFFER_OVERFLOW_CHECKS – التحقق من تجاوز سعة المخزن المؤقت في المكدس، لسوء الحظ ليس حالتنا، نقوم بإيقاف تشغيله.
- MINWIN_SDK_LIB_PATH - تشير هذه القيمة إلى متغير SDK_LIB_PATH، لا تقلق من عدم الإعلان عن متغير النظام هذا، عندما نقوم بتشغيل الإصدار المحدد من DDK، سيتم الإعلان عن هذا المتغير وسيشير إلى المكتبات الضرورية.
- المصادر – قائمة مصادر لبرنامجك.
- يتضمن - ملفات الرأس المطلوبة للتجميع. تشير عادةً هنا إلى المسار إلى الملفات التي تأتي مع DDK، ولكن يمكنك بالإضافة إلى ذلك تحديد أي ملفات أخرى.
- TARGETLIBS – قائمة المكتبات التي تحتاج إلى الارتباط.
- USE_NTDLL هو حقل مطلوب يجب ضبطه على 1 لأسباب واضحة.
- USER_C_FLAGS – أي إشارات يمكنك استخدامها في توجيهات المعالج المسبق عند إعداد كود التطبيق.
لذا، للإنشاء، نحتاج إلى تشغيل الإصدار x86 (أو x64) الذي تم التحقق منه، وتغيير دليل العمل إلى مجلد المشروع وتشغيل أمر البناء. تظهر النتيجة في لقطة الشاشة أن لدينا ملفًا واحدًا قابلاً للتنفيذ.

لا يمكن إطلاق هذا الملف بهذه السهولة، يلعن النظام ويدفعنا للتفكير في سلوكه مع ظهور الخطأ التالي:

كيفية تشغيل التطبيق الأصلي؟
عند بدء التشغيل التلقائي، يتم تحديد تسلسل بدء تشغيل البرامج من خلال قيمة مفتاح التسجيل:
HKLMSystemCurrentControlSetControlSession ManagerBootExecuteيقوم مدير الجلسة بتنفيذ البرامج من هذه القائمة واحدًا تلو الآخر. يبحث مدير الجلسة عن الملفات القابلة للتنفيذ بنفسه في دليل 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
