לדלג לתוכן

8.3 - בידוד וגבולות אמון - פתרון

פתרון - מה הגבול באמת מונע

התרגיל הראשון

הוסיפו בתוך main את הבדיקה המפורשת הבאה:

PROCESS_MITIGATION_CONTROL_FLOW_GUARD_POLICY cfg{};
if (!GetProcessMitigationPolicy(GetCurrentProcess(),
        ProcessControlFlowGuardPolicy, &cfg, sizeof(cfg))) {
    std::printf("CFG query failed: %lu\n", GetLastError());
} else {
    std::printf("CFG enabled=%u strict=%u\n",
                static_cast<unsigned>(cfg.EnableControlFlowGuard),
                static_cast<unsigned>(cfg.StrictMode));
}

הקוד מפענח שני שדות מהמבנה במקום להעניק משמעות למספר ללא שם. הדוח צריך להבחין בין התשובה של השאילתה לבין תוכן ה־load configuration של הבינארי. שאילתת התהליך מתייחסת לתהליך שהופעל בסביבה המסוימת; בדיקת הקובץ מתייחסת לתוצר הקישור.

הבנייה בלי דגל מפורש אינה בהכרח בנייה בלי הגנה. פרויקט יכול להגדיר ברירת מחדל, כלי הבנייה יכולים להוסיף flags ומדיניות מערכת יכולה להשפיע. לכן שומרים את שורת הפקודה המלאה ובודקים את התוצר, במקום להמציא פלט צפוי אחיד לכל מחשב.

התרגיל השני

הפתרון הבא הוא תוכנית מלאה שמרכזת את הניקוי בעטיפה קטנה:

#include <windows.h>
#include <cstdio>
#include <stdexcept>
#include <system_error>

struct file_owner {
    HANDLE value = INVALID_HANDLE_VALUE;
    file_owner() = default;
    file_owner(const file_owner&) = delete;
    ~file_owner() {
        if (value != INVALID_HANDLE_VALUE) CloseHandle(value);
    }
};

void read_and_print(HANDLE file, const char* label) {
    char data[128]{};
    DWORD count = 0;
    if (!ReadFile(file, data, sizeof(data) - 1, &count, nullptr))
        throw std::system_error(static_cast<int>(GetLastError()),
                                std::system_category(), "ReadFile");
    data[count] = '\0';
    std::printf("%s: %s\n", label, data);
}

void open_input(file_owner& owner) {
    owner.value = CreateFileW(L"identity-lab\\input.txt", GENERIC_READ,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (owner.value == INVALID_HANDLE_VALUE)
        throw std::system_error(static_cast<int>(GetLastError()),
                                std::system_category(), "CreateFileW");
}

int main() {
    try {
        file_owner original;
        open_input(original);
        std::puts("Replace the file, then press Enter.");
        std::getchar();
        read_and_print(original.value, "original handle");
        file_owner current;
        open_input(current);
        read_and_print(current.value, "new handle");
    } catch (const std::exception& error) {
        std::fprintf(stderr, "%s\n", error.what());
        return 1;
    }
}

הבנייה דורשת טיפול בחריגות:

cl /nologo /std:c++20 /EHsc /W4 opened_twice.cpp
opened_twice.exe

העטיפה מחזיקה כל handle עד סוף ה־scope שלו. אם הפתיחה השנייה נכשלת, האובייקט הראשון עדיין נהרס בזמן היציאה עם החריגה. הפונקציה open_input מיועדת כאן לבעלים ריק בלבד; בממשק ספרייה היינו אוכפים גם את תנאי הקדם הזה או מחזירים בעלים חדש לפי ערך.

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

התרגיל השלישי

הפקודה הכללית הופכת את השירות החזק לשליח שמבצע את הבחירה של המפענח. אם המפענח יכול לבחור כל נתיב וכל מסכת הרשאות, הבידוד המקומי שלו אינו מונע ממנו לבקש מה־broker את הגישה החסרה.

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

גם ה־broker חייב לבדוק את התוצאה. למשל, מכפלת רוחב, גובה ומספר bytes לפיקסל צריכה להיבדק בלי גלישה לפני הקצאה. רכיב מבודד אינו מקור מהימן רק מפני שאנחנו כתבנו אותו.

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

מגבלת זמן צריכה להסתיים בניקוי העבודה וה־handles שנמסרו לה. אסור להחזיר את ה־buffer למאגר לשימוש חוזר כל עוד רכיב אחר עדיין יכול לכתוב אליו. כך הבידוד והבעלות בזיכרון מתחברים לאותו חוזה סיום.