לדלג לתוכן

אבטחה בקרנל - 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)

אפשר לפענח עם:

capsh --decode=000001ffffffffff

תוכניות setuid

איך /usr/bin/passwd יכולה לשנות את הסיסמה שלכם? הרי קובץ הסיסמאות (/etc/shadow) שייך ל-root.

ls -la /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 ... /usr/bin/passwd

שימו לב ל-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.

#include <linux/seccomp.h>
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

מצב 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:

# inside the container: UID 0 (root)
# outside: UID 1000 (regular user)

מה שאומר שגם אם התוקף משיג 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). גם אם שכבה אחת נפרצת, השכבות האחרות עדיין מגינות. לכן, במערכות מאובטחות מפעילים את כולן.