מדיניות אבטחה בגבול קריאת מערכת¶
פעולה אסורה ומידע שאינו זמין למסנן¶
נניח ששירות צריך לקרוא מסוקטים שכבר נפתחו, אבל אינו צריך ליצור תהליכים חדשים. אפשר להקטין את מרחב הפעולות שלו באמצעות מסנן קריאות מערכת. מנגנון seccomp מקבל החלטה בגבול הכניסה לקריאה, לפני שהקריאה מבצעת את העבודה הרגילה שלה. הוא יכול לבדוק את מספר הקריאה, ארכיטקטורת הקריאה וערכי הארגומנטים המספריים שנמסרו לה.
המסנן הרגיל אינו קורא באופן חופשי את התוכן שאליו מצביע ארגומנט. כתובת של מחרוזת נתיב אינה המחרוזת עצמה, ושני נתיבים שונים יכולים להוביל לאותו קובץ. לכן מסנן שמשווה כתובות של מחרוזות אינו מדיניות הרשאות קבצים תקפה. מדיניות על זהות קובץ, תווית אבטחה או תצוגת מערכת קבצים דורשת מנגנון בעל המידע המתאים.
נבנה קודם מסנן קטן שמסרב לקריאה אחת בלבד. כך נוכל להבין את חוזה ההתקנה ואת תוצאת הסירוב בלי לנחש רשימת קריאות שלמה שנדרשת לספריית הריצה. הדוגמה היא ניסוי מנגנון, ולא מסנן שמספיק להגנת שירות מול קוד בלתי מהימן.
התקנה בלתי הפיכה בתוך ילד¶
נשתמש בפרויקט עם התלות libc = "0.2". הקוד הבא מיועד ללינוקס עם ארכיטקטורת x86_64 וממשק הקריאות הרגיל שלה. נריץ אותו כתוכנית נפרדת, משום שהורדת המגבלות אינה חלק מהחוזה. הוראות המסנן הן הוראות של מכונת הסינון הקלאסית, ולא הוראות המכונה של המעבד.
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
compile_error!("this lab requires Linux x86_64");
use std::io;
fn statement(code: u16, k: u32) -> libc::sock_filter {
libc::sock_filter { code, jt: 0, jf: 0, k }
}
fn jump(code: u16, k: u32, jt: u8, jf: u8) -> libc::sock_filter {
libc::sock_filter { code, jt, jf, k }
}
fn install() -> io::Result<()> {
const LD_WORD_ABS: u16 = 0x20;
const JEQ: u16 = 0x15;
const JSET: u16 = 0x45;
const RET: u16 = 0x06;
const AUDIT_ARCH_X86_64: u32 = 0xc000003e;
const KILL_PROCESS: u32 = 0x80000000;
const ALLOW: u32 = 0x7fff0000;
const ERRNO: u32 = 0x00050000;
let mut filters = [
statement(LD_WORD_ABS, 4),
jump(JEQ, AUDIT_ARCH_X86_64, 1, 0),
statement(RET, KILL_PROCESS),
statement(LD_WORD_ABS, 0),
jump(JSET, 0x40000000, 0, 1),
statement(RET, KILL_PROCESS),
jump(JEQ, libc::SYS_getppid as u32, 0, 1),
statement(RET, ERRNO | libc::EPERM as u32),
statement(RET, ALLOW),
];
let program = libc::sock_fprog {
len: filters.len() as u16,
filter: filters.as_mut_ptr(),
};
if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } == -1 {
return Err(io::Error::last_os_error());
}
let result = unsafe { libc::syscall(libc::SYS_seccomp,
1_u32, 1_u32, &program as *const libc::sock_fprog) };
if result == -1 { return Err(io::Error::last_os_error()); }
if result != 0 {
return Err(io::Error::other(format!("TSYNC failed at thread {result}")));
}
Ok(())
}
fn main() -> io::Result<()> {
let before = unsafe { libc::syscall(libc::SYS_getppid) };
println!("before={before}");
install()?;
let after = unsafe { libc::syscall(libc::SYS_getppid) };
let error = io::Error::last_os_error();
assert_eq!(after, -1);
assert_eq!(error.raw_os_error(), Some(libc::EPERM));
println!("blocked={error}");
println!("allowed getpid={}", unsafe { libc::syscall(libc::SYS_getpid) });
Ok(())
}
המסנן טוען תחילה את שדה הארכיטקטורה מהמבנה שהקרנל מספק. בדיקת מספר הקריאה בלי בדיקת הארכיטקטורה יכולה לייחס משמעות שגויה לאותו מספר. לאחר מכן הוא דוחה מספרים הנושאים את הסיבית של ממשק x32, משום שהמעבדה אינה מגדירה מדיניות לממשק הזה. בחירת ממשקי הקריאה הנתמכים היא חלק מחוזה התוכנית ולא פרט קוסמטי.
הקפיצות נספרות מההוראה הבאה. כאשר הארכיטקטורה נכונה מדלגים על הוראת ההרג. כאשר סיבית הממשק הנוסף אינה דלוקה מדלגים על הוראת ההרג השנייה. כאשר מספר הקריאה הוא getppid מחזירים שגיאת הרשאה, ובכל מקרה אחר מגיעים להוראת האישור. ציירו את כל המסלולים לפני שאתם משנים אפילו ערך קפיצה יחיד.
מדוע נדרש דגל הרשאות¶
הדגל no_new_privs מבטיח שהפעלה עתידית של תוכנית דרך exec לא תעניק לתהליך הרשאות חדשות בדרך הרגילה של קובצי הרצה מיוחסים. הוא עובר בירושה ואינו ניתן לאיפוס על ידי התהליך. הדגל מאפשר לתהליך בלתי מיוחס להתקין מסנן בלי לנצל את המסנן לשינוי מפתיע של התנהגות תוכנית מיוחסת שתופעל אחריו.
הדגל אינו מוחק הרשאות קיימות, אינו סוגר מתארים ואינו מונע כל שינוי סמכות שאפשר לבצע בלי exec. קבלת מתאר דרך סוקט עדיין יכולה להעניק גישה לאובייקט. לכן נבחן את ממשק העברת המתארים מפרק הרשת כחלק ממדיניות השירות, גם אם הדגל כבר מוגדר.
הקריאה בדוגמה משתמשת בפעולת התקנת מסנן ובדגל הסנכרון TSYNC, שערכיהם בממשק הזה הם שניהם אחד. התוצאה החיובית החריגה של סנכרון שנכשל יכולה לזהות תהליכון בעייתי. לכן הקוד בודק גם אותה ואינו מסתפק בהשוואה למינוס אחד. בדוגמה שלנו קיים תהליכון יחיד, אך אותו טיפול בתוצאה חשוב במפעיל שירותים מרובה תהליכונים.
תהליכונים, שלבים ושגיאות מפתיעות¶
התקנת מסנן בתהליכון יחיד אינה בהכרח מדיניות אחידה עבור תהליכונים קיימים אחרים. הפתרון יכול להיות התקנה בשלב חד־תהליכי לפני יצירתם, או שימוש מוצלח בסנכרון המסנן. עבודה שכבר התחילה אינה חוזרת אחורה בזמן בגלל התקנה מאוחרת. נגדיר בבירור את הרגע שממנו המדיניות חייבת להיות תקפה.
ספריית ריצה יכולה לבצע קריאות שלא כתבנו במפורש, למשל הקצאה, טיפול באותות או יצירת תהליכון. רשימת אישור שנבנתה מהרצה קצרה אחת יכולה לפספס נתיב שגיאה נדיר. נבדוק גם כישלון חיבור, הקצאה גדולה, החלפת תצורה וסיום. כאשר קריאה אסורה מחזירה שגיאה במקום להרוג, יש לוודא שהתוכנית אינה מנסה אותה שוב בלולאה צפופה.
אפשר להוסיף מסננים, אך צירוף מסנן מאוחר אינו דרך לבטל הגבלה קודמת. בדיקת שירות צריכה לכלול גם את המדיניות שמנהל ההפעלה כבר התקין. אם התקנת המסנן נכשלת, אין להדפיס הודעת אזהרה ולהמשיך כאילו הגבול קיים. השירות חייב להחליט במפורש האם תכונת האבטחה נדרשת להפעלה.
מדיניות עשירה יותר¶
מסגרת LSM מאפשרת לקרנל להפעיל מנגנוני אבטחה בנקודות בדיקה נוספות. מנגנון SELinux משתמש בתוויות ובמדיניות הקושרת סוגי אובייקטים ותחומי תהליכים. מנגנון AppArmor משתמש בין השאר בפרופילים ובהקשרים הקשורים לנתיבים. אין להחליף ביניהם באמצעות שינוי שם בקובץ תצורה, משום שמודל המדיניות והאופן שבו פותרים שמות שונים.
המנגנון Landlock מאפשר לתהליך להחיל על עצמו הגבלות נתמכות בלי להיות מנהל המערכת. התמיכה המדויקת תלויה בגרסת הממשק הזמינה ובתצורת הקרנל. תוכנית חייבת לברר את היכולות הנתמכות ולהבחין בין הגנה נדרשת שאינה זמינה לבין הרחבה אופציונלית. השמטה שקטה של כלל שאינו נתמך יכולה לשנות את החוזה שהמשתמש חשב שקיבל.
נבדוק את המנגנונים הפעילים במערכת המעבדה, אם ממשק הבדיקה נחשף לנו, בעזרת cat /sys/kernel/security/lsm. היעדר הקובץ אינו מוכיח שכל המנגנונים כבויים, משום שגם עיגון ותצוגה משפיעים על הגישה. נשלב בדיקת תצורה עם ניסיון גישה מותר וניסיון גישה אסור בתהליך שבאמת מריץ את המדיניות.
החוזים מפורטים בתיעוד המסנן, בתיעוד דגל ההרשאות ובתיעוד ההגבלות העצמיות. המטרה של הבדיקה היא להוכיח התנהגות מסוימת תחת המדיניות, ולא רק להראות שקריאת התקנה חזרה בהצלחה.