טעינה מאובטחת ותאימות¶
נניח שהתוכנית שלנו טוענת engine.dll בלי נתיב. במחשב הפיתוח היא מוצאת את הקובץ הנכון. במחשב אחר יש קובץ בעל אותו שם בתיקייה נוספת, ופתאום התוכנית מפעילה קוד אחר. הבעיה אינה רק תפעולית: חיפוש קוד הוא החלטה לגבי מקור אמון.
נתיב מוחלט מצמצם עמימות לגבי הספרייה הראשית, אבל הוא אינו כל הסיפור. הספרייה יכולה לייבא DLLים נוספים בשמות בלבד. בנוסף קיימים מנגנוני טעינה כמו API sets, רכיבים שכבר נטענו ו־side-by-side. לכן צריך לתכנן מדיניות ולבדוק את התוצאה בפועל.
מגדירים אזור חיפוש צר¶
הקוד הבא טוען את ספריית הניסוי מהשיעור 3.5. הוא מקבל נתיב מוחלט, ומאפשר פתרון תלויות מתוך תיקיית הספרייה ומתיקיית המערכת. שומרים אותו כ־load_policy.cpp.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstdio>
#include <filesystem>
#include <vector>
int wmain(int argc, wchar_t** argv) {
if (argc != 2 || !std::filesystem::path(argv[1]).is_absolute())
return 1;
HMODULE module = LoadLibraryExW(argv[1], nullptr,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!module) {
const DWORD error = GetLastError();
std::fprintf(stderr, "load error=%lu\n", error);
return 2;
}
std::vector<wchar_t> path(32768);
const DWORD length = GetModuleFileNameW(
module, path.data(), static_cast<DWORD>(path.size()));
if (length == 0 || length >= path.size()) {
const DWORD error = GetLastError();
FreeLibrary(module);
std::fprintf(stderr, "path error=%lu\n", error);
return 3;
}
std::wprintf(L"loaded: %ls\n", path.data());
FreeLibrary(module);
}
הדוגמה מיועדת לבסיס Windows 11 של הקורס. בקוד שמכוון למערכות ישנות צריך לבדוק את זמינות דגלי החיפוש והעדכונים הרלוונטיים, ולא להניח שהגדרה ב־header מוכיחה תמיכה בזמן ריצה.
הדפסת הנתיב נותנת ראיה לגבי ה־module שנטען. היא אינה אימות חתימה ואינה מבטיחה שתיקיית הקובץ מוגנת מכתיבה בידי גורם לא רצוי. גם נתיב מוחלט יכול להצביע לקובץ שהוחלף. בפרק האבטחה נחבר את המדיניות להרשאות ולפריסה.
בודקים תלות שנייה¶
צרו DLL קטן נוסף בשם helper.dll וגרמו ל־engine.dll לייבא פונקציה ממנו. שמרו את שניהם בתיקיית ניסוי אחת. טעינה עם LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR מאפשרת לחפש את התלות גם בתיקיית ה־DLL הראשי.
העבירו את helper לתיקיית עבודה אחרת ושמרו את engine במקומו. הטעינה צריכה להיבחן לפי המדיניות שבחרנו, בלי להניח ש־current directory נכללת. הניסוי משתמש רק בקבצים שיצרנו, ואין צורך לשנות תיקיות מערכת.
פעולות כמו SetDefaultDllDirectories יכולות לקבוע מדיניות לתהליך, ו־AddDllDirectory מוסיפה תיקייה לשימוש מתאים. כל שינוי כזה הוא state משותף בתהליך, ולכן ספרייה אינה צריכה לשנות אותו בהפתעה עבור כל לקוחותיה. החלטת מדיניות שייכת בדרך כלל למארח.
יכולת בזמן בנייה אינה יכולת בזמן ריצה¶
הגדרת _WIN32_WINNT משפיעה על הצהרות ותכונות שה־SDK מציג בזמן קומפילציה. היא אינה מתקינה API במערכת היעד. אם קישרנו ישירות לפונקציה שאינה קיימת ביעד, התוכנית יכולה להיכשל לפני שהקוד שלנו בודק גרסה.
ליכולת שאינה חובה אפשר לפתור כתובת בזמן ריצה ולבחור fallback. הקריטריון הראשון הוא אם היכולת הנדרשת זמינה, ולא אם מספר גרסה נראה גדול מספיק. עם זאת, עצם קיום export אינו מוכיח שכל התנהגות חדשה שלו נתמכת; צריך לקרוא גם את החוזה של ה־flags והפרמטרים.
אפשר לתרגל את הרעיון באמצעות שתי גרסאות של calc: אחת שמייצאת פעולה נוספת ואחת שלא. הלקוח בודק את הסמל החדש, ואם אינו נמצא משתמש במימוש ישן בעל משמעות מוגדרת. כך בודקים fallback באופן יזום גם במחשב חדש.
מצהירים על סביבת היישום¶
ה־manifest מתאר חלק מהציפיות של היישום, כולל תאימות והרשאות מבוקשות. הקובץ הבא מתאים לניסוי desktop רגיל שפועל בהרשאות המשתמש.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="AmitTech.LoadExperiment"
version="1.0.0.0" type="win32"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
ה־GUID המוצג משמש להצהרת התאימות המתועדת ל־Windows 10 ול־Windows 11. הוא אינו מבטיח שהתוכנית באמת נבדקה בכל build. בונים עם manifest משולב ובודקים את התוצר, ולא רק את הקובץ שנותר ליד המקור.
cl /nologo /std:c++20 /EHsc /W4 load_policy.cpp /Feload_policy.exe /link /MANIFEST:EMBED /MANIFESTINPUT:app.manifest
ממשקי בדיקת גרסה מסוימים מושפעים מהצהרות התאימות. לכן תוצאה מפתיעה אינה סיבה לפרש bytes פנימיים של PEB כדי לעקוף את החוזה. צריך להחליט מה באמת נדרש: תיעוד גרסה לצורכי תחקיר, או בדיקת יכולת לצורך התנהגות.
מגדירים fallback שאפשר להסביר¶
חלופה אינה חייבת לספק אותה איכות שירות. אם פעולה חדשה מספקת דיוק זמן גבוה יותר, החלופה צריכה לציין מגבלת דיוק. אם פעולה חדשה נדרשת לבטיחות הנתונים, ייתכן שהחלופה הנכונה היא סירוב ברור להפעלת היכולת.
בדיקת התאימות כוללת גם מסלול שבו היכולת חסרה. בלי בדיקה כזו, fallback יכול להתיישן בזמן שכל הבדיקות הרגילות נשארות ירוקות.
לקריאה נוספת: סדר חיפוש DLLים והצהרת תאימות במניפסט.