10.1 - זהויות ובדיקת גישה בלינוקס - הרצאה
הרשאה נבדקת עבור פעולה מסוימת¶
עד עכשיו השתמשנו במשאבים של המשתמש שמריץ את התוכנית. שירות מערכת צריך לעיתים להתחיל עם סמכות מסוימת, לפתוח משאבים ואז להמשיך עם סמכות מצומצמת יותר. כדי לעשות זאת נכון צריך להבין איזו זהות נבדקת בכל פעולה, ואילו הרשאות כבר הפכו למתארים פתוחים שאפשר להמשיך להשתמש בהם.
הביטוי בעל הקובץ אינו תיאור מלא של ההחלטה. הקרנל בודק את הזהויות הרלוונטיות של התהליך, את הקבוצות, את הרשאות כל רכיבי הנתיב ואת מדיניות הגישה הנוספת שמופעלת במערכת. גם סוג הפעולה משנה: פתיחת קובץ לקריאה, מחיקת שמו ויצירת קובץ חדש באותה תיקייה אינן אותה בדיקת הרשאה.
נבנה תחילה מודל של בקרת הגישה הרגילה, ואחר כך נוסיף יכולות ומנגנוני מדיניות נוספים. חשוב להשאיר את השכבות נפרדות: הצלחה או כשל של בדיקה אחת אינם מוכיחים שכל שכבות ההרשאה האחרות הסכימו.
זהות אמיתית אפקטיבית ושמורה¶
תהליך מחזיק כמה מזהי משתמש וקבוצה. הזהות האמיתית קשורה למקור ההפעלה ולבדיקות מסוימות, הזהות האפקטיבית משמשת בהחלטות רבות של סמכות, והזהות השמורה מאפשרת מעברים מוגדרים לאחר הפעלת תוכנית בעלת שינוי זהות. בלינוקס קיימות גם זהויות של מערכת הקבצים, שבשימוש רגיל נעות יחד עם הזהויות האפקטיביות אך עדיין ראויות להבחנה כאשר מנתחים קוד נמוך.
אפשר להציג את שלושת המזהים בלי לשנות אותם. הדוגמה הבאה משתמשת בתלות libc = "0.2" במערכת לינוקס.
use std::io;
fn main() -> io::Result<()> {
let (mut real, mut effective, mut saved) = (0, 0, 0);
if unsafe { libc::getresuid(&mut real, &mut effective, &mut saved) } < 0 {
return Err(io::Error::last_os_error());
}
println!("uid real={real}, effective={effective}, saved={saved}");
let (mut real_group, mut effective_group, mut saved_group) = (0, 0, 0);
if unsafe { libc::getresgid(&mut real_group, &mut effective_group, &mut saved_group) } < 0 {
return Err(io::Error::last_os_error());
}
println!("gid real={real_group}, effective={effective_group}, saved={saved_group}");
println!("{}", std::fs::read_to_string("/proc/self/status")?);
Ok(())
}
בתוכנית רגילה שמופעלת ללא שינוי זהות, סביר ששלושת מזהי המשתמש יהיו שווים. אין להשתמש בציפייה הזאת כאינווריאנט של כל תוכנית. קובץ המצב מציג גם קבוצות ופרטי סמכות שנעמיק בהם בהמשך, ומאפשר להשוות בין מה שהקוד חושב לבין מה שהמערכת מציגה.
בדיקת נתיב ובדיקת אובייקט¶
כדי לפתוח נתיב, צריך לעבור דרך התיקיות שמרכיבות אותו. הרשאת חיפוש בתיקייה שונה מהרשאה לקרוא את רשימת השמות שלה. אפשר לתכנן תיקייה שאי אפשר למנות את תוכנה אבל אפשר להגיע לקובץ ששמו ידוע, אם שאר ההרשאות מאפשרות זאת.
גם מחיקה נוגעת לרשומת השם בתיקייה. קובץ שאינו ניתן לכתיבה יכול לעיתים להימחק על ידי מי שרשאי לשנות את התיקייה, בכפוף לכללים נוספים כגון ביט דביק ומדיניות המערכת. לכן הגנה על תוכן והגנה על השם הן שתי שאלות שיש לבדוק בנפרד.
המתאר שנוצר אחרי פתיחה מוצלחת מחזיק גישה לאובייקט פתוח. שינוי הרשאות הנתיב אינו בהכרח שולל את הגישה הזאת בדיעבד. זהו הבסיס להעברת סמכות באמצעות מתארים, וזה גם מסביר מדוע הורדת הרשאות צריכה לכלול סקירה של מתארים שכבר פתוחים.
ניסוי של פתיחה לפני שינוי הרשאה¶
הניסוי הבא מיועד למשתמש רגיל ללא יכולות עקיפה, בתיקיית מעבדה על מערכת קבצים שתומכת בהרשאות יוניקס. הוא יוצר תיקייה חדשה, פותח קובץ ואז מסיר הרשאות מהשם ומהתיקייה. המתאר שכבר פתוח ממשיך לקרוא את התוכן.
use std::fs::{self, DirBuilder, File, OpenOptions, Permissions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
use std::path::PathBuf;
struct Lab { directory: PathBuf, file: PathBuf }
impl Drop for Lab {
fn drop(&mut self) {
let _ = fs::set_permissions(&self.directory, Permissions::from_mode(0o700));
let _ = fs::remove_file(&self.file);
let _ = fs::remove_dir(&self.directory);
}
}
fn main() -> io::Result<()> {
if unsafe { libc::geteuid() } == 0 {
return Err(io::Error::other("run as an ordinary unprivileged user"));
}
let directory = PathBuf::from(format!("permission-lab-{}", std::process::id()));
DirBuilder::new().mode(0o700).create(&directory)?;
let lab = Lab { file: directory.join("item"), directory };
let mut opened = OpenOptions::new().read(true).write(true).create_new(true)
.mode(0o600).open(&lab.file)?;
opened.write_all(b"secret")?;
opened.set_permissions(Permissions::from_mode(0))?;
fs::set_permissions(&lab.directory, Permissions::from_mode(0))?;
assert_eq!(File::open(&lab.file).unwrap_err().kind(), io::ErrorKind::PermissionDenied);
opened.seek(SeekFrom::Start(0))?;
let mut bytes = [0_u8; 6];
opened.read_exact(&mut bytes)?;
assert_eq!(&bytes, b"secret");
println!("existing descriptor remains readable");
drop(opened);
Ok(())
}
הניקוי משחזר תחילה את הרשאת התיקייה, ואז מסיר רק את הקובץ והתיקייה שהניסוי יצר. אין כאן מחיקה רקורסיבית של נתיב שמתקבל מהמשתמש. אם שם התיקייה כבר קיים, היצירה נכשלת לפני שהניסוי משנה אותו.
הכשל בפתיחה החדשה וההצלחה דרך המתאר הישן אינם סתירה. שתי הפעולות עברו מסלולי בדיקה שונים. אם שירות מבטיח שלילת גישה מיידית, הוא צריך מנגנון מתאים מעבר לשינוי ביטי הרשאה על נתיב.
מסכה ורשימות גישה¶
מסכת היצירה umask מסירה הרשאות מבקשת היצירה הרגילה. היא אינה מוסיפה הרשאות שהקוד לא ביקש, ואינה משנה בדיעבד קבצים שכבר קיימים. המסכה היא מצב משותף של התהליך בהקשר הרגיל, ולכן שינוי זמני שלה בספרייה רב־תהליכונית עלול להתחרות ביצירת קבצים בתהליכון אחר.
כאשר לתיקייה יש רשימת גישה ברירת מחדל, יצירת אובייקט מושפעת גם ממנה והכלל הפשוט של חיסור המסכה אינו התיאור המלא. בנוסף, ברשימת גישה מורחבת ביטי הקבוצה שמציג כלי רגיל יכולים לייצג מסכה שמגבילה כמה רשומות, ולא רק את הרשאת קבוצת הבעלים.
האלגוריתם אינו מחפש תמיד את הרשומה המתירנית ביותר. אם המשתמש הוא בעל הקובץ, נבדקת רשומת הבעלים המתאימה; הוא אינו מקבל אוטומטית הרשאת אחרים רק מפני שרשומת הבעלים חסרה אותה. לכן צריך לקרוא את חוזה רשימות הגישה ולא להסיק תוצאה משלושה מספרים שנראים מוכרים.
סדר הורדת הסמכות¶
כאשר תהליך מתחיל עם סמכות לשנות זהות, עליו לתכנן את הסדר לפני שהוא מאבד אותה. בדרך כלל מטפלים תחילה בקבוצות הנוספות, אחר כך במזהי הקבוצה ולבסוף במזהי המשתמש, בהתאם לחוזה המדויק. אם משנים קודם את מזהה המשתמש ומאבדים את היכולת לשנות קבוצות, עלולות להישאר קבוצות שלא התכוונו לשמר.
יש לבדוק כל ערך חזרה. גם תהליך שנראה בעל סמכות יכול להיכשל בשינוי זהות. המשך הרצה אחרי כשל כזה עלול להשאיר את השירות עם סמכות גבוהה יותר מזו שהקוד מניח. כמו כן, שינוי זהות אפקטיבית בלבד אינו בהכרח מבטל אפשרות לחזור לזהות שמורה.
בלינוקס פרטי הזהות נמצאים ברמת משימה קרנלית, בעוד שמעטפות ספריית התהליכונים מתאמות את השינוי כדי לקיים את חוזה התהליך של הממשק התקני. עקיפה ישירה של המעטפת באמצעות קריאת מערכת גולמית בתוך תוכנית רב־תהליכונית יכולה לשנות את התמונה. לכן נבצע שינוי זהות בשלב אתחול מוגדר לפני יצירת עובדים, ככל שאפשר.
התיעוד של זהויות תהליך, שינוי מזהים, פתרון נתיב ומסכת היצירה נותן את חוזי הבסיס. בהרצאה הבאה נוסיף את קבוצות היכולות ונראה מדוע מזהה משתמש לבדו עדיין אינו כל הסמכות של השירות.