8.2 - הקשר אבטחה והגבהה - פתרון
פתרון - מחזור החיים של זהות¶
התרגיל הראשון¶
הפונקציה הבאה משתמשת במחלקה ובהדפסה מההרצאה. מוסיפים אותה לפני main:
void request(int mode) {
self_impersonation scope;
print_context("request");
if (mode == 1) return;
if (mode == 2) throw std::runtime_error("request error");
std::puts("completed");
}
החליפו את גוף ה־try החיצוני ב־main בלולאה הזאת:
for (int mode = 0; mode != 3; ++mode) {
try {
request(mode);
} catch (const std::runtime_error& error) {
std::printf("caught: %s\n", error.what());
}
print_context("after request");
}
בכל קריאה ה־guard נבנה אחרי שבדק שאין impersonation קיים. בהצלחה ובחזרה מוקדמת הוא נהרס ביציאה מהפונקציה. בחריגה הוא נהרס במהלך stack unwinding לפני שהטיפול בחריגה בלולאה ממשיך. ההקשר שמודפס אחרי הבקשה אמור להיות ההקשר של התהליך בכל שלושת המקרים.
הבדיקה חשובה דווקא מפני שהטעות המקורית אינה מופיעה במסלול המצליח. ניקוי שנכתב כשורה רגילה בסוף הפונקציה היה מדולג ב־return ובחריגה.
התרגיל השני¶
הקוד שממחיש את החוזה הוא:
{
self_impersonation outer;
print_context("outer before");
try {
self_impersonation inner;
} catch (const std::logic_error&) {
print_context("outer after rejected nesting");
}
}
print_context("outside");
הבנייה הפנימית מזהה token קיים וזורקת לפני הקריאה ל־ImpersonateSelf. לכן אין destructor של אובייקט פנימי שהושלם, ואין ניסיון להסיר את ההקשר החיצוני. ההדפסה בתוך הטיפול בחריגה עדיין אמורה להראות impersonation. ביציאה מה־scope החיצוני, האובייקט החיצוני מבצע את הניקוי.
זאת אינה עטיפה שמממשת קינון, אלא עטיפה שמסרבת לו בצורה מפורשת. כדי לממש קינון באמת צריך לשמור את ה־token הקודם, להחזיק אותו בחיים ולשחזר אותו במקום להסיר תמיד את זהות התהליכון. צריך להגדיר גם מי רשאי לשנות את ההקשר בזמן שהעטיפה חיה.
התרגיל השלישי¶
הקוד הבא הוא תוכנית מלאה:
#include <windows.h>
#include <cstdio>
int main() {
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
std::printf("open error=%lu\n", GetLastError());
return 1;
}
TOKEN_ELEVATION_TYPE type{};
TOKEN_ELEVATION elevation{};
DWORD bytes = 0;
if (!GetTokenInformation(token, TokenElevationType,
&type, sizeof(type), &bytes)) {
DWORD error = GetLastError();
CloseHandle(token);
std::printf("type error=%lu\n", error);
return 1;
}
if (!GetTokenInformation(token, TokenElevation,
&elevation, sizeof(elevation), &bytes)) {
DWORD error = GetLastError();
CloseHandle(token);
std::printf("elevation error=%lu\n", error);
return 1;
}
CloseHandle(token);
std::printf("type=%u elevated=%lu\n",
static_cast<unsigned>(type), elevation.TokenIsElevated);
}
הבנייה מתבצעת כך:
הטיפוס יכול להיות default, full או limited לפי ההקשר. המספרים שמודפסים הם ערכי ה־enum; בדוח כדאי לתרגם אותם לשמות מתוך ה־SDK ולא לזכור מספר ללא משמעות. הדגל השני אומר אם ה־token מוגבה, אך אינו אומר שהרשאה לכל אובייקט בעולם מאושרת.
גם תהליך מוגבה יכול לבקש פעולה שאינה מותרת לו. צריך לבדוק את האובייקט, ההרשאות המבוקשות ומנגנוני המדיניות הרלוונטיים. לעומת זאת, תהליך שאינו מוגבה יכול לבצע פעולות רבות על משאבים שנמסרו לו כדין. השדה אינו מחליף את בדיקת הגישה.
התרגיל הרביעי¶
בעלות על handle ל־token היא בעלות על משאב, ולא שינוי אוטומטי של הקשר ה־thread שמריץ lambda. התהליכון שיטפל בתור עשוי להיות thread אחר, וגם הקוד שהכניס לתור עשוי כבר לצאת מ־scope ההתחזות.
בתכנון הראשון פותחים את הקובץ בהקשר הלקוח ובהרשאות הדרושות, ואז מעבירים לתור את ה־handle שכבר נפתח. הפתיחה היא נקודת בדיקת ההרשאות, וה־handle קושר את העבודה לאובייקט המסוים. התור חייב להחזיק אותו עד סיום הפעולה והטיפול בביטול. מדיניות המוצר עדיין צריכה להחליט מה קורה אם ההרשאות משתנות בזמן שהעבודה ממתינה.
בתכנון השני מעבירים בעלות על token מתאים ועל תיאור פעולה מצומצם. תהליכון העבודה מפעיל את ההקשר, מבצע את הפעולה ומחזיר את זהותו לפני שהוא יוצא למשימה אחרת. אין להעביר lambda שרירותית שיכולה לפתוח כל משאב; גבול ההרשאה צריך להיות חלק מהממשק. כשל בהפעלת הזהות עוצר את הפעולה במקום לבצע אותה בטעות בשם השירות.
בשני התכנונים נדרש להסביר את משך החיים גם במסלול שבו הבקשה בוטלה לפני שהעבודה התחילה. ביטול של בקשה אינו סיבה לדלג על סגירת ה־handle או על שחרור ה־token שבבעלות התור.