אבטחה בקרנל - פתרון¶
תרגיל 1 - בדיקת מצב אבטחה¶
א. מצב ASLR:
ברוב ההפצות המודרניות הערך הוא 2 (מלא). זה אומר שה-stack, heap, mmap, VDSO, וספריות כולם מקורינים.
ב. הרצה כפולה של /proc/self/maps:
הכתובות צריכות להיות שונות בכל הרצה. למשל:
# הרצה ראשונה:
560a3f200000-560a3f201000 r--p ... /usr/bin/cat
# הרצה שנייה:
55f7a8400000-55f7a8401000 r--p ... /usr/bin/cat
זה ASLR בפעולה - כל הרצה של תוכנית מקבלת כתובות שונות.
ג. capabilities של התהליך:
משתמש רגיל יראה:
כלומר - אין capabilities פעילות. root יראה:
שזה כל ה-capabilities.
ד. KASLR:
אם הכתובת של _text שונה אחרי reboot, KASLR פעיל. אם היא תמיד ffffffff81000000 - KASLR כבוי.
ה. תוכניות setuid:
תוכניות נפוצות:
- /usr/bin/passwd - שינוי סיסמה, צריך לכתוב ל-/etc/shadow (שייך ל-root)
- /usr/bin/sudo - הרצת פקודות כ-root
- /usr/bin/su - החלפת משתמש
- /usr/bin/ping - שימוש ב-raw sockets (דורש CAP_NET_RAW)
- /usr/bin/mount - הרכבת מערכות קבצים (דורש הרשאות root)
- /usr/bin/newgrp - שינוי קבוצה ראשית
תרגיל 2 - capabilities¶
א. ההבדל בין סטי capabilities:
- CapInh (inherited) - capabilities שעוברות בירושה ל-exec(). אם capability נמצאת גם ב-inherited של התהליך וגם ב-permitted של הקובץ, היא תהיה פעילה בתהליך החדש.
- CapPrm (permitted) - הסט המקסימלי של capabilities שהתהליך יכול להפעיל. הוא יכול "להדליק" capability ב-effective רק אם היא קיימת ב-permitted.
- CapEff (effective) - ה-capabilities שפעילות כרגע. אלה ה-capabilities שהקרנל באמת בודק כשהתהליך מנסה לבצע פעולה פריבילגית.
- CapBnd (bounding set) - הגבלה עליונה. capability שלא נמצאת ב-bounding set לא יכולה להיכנס ל-permitted, גם לא דרך exec() של קובץ עם capabilities.
ב. הרצת nginx על פורט 80 בלי root:
# נותנים ל-nginx את היכולת לחבור לפורטים נמוכים:
sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx
זה נותן את CAP_NET_BIND_SERVICE בסט ה-effective ו-permitted של הקובץ. כשמשתמש רגיל מריץ את nginx, הוא יקבל רק את ה-capability הזו - לא את כל כוחות ה-root.
ג. CAP_SYS_ADMIN - "the new root":
CAP_SYS_ADMIN היא capability "סל זבל" שכוללת הרבה מאוד פעולות שלא קיבלו capability ייעודית. היא מאפשרת:
- mount/umount
- שינוי hostname
- שימוש ב-ioctl ספציפיים
- שינוי הגדרות מערכת רבות
- ועוד הרבה מאוד
הבעיה: מי שמחזיק ב-CAP_SYS_ADMIN יכול לעשות כמעט הכל. זה מפר את עיקרון ה-least privilege - נתנו capability אחת וקיבלנו כמעט root. זה מוריד את הערך של כל מנגנון ה-capabilities.
תרגיל 3 - seccomp¶
א. syscalls מותרים:
- read (מספר 0)
- write (מספר 1)
- exit_group (מספר 231)
כל syscall אחר יגרום להריגת התהליך (SECCOMP_RET_KILL).
ב. מה יקרה בשורה A (fopen)?
fopen קורא בפנים ל-open syscall (או openat). ה-syscall הזה לא מותר במסנן. התוצאה: הקרנל שולח SIGKILL לתהליך, והוא נהרג מיד.
ג. האם שורה B תתבצע?
לא. התהליך נהרג בשורה A. הוא לא מגיע לשורה B. seccomp עם SECCOMP_RET_KILL הורג את התהליך מיד כשה-syscall הלא מורשה נקרא.
ד. PR_SET_NO_NEW_PRIVS:
הדגל הזה מבטיח שהתהליך (וצאצאיו) לא יוכלו לקבל הרשאות חדשות. למשל, exec() של קובץ setuid לא ייתן הרשאות root.
זה נדרש לפני seccomp כי בלעדיו, תהליך יכול:
1. להתקין מסנן seccomp
2. לעשות exec() לתוכנית setuid שרצה כ-root
3. התוכנית רצה עם ה-seccomp filter - אבל כ-root!
זה מאפשר לתוקף להגביל syscalls של תוכנית root ולנצל את ההגבלה (למשל, למנוע מהתוכנית לשחרר הרשאות). PR_SET_NO_NEW_PRIVS מונע את כל התרחיש הזה.
תרגיל 4 - namespaces¶
א. PID namespace:
בתוך ה-shell החדש, ps aux מראה רק את bash ו-ps עצמו. כל שאר התהליכים במערכת לא נראים.
הסיבה: יצרנו PID namespace חדש. bash הוא PID 1 בתוך ה-namespace הזה (כמו init). הוא רואה רק תהליכים שנוצרו בתוך ה-namespace. הדגל --mount-proc מרכיב /proc מחדש בתוך ה-namespace, כך ש-ps קורא מה-/proc החדש.
ב. Network namespace:
ip addr מראה רק את lo (loopback). אין eth0, wlan0, או כל ממשק רשת אחר.
לא ניתן לגלוש לאינטרנט - אין ממשק רשת שמחובר לעולם החיצון. כדי שזה יעבוד, צריך ליצור veth pair ולחבר אותו ל-bridge ב-namespace הראשי (זה מה ש-Docker עושה).
ג. User namespaces ואבטחת קונטיינרים:
ב-user namespace, UID 0 בתוך הקונטיינר ממופה ל-UID אחר (למשל 65534) בחוץ. זה אומר:
- בתוך הקונטיינר: התהליך חושב שהוא root ויכול לעשות הכל (בתוך ה-namespace)
- בחוץ: אם הוא מצליח "לברוח" מהקונטיינר, הוא משתמש רגיל ללא הרשאות
זה שכבת הגנה קריטית - container escape שהיה נותן root, עכשיו נותן רק משתמש רגיל.
ד. זיהוי מנגנונים:
- קונטיינר לא רואה תהליכים אחרים - PID namespace
- הגבלת 512MB זיכרון - cgroups (memory controller)
- לא יכול לקרוא ל-reboot() - seccomp (חוסם את ה-syscall) ו/או capabilities (חסר CAP_SYS_BOOT)
- כתובת IP משלו - Network namespace
- root בקונטיינר לא יכול לטעון מודולים - capabilities (חסר CAP_SYS_MODULE). גם user namespace תורם - root בקונטיינר אינו root אמיתי.
תרגיל 5 - ניתוח חולשות¶
קטע א - buffer overflow¶
החולשה: req.len מגיע מהמשתמש ולא מאומת. אם req.len > 256, copy_from_user יכתוב מעבר לגבול של buffer (שהוא 256 בייטים על ה-stack).
ניצול: תוקף שולח req.len = 1024 (או כל ערך גדול מ-256). ה-copy_from_user דורס את ה-return address על ה-stack. התוקף יכול לנתב את הביצוע לכתובת שהוא בוחר.
תיקון:
מנגנוני הגנה:
- Stack canary - יזהה את ה-overflow ויגרום ל-panic
- KASLR - גם אם דורסים, לא יודעים לאן לקפוץ
- NX - לא ניתן להריץ shellcode על ה-stack
קטע ב - use-after-free¶
החולשה: אם device_close נקרא (ומשחרר את ה-device) ואז device_read נקרא (שמנסה לקרוא מה-device), יש גישה לזיכרון שכבר שוחרר.
ניצול:
1. תהליך A פותח את ה-device
2. תהליך A סוגר את ה-device (kfree)
3. תוקף מקצה אובייקט slab באותו גודל - הוא מקבל את אותו הזיכרון!
4. תהליך A קורא מה-device - קורא את הנתונים של התוקף
5. או גרוע יותר: אם cleanup function pointer נדרס, ה-release הבא מפעיל קוד של התוקף
תיקון: לבדוק ב-device_read שה-refcount עדיין חיובי, או להשתמש ב-kref_get/kref_put נכון כך שהאובייקט לא ישוחרר כל עוד יש readers.
מנגנוני הגנה:
- SLAB_TYPESAFE_BY_RCU - מאפשר RCU-style גישה ל-slab objects
- CONFIG_KASAN (Kernel Address Sanitizer) - מזהה use-after-free בזמן פיתוח
קטע ג - TOCTOU race condition¶
החולשה: Time-Of-Check to Time-Of-Use (TOCTOU). יש חלון זמן בין הבדיקה (Step 1) לבין הקריאה (Step 2). במהלך החלון הזה, התוקף יכול להחליף את הקובץ.
ניצול:
1. תוקף יוצר symlink שמצביע לקובץ שלו (עובר את הבדיקה)
2. הקרנל בודק הרשאות - עובר
3. בין הבדיקה לקריאה, תוקף מחליף את ה-symlink להצביע על /etc/shadow
4. הקרנל קורא את /etc/shadow ומחזיר את התוכן לתוקף
תיקון: במקום לבדוק ואז לפתוח, לפתוח מיד ולבדוק הרשאות על ה-file descriptor (שכבר פתוח ולא ניתן להחלפה). להשתמש ב-fstat במקום stat, או לבצע את הבדיקה והקריאה כפעולה אטומית.
// תיקון - פתיחה עם בדיקת הרשאות אטומית:
struct file *f = filp_open(filename, O_RDONLY, 0);
if (IS_ERR(f))
return PTR_ERR(f);
// כעת f מצביע לקובץ ספציפי, לא ניתן להחלפה
מנגנוני הגנה:
- symlink restrictions (protected_symlinks) - מגביל מעקב אחרי symlinks ב-sticky directories
- LSM hooks - יכולים לחסום את הגישה גם ב-Step 2