לדלג לתוכן

10.2 - יכולות והפרדת הרשאות - הרצאה

סמכות קטנה יותר עדיין דורשת חוזה

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

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

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

חמש קבוצות שאינן אותו דבר

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

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

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

החישוב בעת החלפת תוכנית

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

new_ambient = privileged_file ? 0 : old_ambient
new_permitted = (old_inheritable & file_inheritable)
              | (file_permitted & old_bounding)
              | new_ambient
new_effective = file_effective_bit ? new_permitted : new_ambient

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

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

ניסוי של רכישה ואז ויתור

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

הקוד משתמש בממשק הגולמי של גרסת מבני היכולות השלישית בלינוקס ובתלות libc = "0.2". הבחירה מפורשת כדי שנוכל לבדוק את גודל המבנים ואת מספר המילים שנמסרות לקרנל.

use std::io;
use std::net::TcpListener;

#[repr(C)]
struct Header { version: u32, pid: i32 }

#[repr(C)]
#[derive(Clone, Copy)]
struct Data { effective: u32, permitted: u32, inheritable: u32 }

fn show() -> io::Result<()> {
    for line in std::fs::read_to_string("/proc/self/status")?.lines() {
        if line.starts_with("Cap") || line.starts_with("Uid:") { println!("{line}"); }
    }
    Ok(())
}

fn drop_sets() -> io::Result<()> {
    let header = Header { version: 0x2008_0522, pid: 0 };
    let data = [Data { effective: 0, permitted: 0, inheritable: 0 }; 2];
    let result = unsafe { libc::syscall(libc::SYS_capset, &header as *const Header, data.as_ptr()) };
    if result < 0 { return Err(io::Error::last_os_error()); }
    Ok(())
}

fn main() -> io::Result<()> {
    if unsafe { libc::geteuid() } == 0 {
        return Err(io::Error::other("run this experiment as a non-root account"));
    }
    show()?;
    let listener = TcpListener::bind(("127.0.0.1", 81))?;
    drop_sets()?;
    show()?;
    assert_eq!(listener.local_addr()?.port(), 81);
    println!("listener still owned after dropping capability sets");
    Ok(())
}

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

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

sysctl net.ipv4.ip_unprivileged_port_start
cargo build
cp target/debug/capability_probe ./capability-probe-lab
sudo setcap cap_net_bind_service=ep ./capability-probe-lab
getcap ./capability-probe-lab
./capability-probe-lab
sudo setcap -r ./capability-probe-lab
getcap ./capability-probe-lab

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

מה הוויתור הוכיח

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

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

תהליך עזר שאינו שליח מבולבל

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

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

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

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