ספר "BPF לניטור לינוקס"

ספר "BPF לניטור לינוקס"שלום תושבי חברו! המכונה הוירטואלית BPF היא אחד המרכיבים החשובים ביותר של ליבת לינוקס. שימוש נכון בו יאפשר למהנדסי מערכת למצוא תקלות ולפתור אפילו את הבעיות המורכבות ביותר. תלמד כיצד לכתוב תוכניות המנטרות ומשנות את התנהגות הליבה, כיצד ליישם בבטחה קוד לניטור אירועים בקרנל ועוד ועוד. דיוויד קלאוורה ולורנצו פונטנה יעזרו לך לפתוח את הכוח של BPF. הרחב את הידע שלך באופטימיזציה של ביצועים, רשתות, אבטחה. - השתמש ב-BPF כדי לפקח ולשנות את ההתנהגות של ליבת לינוקס. - הכנס קוד לניטור מאובטח של אירועי ליבה מבלי צורך להדר מחדש את הליבה או לאתחל את המערכת. - השתמש בדוגמאות קוד נוחות ב-C, Go או Python. - קח שליטה על ידי בעלות על מחזור החיים של תוכנית BPF.

אבטחת ליבת לינוקס, תכונותיה ו-Secomp

BPF מספק דרך רבת עוצמה להרחיב את הליבה מבלי להקריב יציבות, אבטחה או מהירות. מסיבה זו, מפתחי הליבה חשבו שזה יהיה רעיון טוב להשתמש ברבסטיות שלו כדי לשפר את בידוד התהליך ב-Seccomp על ידי הטמעת מסנני Seccomp הנתמכים על ידי תוכניות BPF, הידועות גם בשם Seccomp BPF. בפרק זה נסביר מהי Seccomp וכיצד משתמשים בה. לאחר מכן תלמד כיצד לכתוב מסנני Seccomp באמצעות תוכניות BPF. לאחר מכן, נסתכל על ה-BPF המובנים הכלולים בקרנל עבור מודולי אבטחה של לינוקס.

מודולי אבטחה של לינוקס (LSM) הם מסגרת המספקת סט של פונקציות שניתן להשתמש בהן כדי ליישם מודלים אבטחה שונים בצורה סטנדרטית. ניתן להשתמש ב-LSM ישירות בעץ המקור של הליבה, כגון Apparmor, SELinux ו-Tomoyo.

בואו נתחיל בדיון ביכולות של לינוקס.

יכולות

המהות של היכולות של לינוקס היא שעליך להעניק לתהליך ללא הרשאות הרשאה לבצע משימה מסוימת, אך מבלי להשתמש ב-suid למטרה זו, או להפוך את התהליך לפריבילגי בדרך אחרת, מה שמפחית את האפשרות להתקפה ומאפשר לתהליך לבצע משימות מסוימות. לדוגמה, אם היישום שלך צריך לפתוח פורט מיוחס, נניח 80, במקום להריץ את התהליך כשורש, אתה יכול פשוט לתת לו את היכולת CAP_NET_BIND_SERVICE.

שקול תוכנית Go בשם main.go:

package main
import (
            "net/http"
            "log"
)
func main() {
     log.Fatalf("%v", http.ListenAndServe(":80", nil))
}

תוכנית זו משרתת שרת HTTP ביציאה 80 (זוהי יציאה מיוחסת). בדרך כלל אנו מריצים אותו מיד לאחר הקומפילציה:

$ go build -o capabilities main.go
$ ./capabilities

עם זאת, מכיוון שאיננו מעניקים הרשאות שורש, קוד זה יזרוק שגיאה בעת קשירת הפורט:

2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1

capsh (מנהל מעטפת) הוא כלי המריץ מעטפת עם סט ספציפי של יכולות.

במקרה זה, כפי שכבר צוין, במקום להעניק זכויות שורש מלאות, אתה יכול לאפשר קישור פורט פריבילגי על ידי מתן יכולת cap_net_bind_service יחד עם כל השאר שכבר נמצא בתוכנית. לשם כך, אנו יכולים לצרף את התוכנית שלנו ב-capsh:

# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' 
   --keep=1 --user="nobody" 
   --addamb=cap_net_bind_service -- -c "./capabilities"

בואו נבין קצת את הצוות הזה.

  • capsh - השתמש בקאש כקליפה.
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - מכיוון שעלינו לשנות את המשתמש (איננו רוצים להפעיל כ-root), נציין את cap_net_bind_service ואת היכולת לשנות בפועל את מזהה המשתמש מ- שורש לאף אחד, כלומר cap_setuid ו-cap_setgid.
  • —keep=1 — אנחנו רוצים לשמור על היכולות המותקנות בעת מעבר מחשבון השורש.
  • —user="אף אחד" - משתמש הקצה שיפעיל את התוכנית יהיה אף אחד.
  • —addamb=cap_net_bind_service — הגדר את ניקוי היכולות הקשורות לאחר מעבר ממצב שורש.
  • - -c "./capabilities" - פשוט הפעל את התוכנית.

יכולות מקושרות הן סוג מיוחד של יכולות שעוברות בירושה על ידי תוכניות ילד כאשר התוכנית הנוכחית מבצעת אותן באמצעות execve(). רק יכולות שמותר לשייך, או במילים אחרות, כיכולות סביבה, יכולות לעבור בירושה.

אתה בטח תוהה מה המשמעות של +eip לאחר ציון היכולת באפשרות --caps. דגלים אלה משמשים כדי לקבוע שהיכולת:

-חייב להיות מופעל (p);

- זמין לשימוש (ה);

-יכול לעבור בירושה על ידי תהליכי צאצא (i).

מכיוון שאנו רוצים להשתמש ב-cap_net_bind_service, עלינו לעשות זאת עם הדגל e. לאחר מכן נתחיל את המעטפת בפקודה. זה יריץ את היכולות הבינאריות ועלינו לסמן אותה בדגל i. לבסוף, אנו רוצים שהתכונה תהיה מופעלת (עשינו זאת מבלי לשנות את ה-UID) עם p. זה נראה כמו cap_net_bind_service+eip.

אתה יכול לבדוק את התוצאה באמצעות ss. בואו נקצר מעט את הפלט כדי להתאים לדף, אבל הוא יציג את היציאה ומזהה המשתמש המשויכים מלבד 0, במקרה זה 65:

# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0

בדוגמה זו השתמשנו ב-capsh, אך ניתן לכתוב מעטפת באמצעות libcap. למידע נוסף, ראה man 3 libcap.

בעת כתיבת תוכניות, לעתים קרובות המפתח אינו יודע מראש את כל התכונות שהתוכנית צריכה בזמן הריצה; יתר על כן, תכונות אלו עשויות להשתנות בגרסאות חדשות.

כדי להבין טוב יותר את היכולות של התוכנית שלנו, אנו יכולים לקחת את הכלי בעל יכולת BCC, אשר מגדיר את kprobe עבור פונקציית הליבה cap_capable:

/usr/share/bcc/tools/capable
TIME      UID  PID   TID   COMM               CAP    NAME           AUDIT
10:12:53 0 424     424     systemd-udevd 12 CAP_NET_ADMIN         1
10:12:57 0 1103   1101   timesync        25 CAP_SYS_TIME         1
10:12:57 0 19545 19545 capabilities       10 CAP_NET_BIND_SERVICE 1

אנחנו יכולים להשיג את אותו הדבר על ידי שימוש ב-bpftrace עם kprobe חד-שנתי בפונקציית הקרנל cap_capable:

bpftrace -e 
   'kprobe:cap_capable {
      time("%H:%M:%S ");
      printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
    }' 
    | grep -i capabilities

זה יוציא משהו כמו הבא אם היכולות של התוכנית שלנו מופעלות לאחר kprobe:

12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1

העמודה החמישית היא היכולות שהתהליך צריך, ומכיוון שפלט זה כולל אירועים שאינם ביקורת, אנו רואים את כל הבדיקות שאינן ביקורת ולבסוף את היכולת הנדרשת עם דגל הביקורת (אחרון בפלט) מוגדר כ-1. יכולת. אחד שאנו מעוניינים בו הוא CAP_NET_BIND_SERVICE, הוא מוגדר כקבוע בקוד המקור של הליבה בקובץ include/uapi/linux/ability.h עם מזהה 10:

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">

היכולות מופעלות לעתים קרובות בזמן ריצה עבור קונטיינרים כגון runC או Docker כדי לאפשר להם לפעול במצב לא מוגן, אך מותרות להם רק את היכולות הדרושות להפעלת רוב היישומים. כאשר אפליקציה דורשת יכולות מסוימות, Docker יכול לספק אותן באמצעות --cap-add:

docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy

פקודה זו תעניק למכולה את יכולת CAP_NET_ADMIN, ותאפשר לו להגדיר קישור רשת להוספת ממשק dummy0.

הסעיף הבא מראה כיצד להשתמש בתכונות כגון סינון, אך באמצעות טכניקה אחרת המאפשרת לנו ליישם באופן פרוגרמטי את המסננים שלנו.

Secomp

Seccomp ראשי תיבות של Secure Computing והיא שכבת אבטחה המיושמת בליבת לינוקס המאפשרת למפתחים לסנן קריאות מערכת מסוימות. למרות ש-Secomp דומה ביכולות ללינוקס, היכולת שלה לנהל שיחות מערכת מסוימות הופכת אותה לגמישה הרבה יותר בהשוואה אליהן.

תכונות Seccomp ו-Linux אינן סותרות זו את זו ולעתים קרובות משמשות יחד כדי להפיק תועלת משתי הגישות. לדוגמה, ייתכן שתרצה לתת לתהליך את יכולת CAP_NET_ADMIN אך לא לאפשר לו לקבל חיבורי שקע, לחסום את הקבלה וקבלת קריאות המערכת.

שיטת הסינון של Seccomp מבוססת על מסנני BPF הפועלים במצב SECCOMP_MODE_FILTER, וסינון שיחות המערכת מתבצע באותו אופן כמו עבור מנות.

מסנני Seccomp נטענים באמצעות prctl דרך פעולת PR_SET_SECCOMP. מסננים אלה לובשים צורה של תוכנית BPF המופעלת עבור כל חבילת Seccomp המיוצגת על ידי מבנה seccomp_data. מבנה זה מכיל את ארכיטקטורת ההתייחסות, מצביע להוראות המעבד בזמן קריאת המערכת, ומקסימום שישה ארגומנטים של קריאת מערכת, המבוטאים כ-uint64.

כך נראה מבנה seccomp_data מקוד המקור של הליבה בקובץ linux/seccomp.h:

struct seccomp_data {
int nr;
      __u32 arch;
      __u64 instruction_pointer;
      __u64 args[6];
};

כפי שניתן לראות ממבנה זה, אנו יכולים לסנן לפי קריאת המערכת, הארגומנטים שלה או שילוב של שניהם.

לאחר קבלת כל חבילת Seccomp, המסנן חייב לבצע עיבוד כדי לקבל החלטה סופית ולומר לקרנל מה לעשות הלאה. ההחלטה הסופית מתבטאת באחד מערכי ההחזרה (קודי סטטוס).

- SECCOMP_RET_KILL_PROCESS - הורג את כל התהליך מיד לאחר סינון קריאת מערכת שלא מבוצעת בגלל זה.

- SECCOMP_RET_KILL_THREAD - מסיים את השרשור הנוכחי מיד לאחר סינון קריאת מערכת שלא מבוצעת בגלל זה.

— SECCOMP_RET_KILL — כינוי עבור SECCOMP_RET_KILL_THREAD, שמאל לתאימות לאחור.

- SECCOMP_RET_TRAP - קריאת המערכת אסורה, ואות SIGSYS (שיחת מערכת גרועה) נשלח למשימה שקוראת לה.

- SECCOMP_RET_ERRNO - קריאת המערכת לא מבוצעת, וחלק מערך החזרת המסנן SECCOMP_RET_DATA מועבר למרחב המשתמש כערך השגיאה. בהתאם לסיבת השגיאה, מוחזרים ערכי שגיאה שונים. רשימה של מספרי שגיאות מסופקת בסעיף הבא.

- SECCOMP_RET_TRACE - משמש כדי להודיע ​​ל-ptrace tracer באמצעות - PTRACE_O_TRACESECCOMP כדי ליירט כאשר מבוצעת קריאת מערכת כדי לראות ולשלוט בתהליך זה. אם מעקב לא מחובר, מוחזרת שגיאה, errno מוגדר ל-ENOSYS, וקריאה למערכת לא מבוצעת.

- SECCOMP_RET_LOG - קריאת המערכת נפתרה ונרשמת.

- SECCOMP_RET_ALLOW - קריאת המערכת פשוט מותרת.

ptrace היא קריאת מערכת להטמעת מנגנוני מעקב בתהליך הנקרא tracee, עם יכולת ניטור ובקרה על ביצוע התהליך. תוכנית המעקב יכולה להשפיע ביעילות על הביצוע ולשנות את אוגרי הזיכרון של tracee. בהקשר של Seccomp, נעשה שימוש ב-ptrace כאשר הוא מופעל על ידי קוד המצב SECCOMP_RET_TRACE, כך שה-Tracer יכול למנוע את ביצוע קריאת המערכת וליישם את ההיגיון שלה.

שגיאות Secomp

מעת לעת, תוך כדי עבודה עם Seccomp, תיתקלו בשגיאות שונות, אשר מזוהות על ידי ערך החזרה מסוג SECCOMP_RET_ERRNO. כדי לדווח על שגיאה, קריאת המערכת של seccomp תחזיר -1 במקום 0.

השגיאות הבאות אפשריות:

- EACCESS - המתקשר אינו רשאי לבצע שיחת מערכת. זה קורה בדרך כלל בגלל שאין לו הרשאות CAP_SYS_ADMIN או ש-no_new_privs לא מוגדר באמצעות prctl (נדבר על זה מאוחר יותר);

— EFAULT — לארגומנטים שעברו (args במבנה seccomp_data) אין כתובת חוקית;

- EINVAL - יכולות להיות כאן ארבע סיבות:

-הפעולה המבוקשת אינה ידועה או אינה נתמכת על ידי הליבה בתצורה הנוכחית;

-הדגלים שצוינו אינם תקפים עבור הפעולה המבוקשת;

-הפעולה כוללת BPF_ABS, אך יש בעיות עם ההיסט שצוין, שעלולות לחרוג מגודל מבנה seccomp_data;

-מספר ההוראות המועברות למסנן חורג מהמקסימום;

- ENOMEM - אין מספיק זיכרון כדי להפעיל את התוכנית;

- EOPNOTSUPP - הפעולה ציינה שעם SECCOMP_GET_ACTION_AVAIL הפעולה הייתה זמינה, אך הקרנל אינו תומך בהחזרות בארגומנטים;

- ESRCH - התרחשה בעיה בעת סנכרון זרם אחר;

- ENOSYS - אין מעקב מצורף לפעולת SECCOMP_RET_TRACE.

prctl היא קריאת מערכת המאפשרת לתוכנית מרחב משתמש לתפעל (להגדיר ולהשיג) היבטים ספציפיים של תהליך, כמו סוף בתים, שמות שרשורים, מצב חישוב מאובטח (Seccomp), הרשאות, אירועי Perf וכו'.

Secomp אולי נראית לך כמו טכנולוגיית ארגז חול, אבל היא לא. Seccomp הוא כלי עזר המאפשר למשתמשים לפתח מנגנון ארגז חול. כעת בואו נסתכל כיצד נוצרות תוכניות אינטראקציה עם משתמשים באמצעות מסנן הנקרא ישירות על ידי קריאת המערכת של Seccomp.

דוגמה למסנן BPF Secomp

כאן נראה כיצד לשלב את שתי הפעולות שנדונו קודם לכן, כלומר:

- נכתוב תוכנית Seccomp BPF, שתשמש כמסנן עם קודי החזרה שונים בהתאם להחלטות שהתקבלו;

- טען את המסנן באמצעות prctl.

ראשית אתה צריך כותרות מהספרייה הרגילה ומקרנל לינוקס:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

לפני שננסה את הדוגמה הזו, עלינו לוודא שהקרנל הידור עם CONFIG_SECCOMP ו-CONFIG_SECCOMP_FILTER מוגדרים ל-y. במכונה עובדת אתה יכול לבדוק את זה כך:

cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP

שאר הקוד הוא פונקציית install_filter בת שני חלקים. החלק הראשון מכיל את רשימת הוראות סינון BPF שלנו:

static int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
  };

ההוראות מוגדרות באמצעות פקודות המאקרו BPF_STMT ו-BPF_JUMP המוגדרות בקובץ linux/filter.h.
בוא נעבור על ההוראות.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) - המערכת נטענת ומצטברת מ-BPF_LD בצורה של המילה BPF_W, נתוני מנות ממוקמים בהיסט קבוע BPF_ABS.

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - בודק באמצעות BPF_JEQ האם ערך הארכיטקטורה בקבוע מצבר BPF_K שווה לקשת. אם כן, קופץ בהיסט 0 להוראה הבאה, אחרת קופץ בהיסט 3 (במקרה זה) כדי לזרוק שגיאה כי הקשת לא תואמת.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - טוען ומצטבר מ-BPF_LD בצורה של המילה BPF_W, שהיא מספר השיחה של המערכת הכלול בקיזוז הקבוע של BPF_ABS.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — משווה את מספר השיחה של המערכת עם הערך של משתנה nr. אם הם שווים, עובר להוראה הבאה ומשבית את קריאת המערכת, אחרת מאפשר את קריאת המערכת עם SECCOMP_RET_ALLOW.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) - מסיים את התוכנית עם BPF_RET וכתוצאה מכך מייצר שגיאה SECCOMP_RET_ERRNO עם המספר ממשתנה השגיאה.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - מסיים את התוכנית עם BPF_RET ומאפשר ביצוע קריאת המערכת באמצעות SECCOMP_RET_ALLOW.

SECCOMP הוא CBPF
אולי אתה תוהה מדוע נעשה שימוש ברשימת הוראות במקום אובייקט ELF הידור או תוכנית C הידורית של JIT.

יש לכך שתי סיבות.

• ראשית, Seccomp משתמש ב-cBPF (BPF קלאסי) ולא ב-eBPF, כלומר: אין לו אוגרים, אלא רק מצבר לאחסון תוצאת החישוב האחרונה, כפי שניתן לראות בדוגמה.

שנית, Seccomp מקבל מצביע למערך של הוראות BPF ישירות ותו לא. פקודות המאקרו בהן השתמשנו פשוט עוזרות לציין הוראות אלה בצורה ידידותית למתכנת.

אם אתה זקוק לעזרה נוספת בהבנת המכלול הזה, שקול את הפסאודוקוד שעושה את אותו הדבר:

if (arch != AUDIT_ARCH_X86_64) {
    return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
    return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;

לאחר הגדרת קוד המסנן במבנה socket_filter, עליך להגדיר sock_fprog המכיל את הקוד ואת האורך המחושב של המסנן. מבנה נתונים זה נחוץ כטיעון להכרזה על הפעלת התהליך מאוחר יותר:

struct sock_fprog prog = {
   .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
   .filter = filter,
};

נותר רק דבר אחד לעשות בפונקציית install_filter - טען את התוכנית עצמה! לשם כך, אנו משתמשים ב-prctl, תוך שימוש ב-PR_SET_SECCOMP כאפשרות לכניסה למצב מחשוב מאובטח. לאחר מכן אנו אומרים למצב לטעון את המסנן באמצעות SECCOMP_MODE_FILTER, הכלול במשתנה הפרוג מסוג sock_fprog:

  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

לבסוף, נוכל להשתמש בפונקציית install_filter שלנו, אך לפני כן עלינו להשתמש ב-prctl כדי להגדיר PR_SET_NO_NEW_PRIVS לביצוע הנוכחי ועל ידי כך להימנע מהמצב שבו תהליכי ילד מקבלים יותר הרשאות מהוריהם. בעזרת זה, נוכל לבצע את הקריאות prctl הבאות בפונקציית install_filter ללא זכויות שורש.

כעת נוכל לקרוא לפונקציה install_filter. בואו נחסום את כל קריאות הכתיבה הקשורות לארכיטקטורת X86-64 ופשוט ניתן הרשאה שחוסמת את כל הניסיונות. לאחר התקנת המסנן, אנו ממשיכים בביצוע באמצעות הארגומנט הראשון:

int main(int argc, char const *argv[]) {
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
   perror("prctl(NO_NEW_PRIVS)");
   return 1;
  }
   install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
  return system(argv[1]);
 }

בואו נתחיל. כדי להרכיב את התוכנית שלנו אנחנו יכולים להשתמש ב-clang או gcc, כך או כך זה פשוט קומפילציה של הקובץ main.c ללא אפשרויות מיוחדות:

clang main.c -o filter-write

כאמור, חסמנו את כל הערכים בתוכנית. כדי לבדוק את זה אתה צריך תוכנית שמוציאה משהו - זה נראה כמו מועמד טוב. כך היא מתנהגת בדרך כלל:

ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c

נִפלָא! כך נראה השימוש בתוכנית המעטפת שלנו: אנו פשוט עוברים את התוכנית שאנו רוצים לבדוק כארגומנט הראשון:

./filter-write "ls -la"

כאשר מופעלת, תוכנית זו מייצרת פלט ריק לחלוטין. עם זאת, אנו יכולים להשתמש ב-strace כדי לראות מה קורה:

strace -f ./filter-write "ls -la"

תוצאת העבודה מתקצרת מאוד, אבל החלק המקביל שלה מראה שהרשומות חסומות בשגיאת EPERM - אותה אחת שהגדרנו. זה אומר שהתוכנה לא מפלטת שום דבר מכיוון שהיא לא יכולה לגשת לקריאת המערכת לכתוב:

[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation not permitted)

עכשיו אתה מבין איך Seccomp BPF עובד ויש לך מושג טוב מה אתה יכול לעשות איתו. אבל האם לא היית רוצה להשיג את אותו הדבר עם eBPF במקום cBPF כדי לרתום את מלוא הכוח שלו?

כשחושבים על תוכניות eBPF, רוב האנשים חושבים שהם פשוט כותבים אותן ומטעינים אותן עם הרשאות מנהל. בעוד שהצהרה זו נכונה בדרך כלל, הליבה מיישמת קבוצה של מנגנונים להגנה על אובייקטי eBPF ברמות שונות. מנגנונים אלו נקראים מלכודות BPF LSM.

מלכודות BPF LSM

כדי לספק ניטור בלתי תלוי בארכיטקטורה של אירועי מערכת, LSM מיישמת את הרעיון של מלכודות. שיחת Hook דומה מבחינה טכנית לשיחת מערכת, אך היא עצמאית במערכת ומשולבת עם התשתית. LSM מספק תפיסה חדשה שבה שכבת הפשטה יכולה לעזור למנוע בעיות שנתקלות בהן בעת ​​התמודדות עם קריאות מערכת בארכיטקטורות שונות.

בזמן הכתיבה, לקרנל יש שבעה הוקים הקשורים לתוכניות BPF, ו-SELinux הוא ה-LSM המובנה היחיד שמיישם אותם.

קוד המקור של המלכודות נמצא בעץ הקרנל בקובץ include/linux/security.h:

extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);

כל אחד מהם ייקרא בשלבים שונים של ביצוע:

— security_bpf — מבצע בדיקה ראשונית של קריאות מערכת BPF שבוצעו;

- security_bpf_map - בודק מתי הקרנל מחזיר מתאר קובץ עבור המפה;

- security_bpf_prog - בודק מתי הקרנל מחזיר מתאר קובץ עבור תוכנית eBPF;

— security_bpf_map_alloc — בודק אם שדה האבטחה בתוך מפות BPF מאותחל;

- security_bpf_map_free - בודק אם שדה האבטחה מנוקה בתוך מפות BPF;

— security_bpf_prog_alloc — בודק אם שדה האבטחה מאותחל בתוך תוכניות BPF;

- security_bpf_prog_free - בודק אם שדה האבטחה מנוקה בתוך תוכניות BPF.

כעת, כשראינו את כל זה, אנו מבינים: הרעיון מאחורי מיירטי LSM BPF הוא שהם יכולים לספק הגנה לכל אובייקט eBPF, ולהבטיח שרק בעלי ההרשאות המתאימות יוכלו לבצע פעולות בכרטיסים ובתוכניות.

תקציר

אבטחה היא לא משהו שאתה יכול ליישם בצורה חד משמעית לכל מה שאתה רוצה להגן עליו. חשוב להיות מסוגל להגן על מערכות ברמות שונות ובדרכים שונות. תאמינו או לא, הדרך הטובה ביותר לאבטח מערכת היא לארגן רמות שונות של הגנה מתפקידים שונים, כך שהפחתת האבטחה של רמה אחת אינה מאפשרת גישה לכל המערכת. מפתחי הליבה עשו עבודה מצוינת לתת לנו קבוצה של שכבות ונקודות מגע שונות. אנו מקווים שנתנו לך הבנה טובה של מהן שכבות וכיצד להשתמש בתוכניות BPF כדי לעבוד איתן.

על המחברים

דיוויד קלאוורה הוא ה-CTO ב-Netlify. הוא עבד בתמיכת Docker ותרם לפיתוח כלי Runc, Go ו-BCC, כמו גם פרויקטים אחרים של קוד פתוח. ידוע בעבודתו על פרויקטים של Docker ופיתוח של מערכת האקולוגית של התוסף Docker. דיוויד מאוד נלהב מגרפי להבה ותמיד מחפש לייעל את הביצועים.

לורנצו פונטנה עובד בצוות הקוד הפתוח ב-Sysdig, שם הוא מתמקד בעיקר ב-Falco, פרויקט Cloud Native Computing Foundation המספק אבטחת זמן ריצה של מיכל וזיהוי אנומליות באמצעות מודול ליבה ו-eBPF. הוא נלהב ממערכות מבוזרות, רשתות מוגדרות תוכנה, ליבת לינוקס וניתוח ביצועים.

» פרטים נוספים על הספר ניתן למצוא בכתובת אתר האינטרנט של ההוצאה לאור
» תוכן העניינים
» קטע

עבור Khabrozhiteley 25% הנחה באמצעות קופון - לינוקס

בתשלום גרסת הנייר של הספר ישלח ספר אלקטרוני במייל.

מקור: www.habr.com

הוספת תגובה