אבטחה בקרנל - kernel security¶
מבוא¶
הקרנל הוא גבול האבטחה האולטימטיבי במערכת ההפעלה. אם תוקף מצליח לפרוץ לקרנל - הוא שולט על כל המערכת. אין שכבה גבוהה יותר שתגן עליכם. לכן, הבנה של מנגנוני האבטחה בקרנל היא קריטית - גם למי שכותב קוד קרנל, וגם למי שמנסה לפרוץ אותו.
בהרצאה הזו נעבור על כל שכבות ההגנה של לינוקס, מרמת החומרה ועד רמת היישום.
הגנה מבוססת טבעות - ring-based protection¶
חזרה לחומר מסעיף 2 - ראינו שהמעבד מספק מנגנון הגנה מובנה בחומרה:
- טבעת 0 (Ring 0) - הקרנל. גישה מלאה לכל הזיכרון, כל ההוראות
- טבעת 3 (Ring 3) - user space. גישה מוגבלת
ה-CPU אוכף את ההפרדה הזו. קוד ב-Ring 3 לא יכול:
- לגשת לזיכרון הקרנל (page table entries מסומנים כ-supervisor only)
- להריץ הוראות פריבילגיות (כמו hlt, lgdt, mov cr3)
- לשנות את ה-ring שבו הוא רץ (חוץ מדרך syscall)
קריאות מערכת (syscalls) הן השער המבוקר - הדרך היחידה שבה קוד user space יכול לבקש שירות מהקרנל. הקרנל מאמת כל בקשה לפני שמבצע אותה.
הרשאות משתמשים ויכולות - capabilities¶
המודל המסורתי¶
במודל המסורתי של Unix, יש הפרדה פשוטה:
- UID 0 (root) - יכול לעשות הכל
- כל השאר - מוגבלים
הבעיה: זה all-or-nothing. אם תוכנה צריכה רק לפתוח פורט מתחת ל-1024, היא צריכה הרשאות root מלאות? זה מסוכן.
יכולות - capabilities¶
לינוקס פיצל את "כוחות ה-root" ליכולות (capabilities) נפרדות:
| יכולת | מה היא מאפשרת |
|---|---|
| CAP_NET_RAW | יצירת raw sockets (לסריקת רשת, ping) |
| CAP_NET_BIND_SERVICE | חיבור לפורטים מתחת ל-1024 |
| CAP_SYS_ADMIN | פעולות ניהול שונות (mount, swapon, ועוד) |
| CAP_SYS_PTRACE | דיבוג תהליכים אחרים |
| CAP_DAC_OVERRIDE | עקיפת בדיקות הרשאות קבצים |
| CAP_SETUID | שינוי UID |
| CAP_SYS_MODULE | טעינת מודולי קרנל |
| CAP_SYS_RAWIO | גישה ישירה לחומרה (I/O ports) |
כל תהליך מחזיק ב-set של capabilities. אפשר לראות אותן:
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000 (inherited - עוברות בירושה)
# CapPrm: 0000000000000000 (permitted - מותרות)
# CapEff: 0000000000000000 (effective - פעילות)
# CapBnd: 000001ffffffffff (bounding set - הגבלה עליונה)
# CapAmb: 0000000000000000 (ambient - סביבתיות)
אפשר לפענח עם:
תוכניות setuid¶
איך /usr/bin/passwd יכולה לשנות את הסיסמה שלכם? הרי קובץ הסיסמאות (/etc/shadow) שייך ל-root.
שימו לב ל-s (setuid bit). כשתהליך מריץ קובץ עם setuid bit, הוא רץ עם ההרשאות של הבעלים (במקרה הזה, root). הקרנל מבצע את זה בזמן execve().
מנקודת מבט אבטחתית, תוכניות setuid הן יעד תקיפה מועדף - כי הן רצות עם הרשאות גבוהות אבל מקבלות קלט מהמשתמש.
מנגנוני הגנת זיכרון - memory protection¶
ASLR - Address Space Layout Randomization¶
ASLR מקרין (randomizes) את הכתובות של אזורים שונים בזיכרון: stack, heap, mmap, ספריות, ואפילו ה-executable עצמו (כש-PIE מופעל).
# בדיקת מצב ASLR:
cat /proc/sys/kernel/randomize_va_space
# 0 = כבוי
# 1 = חלקי (stack, mmap, VDSO)
# 2 = מלא (כולל heap)
למה זה עוזר? תוקף שמנצל buffer overflow צריך לדעת לאן לקפוץ. עם ASLR, הכתובות משתנות בכל הרצה, אז הוא לא יכול לחזות אותן.
חולשה: אם התוקף מצליח לדלוף (leak) כתובת אחת, הוא יכול לחשב את כל השאר (כי ה-offsets בתוך ספריה קבועים).
NX/DEP - No-Execute¶
מסמן דפי זיכרון של נתונים כ-לא ניתנים להרצה. זה מונע את ההתקפה הקלאסית של הזרקת קוד (shellcode injection) - גם אם התוקף הצליח לכתוב קוד ל-stack או ל-heap, ה-CPU יסרב להריץ אותו.
מבחינה טכנית, זה ה-NX bit ב-page table entry (חזרה לסעיף 2 על paging!). ה-CPU בודק את הביט הזה בכל instruction fetch.
SMEP - Supervisor Mode Execution Prevention¶
מונע מהקרנל להריץ קוד שנמצא ב-user space. למה זה חשוב?
התקפה קלאסית בשם ret2usr (חזרה ל-user): התוקף מנצל באג בקרנל כדי לקפוץ לקוד שהוא כתב ב-user space (שם הוא שולט). הקוד רץ עם הרשאות קרנל כי ה-CPU עדיין ב-Ring 0. SMEP חוסם את זה - אם ה-CPU ב-Ring 0 ומנסה להריץ קוד מדף שמסומן כ-user, הוא מייצר page fault.
SMAP - Supervisor Mode Access Prevention¶
מונע מהקרנל לקרוא זיכרון user space בלי אישור מפורש. זה מכריח שימוש ב-copy_from_user() / copy_to_user() - פונקציות שמאמתות שהכתובת באמת שייכת ל-user space.
בלי SMAP, באג בקרנל יכול לאפשר לתוקף להערים על הקרנל לקרוא נתונים מזויפים מ-user space.
KASLR - Kernel ASLR¶
כמו ASLR, אבל לקרנל עצמו. בכל boot, הקרנל נטען לכתובת אקראית. זה מקשה על תוקף שמנצל באג בקרנל לדעת איפה נמצאות פונקציות ומבני נתונים של הקרנל.
# בלי KASLR, הכתובת קבועה:
# _text = 0xffffffff81000000
# עם KASLR, הכתובת משתנה בכל boot:
# _text = 0xffffffff81000000 + random_offset
הגנה משלימה: /proc/kallsyms מוצפן ברירת מחדל למשתמשים רגילים (kptr_restrict).
מגני מחסנית - stack canaries¶
ערכים אקראיים שממוקמים על ה-stack, בין המשתנים המקומיים לכתובת החזרה:
+------------------+
| return address | <-- התוקף רוצה לשנות את זה
+------------------+
| stack canary | <-- ערך אקראי. אם שונה = overflow detected
+------------------+
| local variables | <-- buffer overflow מתחיל פה
+------------------+
אם buffer overflow דורס את ה-canary, הקרנל מזהה את השינוי ומבצע panic (עדיף לקרוס מאשר לתת לתוקף שליטה). מופעל עם CONFIG_STACKPROTECTOR.
Seccomp - מצב חישוב מאובטח¶
Seccomp מגביל אילו syscalls תהליך יכול לבצע. זה עיקרון ה-least privilege - למה לתת לתוכנה גישה ל-300+ syscalls אם היא צריכה רק 10?
מצבים¶
מצב 1 (strict): מאפשר רק read, write, _exit, sigreturn. כל syscall אחר = SIGKILL.
מצב 2 (filter - seccomp-bpf): מאפשר להגדיר מסנן BPF שמחליט לכל syscall אם לאשר, לדחות, או לדווח.
// דוגמה פשוטה עם libseccomp:
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // ברירת מחדל: kill
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_load(ctx);
שימושים בעולם האמיתי¶
- Chrome/Chromium - ה-renderer process (שמריץ JavaScript ו-HTML לא מהימנים) רץ עם seccomp שחוסם syscalls מסוכנים
- Docker - קונטיינרים רצים עם profil seccomp שחוסם ~44 syscalls כברירת מחדל
- systemd - שירותים יכולים להגדיר
SystemCallFilter=כדי להגביל syscalls
מרחבי שמות - namespaces¶
מרחבי שמות מאפשרים בידוד - כל תהליך (או קבוצת תהליכים) רואה "גרסה" שונה של המערכת.
| מרחב שמות | מה הוא מבודד | דוגמה |
|---|---|---|
| PID namespace | טבלת תהליכים | תהליך רואה את עצמו כ-PID 1 |
| Mount namespace | מערכת קבצים | עץ קבצים שונה לגמרי |
| Network namespace | מחסנית רשת | כתובות IP, טבלאות ניתוב, ממשקים שונים |
| User namespace | מיפוי UID/GID | root בתוך הקונטיינר הוא user רגיל בחוץ! |
| UTS namespace | שם המחשב | hostname שונה |
| IPC namespace | תקשורת בין-תהליכית | תורי הודעות, semaphores נפרדים |
| Cgroup namespace | תצוגת cgroups | התהליך רואה את עצמו בשורש ה-cgroup |
| Time namespace | שעון המערכת | זמן שונה (נוסף ב-5.6) |
יצירת namespace¶
// יצירת תהליך ב-namespace חדש:
clone(child_func, stack, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL);
// כניסה ל-namespace קיים:
int fd = open("/proc/1234/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);
// יצירת namespace חדש בתהליך הנוכחי:
unshare(CLONE_NEWNS);
מרחב שמות משתמשים - user namespace¶
זה אולי ה-namespace המעניין ביותר מבחינה אבטחתית. הוא מאפשר מיפוי UID:
מה שאומר שגם אם התוקף משיג root בתוך הקונטיינר, בחוץ הוא עדיין משתמש רגיל. זה בסיס האבטחה של קונטיינרים rootless.
namespaces וקונטיינרים¶
Docker = namespaces + cgroups + seccomp + capabilities.
כשאתם מריצים docker run, Docker יוצר:
- PID namespace (התהליך רואה את עצמו כ-PID 1)
- Mount namespace (מערכת קבצים של ה-image)
- Network namespace (ממשק veth משלו)
- UTS namespace (hostname של הקונטיינר)
- ומגביל capabilities ו-syscalls
Cgroups - קבוצות בקרה¶
קבוצות בקרה (Control Groups) מגבילות משאבים לקבוצת תהליכים:
- CPU - כמה זמן מעבד הקבוצה מקבלת
- זיכרון - הגבלת זיכרון (OOM killer הורג תהליכים בקבוצה שחורגת)
- קלט/פלט - הגבלת bandwidth לדיסק
- רשת - תעדוף תעבורה
# מבנה cgroups:
ls /sys/fs/cgroup/
# cpu memory blkio pids ...
# הגבלת זיכרון לתהליך:
echo $$ > /sys/fs/cgroup/memory/mygroup/tasks
echo 100M > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes
# הגבלת CPU ל-50%:
echo 50000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us
Cgroups v2¶
הגרסה החדשה (cgroups v2) מאחדת את כל הבקרים תחת היררכיה אחת ופשוטה יותר:
# cgroups v2 - הכל תחת נקודת mount אחת:
ls /sys/fs/cgroup/
# cgroup.controllers cgroup.subtree_control memory.max cpu.max ...
LSM - Linux Security Modules¶
LSM הוא framework שמאפשר להוסיף מדיניות אבטחה מעל ההרשאות הרגילות. הוא מוסיף hooks בנקודות קריטיות בקרנל (פתיחת קובץ, יצירת socket, טעינת מודול, וכו').
SELinux¶
- פותח ע"י ה-NSA (כן, באמת)
- בקרת גישה חובה (Mandatory Access Control - MAC) - בניגוד ל-DAC שבו הבעלים מחליט, ב-MAC המדיניות מחליטה
- כל קובץ, תהליך ו-socket מקבל תווית (label)
- מדיניות מגדירה אילו תוויות יכולות לגשת לאילו תוויות
- משמש ב-Android, Red Hat, Fedora
# בדיקת מצב SELinux:
getenforce
# Enforcing / Permissive / Disabled
# תוויות:
ls -Z /usr/bin/passwd
# system_u:object_r:passwd_exec_t:s0 /usr/bin/passwd
AppArmor¶
- גישה פשוטה יותר מ-SELinux
- מבוסס על נתיבי קבצים (לא תוויות)
- משמש ב-Ubuntu, SUSE
# פרופיל AppArmor:
cat /etc/apparmor.d/usr.bin.firefox
# /usr/bin/firefox {
# /home/*/.mozilla/** rw,
# /tmp/** rw,
# deny /etc/shadow r,
# ...
# }
חולשות נפוצות בקרנל¶
סוגי חולשות¶
גלישת מאגר - buffer overflow:
כמו ב-user space, אבל עם השלכות חמורות יותר. overflow בקרנל יכול לדרוס מבני נתונים קריטיים.
שימוש אחרי שחרור - use-after-free:
אובייקט ב-slab allocator משוחרר, אבל מצביע אליו עדיין קיים. תוקף יכול להקצות אובייקט חדש באותו מקום ולשלוט בתוכן.
struct my_obj *obj = kmalloc(sizeof(*obj), GFP_KERNEL);
// ... שימוש ב-obj ...
kfree(obj);
// ... בהמשך, בטעות ...
obj->func_ptr(); // Use-after-free! התוקף יכול לשלוט ב-func_ptr
תנאי מרוץ - race conditions:
שני threads ניגשים למשאב ללא סנכרון נכון. יכול להוביל ל-privilege escalation (ראו את חולשת Dirty COW - CVE-2016-5195).
גלישת שלמים - integer overflow:
size_t size = user_provided_count * sizeof(struct element);
// אם user_provided_count גדול מאוד, size גולש ל-0
void *buf = kmalloc(size, GFP_KERNEL); // מקצים 0 בייטים
copy_from_user(buf, user_data, real_size); // כותבים מעבר לגבול!
מבנה ה-cred - המטרה של התוקף¶
struct cred {
atomic_t usage;
kuid_t uid; // מי אני?
kgid_t gid;
kuid_t euid; // מי אני בפועל?
kgid_t egid;
// ...
kernel_cap_t cap_effective; // יכולות פעילות
kernel_cap_t cap_permitted; // יכולות מותרות
// ...
};
כל תהליך מצביע למבנה cred שמכיל את ההרשאות שלו. אם תוקף מצליח לשנות את ה-uid ל-0, או להוסיף capabilities - הוא השיג privilege escalation.
טכניקת ההתקפה הקלאסית:
1. מנצלים באג בקרנל (overflow, use-after-free, וכו')
2. משיגים שליטה על ביצוע קוד בקרנל
3. קוראים ל-commit_creds(prepare_kernel_cred(NULL)) - יוצרים cred חדש עם הרשאות root ומחילים אותו על התהליך הנוכחי
4. חוזרים ל-user space עם הרשאות root
עם SMEP, SMAP, ו-KASLR, ההתקפה הזו הרבה יותר קשה - אבל לא בלתי אפשרית (יש טכניקות עקיפה כמו ROP בקרנל).
סיכום - שכבות ההגנה¶
+-----------------------------------------------+
| אפליקציה |
+-----------------------------------------------+
| seccomp (הגבלת syscalls) |
+-----------------------------------------------+
| LSM - SELinux/AppArmor (בקרת גישה חובה) |
+-----------------------------------------------+
| capabilities (הרשאות מפורטות) |
+-----------------------------------------------+
| namespaces (בידוד) | cgroups (הגבלת משאבים) |
+-----------------------------------------------+
| ASLR / KASLR / NX / SMEP / SMAP / canaries |
+-----------------------------------------------+
| Ring 0 / Ring 3 (הפרדת חומרה) |
+-----------------------------------------------+
אבטחה בקרנל עובדת בשכבות - הגנה לעומק (defense in depth). גם אם שכבה אחת נפרצת, השכבות האחרות עדיין מגינות. לכן, במערכות מאובטחות מפעילים את כולן.