לדלג לתוכן

סביבת קרנל וחוזה ביצוע

תוכנית משתמש שפוגעת בזיכרון שלה יכולה להתרסק ולהשאיר את שאר המערכת פועלת. דרייבר פגום יכול לפגוע בזיכרון ובמצב שמשותפים למערכת כולה, ולכן נצטרך סביבת ניסוי שמאפשרת לעצור, לבדוק ולחזור לנקודת התחלה ידועה. המטרה אינה רק להצליח לטעון קובץ sys, אלא לדעת באיזה context הקוד רץ ואיזה חוזה מותר לו להניח.

נבחר בסיס מעבדה קבוע: Windows 11 x64 ב־VM נפרדת, Visual Studio 2022 ו־WDK בגרסה 26100.6584 עם SDK תואם. זוהי נעילת סביבת לימוד, לא טענה שזאת הגרסה החדשה ביותר. נבדוק בתיעוד של Microsoft את שילוב ה־SDK וה־WDK ואת חבילות הכלים הנדרשות, ונשמור בדוח את גרסאות מערכת היעד וה־debugger. מכונת הפיתוח וה־VM אינן אותה מערכת שעליה נריץ ניסוי קריסה.

ה־kernel debugger צריך להיות מחובר לפני טעינת הדרייבר הראשון. נשתמש ב־KDNET לפי סביבת הווירטואליזציה והתיעוד הרשמי, עם מפתח וכתובת שמתאימים לרשת המעבדה. נוודא שה־host יכול לעצור את היעד ושיש snapshot של VM שאפשר לשחזר. הפעלת test signing, כאשר היא נדרשת, תתבצע רק ב־VM ובהתאם למדיניות Secure Boot והחתימה של סביבת המעבדה; אין צורך לשנות את הגנת מכונת העבודה כדי לקרוא ולבנות את הקוד.

ניצור פרויקט WDM ריק ב־WDK ונוסיף את הקובץ driver.c הבא. זהו דרייבר מינימלי מלא שאינו יוצר התקן ואינו מקבל בקשות משתמש. הוא מדגים נקודות כניסה ויציאה בלבד; את התקן ה־I/O נבנה בהמשך.

#include <ntddk.h>

DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD LabUnload;

_Use_decl_annotations_
VOID LabUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    PAGED_CODE();
    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "AmitLab unload\n");
}

_Use_decl_annotations_
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    PAGED_CODE();
    DriverObject->DriverUnload = LabUnload;
    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "AmitLab loaded\n");
    return STATUS_SUCCESS;
}

ה־DriverEntry נקרא בעת אתחול הדרייבר ומקבל DRIVER_OBJECT שהמערכת הקימה. ה־RegistryPath אינו string שמסתיים בהכרח באפס; זהו UNICODE_STRING עם אורך וחוזה משלו. בדוגמה איננו שומרים אותו. ה־Unload נקרא במסלול פריקה מתאים, והוא חייב לשחרר כל משאב שהדרייבר מחזיק ולוודא שאין עבודה שתיכנס לקוד לאחר הפריקה. כרגע אין משאבים כאלה, ולכן הבסיס פשוט בכוונה.

ה־PAGED_CODE הוא assertion של הנחת IRQL בתצורות המתאימות, ולא פקודה שהופכת כל גישה לזיכרון לבטוחה. ה־DbgPrintEx הוא כלי מעבדה, והמסנן של ה־debugger עשוי להסתיר את הפלט. העדר הודעה אינו מוכיח שהפונקציה לא רצה; נציב breakpoint על הסמל ונוודא שסמלי ה־PDB תואמים ל־binary שנבנה.

ב־WinDbg נתחיל ב־lm וב־lmvm עבור המודול, נבדוק symbols ונציב breakpoint על DriverEntry או על פונקציה שלנו לפי מצב הטעינה. נבחן את ה־stack ואת current process/thread. אין להניח שהדרייבר רץ תמיד בהקשר של האפליקציה ששלחה בקשה; callbacks שונים יכולים להופיע בהקשרים שונים, ובפרט עבודה נדחית אינה שומרת אוטומטית את כתובות המשתמש המקוריות תקפות.

ה־executive כולל מנהלים כמו object, I/O ו־memory, וה־HAL מסתיר חלק מן הפרטים התלויים בחומרה. ה־driver stack מחבר רכיבים שמקבלים ומעבירים בקשות, ולא כל דרייבר מדבר ישירות עם התקן. נשתמש בתת־קבוצה מפורשת של C או C++ שמתאימה ל־kernel: אין להניח זמינות של CRT משתמש, STL רגיל, exceptions או RTTI רק מפני שהקומפיילר מכיר את התחביר. כל תלות צריכה להתאים לחוזה ה־WDK ולמודל הבנייה.

התיעוד על סביבת WDK ועל חיבור KDNET הוא מקור ההקמה. הצלחת build ב־C++ רגיל או syntax check אינה אימות WDK או הוכחה שהדרייבר נטען; בדוח נפריד את השלבים האלה.