לדלג לתוכן

אבטחה בקרנל - פתרון

תרגיל 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 של התהליך:
משתמש רגיל יראה:

CapEff: 0000000000000000

כלומר - אין capabilities פעילות. root יראה:
CapEff: 000001ffffffffff

שזה כל ה-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:

sudo unshare --pid --fork --mount-proc bash

בתוך ה-shell החדש, ps aux מראה רק את bash ו-ps עצמו. כל שאר התהליכים במערכת לא נראים.

הסיבה: יצרנו PID namespace חדש. bash הוא PID 1 בתוך ה-namespace הזה (כמו init). הוא רואה רק תהליכים שנוצרו בתוך ה-namespace. הדגל --mount-proc מרכיב /proc מחדש בתוך ה-namespace, כך ש-ps קורא מה-/proc החדש.

ב. Network namespace:

sudo unshare --net bash

ip addr מראה רק את lo (loopback). אין eth0, wlan0, או כל ממשק רשת אחר.

לא ניתן לגלוש לאינטרנט - אין ממשק רשת שמחובר לעולם החיצון. כדי שזה יעבוד, צריך ליצור veth pair ולחבר אותו ל-bridge ב-namespace הראשי (זה מה ש-Docker עושה).

ג. User namespaces ואבטחת קונטיינרים:

ב-user namespace, UID 0 בתוך הקונטיינר ממופה ל-UID אחר (למשל 65534) בחוץ. זה אומר:

  • בתוך הקונטיינר: התהליך חושב שהוא root ויכול לעשות הכל (בתוך ה-namespace)
  • בחוץ: אם הוא מצליח "לברוח" מהקונטיינר, הוא משתמש רגיל ללא הרשאות

זה שכבת הגנה קריטית - container escape שהיה נותן root, עכשיו נותן רק משתמש רגיל.

ד. זיהוי מנגנונים:

  1. קונטיינר לא רואה תהליכים אחרים - PID namespace
  2. הגבלת 512MB זיכרון - cgroups (memory controller)
  3. לא יכול לקרוא ל-reboot() - seccomp (חוסם את ה-syscall) ו/או capabilities (חסר CAP_SYS_BOOT)
  4. כתובת IP משלו - Network namespace
  5. 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. התוקף יכול לנתב את הביצוע לכתובת שהוא בוחר.

תיקון:

if (req.len > sizeof(buffer))
    return -EINVAL;

מנגנוני הגנה:
- 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