אבטחה בקרנל - 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 | דיבוג processes אחרים |
| CAP_DAC_OVERRIDE | עקיפת בדיקות הרשאות קבצים |
| CAP_SETUID | שינוי UID |
| CAP_SYS_MODULE | טעינת מודולי קרנל |
| CAP_SYS_RAWIO | גישה ישירה לחומרה (I/O ports) |
כל process מחזיק ב-set של capabilities. אפשר לראות אותן:
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000 (inherited - passed down through inheritance)
# CapPrm: 0000000000000000 (permitted - allowed)
# CapEff: 0000000000000000 (effective - active)
# CapBnd: 000001ffffffffff (bounding set - upper limit)
# CapAmb: 0000000000000000 (ambient - environmental)
אפשר לפענח עם:
תוכניות setuid¶
איך /usr/bin/passwd יכולה לשנות את הסיסמה שלכם? הרי קובץ הסיסמאות (/etc/shadow) שייך ל-root.
שימו לב ל-s (setuid bit). כשprocess מריץ קובץ עם setuid bit, הוא רץ עם ההרשאות של הבעלים (במקרה הזה, root). הקרנל מבצע את זה בזמן execve().
מנקודת מבט אבטחתית, תוכניות setuid הן יעד תקיפה מועדף - כי הן רצות עם הרשאות גבוהות אבל מקבלות קלט מהמשתמש.
מנגנוני הגנת זיכרון - memory protection¶
ASLR - Address Space Layout Randomization¶
ASLR מקרין (randomizes) את הכתובות של אזורים שונים בזיכרון: stack, heap, mmap, ספריות, ואפילו ה-executable עצמו (כש-PIE מופעל).
# check ASLR state:
cat /proc/sys/kernel/randomize_va_space
# 0 = off
# 1 = partial (stack, mmap, VDSO)
# 2 = full (including heap)
למה זה עוזר? תוקף שמנצל buffer overflow צריך לדעת לאן לקפוץ. עם ASLR, הכתובות משתנות בכל הרצה, אז הוא לא יכול לחזות אותן.
# repeated runs show different addresses:
cat /proc/self/maps | head -5
# the addresses will change on every run
חולשה: אם התוקף מצליח לדלוף (leak) כתובת אחת, הוא יכול לחשב את כל השאר (כי ה-offsets בתוך ספריה קבועים).
NX/DEP - No-Execute¶
מסמן pages זיכרון של נתונים כ-לא ניתנים להרצה. זה מונע את ההתקפה הקלאסית של הזרקת קוד (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 ומנסה להריץ קוד מpage שמסומן כ-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, הקרנל נטען לכתובת אקראית. זה מקשה על תוקף שמנצל באג בקרנל לדעת איפה נמצאות פונקציות ומבני נתונים של הקרנל.
# without KASLR, the address is fixed:
# _text = 0xffffffff81000000
# with KASLR, the address changes on every boot:
# _text = 0xffffffff81000000 + random_offset
הגנה משלימה: /proc/kallsyms מוצפן ברירת מחדל למשתמשים רגילים (kptr_restrict).
מגני מחסנית - stack canaries¶
ערכים אקראיים שממוקמים על ה-stack, בין המשתנים המקומיים לכתובת החזרה:
+------------------+
| return address | <-- the attacker wants to change this
+------------------+
| stack canary | <-- random value. if changed = overflow detected
+------------------+
| local variables | <-- buffer overflow starts here
+------------------+
אם buffer overflow דורס את ה-canary, הקרנל מזהה את השינוי ומבצע panic (עדיף לקרוס מאשר לתת לתוקף שליטה). מופעל עם CONFIG_STACKPROTECTOR.
Seccomp - מצב חישוב מאובטח¶
Seccomp מגביל אילו syscalls process יכול לבצע. זה עיקרון ה-least privilege - למה לתת לתוכנה גישה ל-300+ syscalls אם היא צריכה רק 10?
מצבים¶
מצב 1 (strict): מאפשר רק read, write, _exit, sigreturn. כל syscall אחר = SIGKILL.
מצב 2 (filter - seccomp-bpf): מאפשר להגדיר מסנן BPF שמחליט לכל syscall אם לאשר, לדחות, או לדווח.
// simple example with libseccomp:
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // default: 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¶
מרחבי שמות מאפשרים בידוד - כל process (או קבוצת processes) רואה "גרסה" שונה של המערכת.
| מרחב שמות | מה הוא מבודד | דוגמה |
|---|---|---|
| PID namespace | טבלת processes | הprocess רואה את עצמו כ-PID 1 |
| Mount namespace | מערכת קבצים | עץ קבצים שונה לגמרי |
| Network namespace | מחסנית רשת | כתובות IP, טבלאות ניתוב, interfaces שונים |
| User namespace | מיפוי UID/GID | root בתוך הקונטיינר הוא user רגיל בחוץ! |
| UTS namespace | שם המחשב | hostname שונה |
| IPC namespace | תקשורת בין-תהליכית | הqueues הודעות, semaphores נפרדים |
| Cgroup namespace | תצוגת cgroups | הprocess רואה את עצמו בשורש ה-cgroup |
| Time namespace | שעון המערכת | זמן שונה (נוסף ב-5.6) |
יצירת namespace¶
// create a process in a new namespace:
clone(child_func, stack, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL);
// enter an existing namespace:
int fd = open("/proc/1234/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);
// create a new namespace in the current process:
unshare(CLONE_NEWNS);
מרחב שמות משתמשים - user namespace¶
זה אולי ה-namespace המעניין ביותר מבחינה אבטחתית. הוא מאפשר מיפוי UID:
מה שאומר שגם אם התוקף משיג root בתוך הקונטיינר, בחוץ הוא עדיין משתמש רגיל. זה בסיס האבטחה של קונטיינרים rootless.
namespaces וקונטיינרים¶
Docker = namespaces + cgroups + seccomp + capabilities.
כשאתם מריצים docker run, Docker יוצר:
- PID namespace (הprocess רואה את עצמו כ-PID 1)
- Mount namespace (מערכת קבצים של ה-image)
- Network namespace (interface veth משלו)
- UTS namespace (hostname של הקונטיינר)
- ומגביל capabilities ו-syscalls
Cgroups - קבוצות בקרה¶
קבוצות בקרה (Control Groups) מגבילות משאבים לקבוצת processes:
- CPU - כמה זמן מעבד הקבוצה מקבלת
- זיכרון - הגבלת זיכרון (OOM killer הורג processes בקבוצה שחורגת)
- קלט/פלט - הגבלת bandwidth לדיסק
- רשת - תעדוף תעבורה
# cgroups structure:
ls /sys/fs/cgroup/
# cpu memory blkio pids ...
# limit memory for a process:
echo $$ > /sys/fs/cgroup/memory/mygroup/tasks
echo 100M > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes
# limit CPU to 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 - everything under a single mount point:
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 המדיניות מחליטה
- כל קובץ, process ו-socket מקבל תווית (label)
- מדיניות מגדירה אילו תוויות יכולות לגשת לאילו תוויות
- משמש ב-Android, Red Hat, Fedora
# check SELinux state:
getenforce
# Enforcing / Permissive / Disabled
# labels:
ls -Z /usr/bin/passwd
# system_u:object_r:passwd_exec_t:s0 /usr/bin/passwd
AppArmor¶
- גישה פשוטה יותר מ-SELinux
- מבוסס על נתיבי קבצים (לא תוויות)
- משמש ב-Ubuntu, SUSE
# AppArmor profile:
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);
// ... use obj ...
kfree(obj);
// ... later, by mistake ...
obj->func_ptr(); // Use-after-free! the attacker can control func_ptr
תנאי מרוץ - race conditions:
שני threads ניגשים למשאב ללא סנכרון נכון. יכול להוביל ל-privilege escalation (ראו את חולשת Dirty COW - CVE-2016-5195).
גלישת שלמים - integer overflow:
size_t size = user_provided_count * sizeof(struct element);
// if user_provided_count is very large, size overflows to 0
void *buf = kmalloc(size, GFP_KERNEL); // allocates 0 bytes
copy_from_user(buf, user_data, real_size); // writes past the boundary!
מבנה ה-cred - המטרה של התוקף¶
struct cred {
atomic_t usage;
kuid_t uid; // who am I?
kgid_t gid;
kuid_t euid; // who am I in practice?
kgid_t egid;
// ...
kernel_cap_t cap_effective; // effective capabilities
kernel_cap_t cap_permitted; // permitted capabilities
// ...
};
כל process מצביע למבנה cred שמכיל את ההרשאות שלו. אם תוקף מצליח לשנות את ה-uid ל-0, או להוסיף capabilities - הוא השיג privilege escalation.
טכניקת ההתקפה הקלאסית:
1. מנצלים באג בקרנל (overflow, use-after-free, וכו')
2. משיגים שליטה על ביצוע קוד בקרנל
3. קוראים ל-commit_creds(prepare_kernel_cred(NULL)) - יוצרים cred חדש עם הרשאות root ומחילים אותו על הprocess הנוכחי
4. חוזרים ל-user space עם הרשאות root
עם SMEP, SMAP, ו-KASLR, ההתקפה הזו הרבה יותר קשה - אבל לא בלתי אפשרית (יש טכניקות עקיפה כמו ROP בקרנל).
סיכום - שכבות ההגנה¶
+-----------------------------------------------+
| application |
+-----------------------------------------------+
| seccomp (restricting syscalls) |
+-----------------------------------------------+
| LSM - SELinux/AppArmor (mandatory access control) |
+-----------------------------------------------+
| capabilities (fine-grained permissions) |
+-----------------------------------------------+
| namespaces (isolation) | cgroups (resource limiting) |
+-----------------------------------------------+
| ASLR / KASLR / NX / SMEP / SMAP / canaries |
+-----------------------------------------------+
| Ring 0 / Ring 3 (hardware separation) |
+-----------------------------------------------+
אבטחה בקרנל עובדת בשכבות - הגנה לעומק (defense in depth). גם אם שכבה אחת נפרצת, השכבות האחרות עדיין מגינות. לכן, במערכות מאובטחות מפעילים את כולן.