2. יצירת מדיניות של ארגז חול (sandbox)

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

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

אחרי שיוצרים את רשימת הקריאות למערכת, אפשר להשתמש ב-PolicyBuilder כדי להגדיר את המדיניות.

‫PolicyBuilder כולל הרבה פונקציות נוחות ופונקציות עזר שמאפשרות לבצע הרבה פעולות נפוצות (רבות מהן כבר כלולות ב-DefaultPolicyBuilder()). הרשימה הבאה היא רק קטע קטן מתוך הפונקציות הזמינות:

  • הוספה לרשימת ההיתרים של כל קריאת מערכת להפעלת תהליך:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • מוסיפים לרשימת ההיתרים את כל קריאות המערכת הפתוחות/לקריאה/לכתיבה*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • הוספה לרשימת ההיתרים של כל קריאות המערכת שקשורות ליציאה, לגישה או למצב:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • מוסיפים לרשימת ההיתרים קריאות מערכת שקשורות לשינה או לזמן:
    • AllowTime();
    • AllowSleep();

הפונקציות האלה מאפשרות להוסיף לרשימת ההיתרים כל קריאת מערכת רלוונטית. היתרון הוא שאפשר להשתמש באותה מדיניות בארכיטקטורות שונות שבהן קריאות מערכת מסוימות לא זמינות (למשל, ב-ARM64 אין קריאת מערכת OPEN), אבל יש סיכון אבטחה קטן יותר של הפעלת יותר קריאות מערכת ממה שנדרש. לדוגמה, AllowOpen()‎ מאפשרת ל-Sandboxee לקרוא לכל קריאת מערכת שקשורה לפתיחה. אם רוצים להוסיף לרשימת ההיתרים רק קריאת מערכת ספציפית אחת, אפשר להשתמש ב-AllowSyscall();. כדי לאפשר כמה קריאות מערכת בבת אחת, אפשר להשתמש ב-AllowSyscalls().

בשלב הזה, המדיניות בודקת רק את המזהה של syscall. אם אתם רוצים לחזק עוד יותר את המדיניות ולהגדיר מדיניות שבה אתם מאפשרים רק קריאת מערכת עם ארגומנטים מסוימים, אתם צריכים להשתמש ב-AddPolicyOnSyscall() או ב-AddPolicyOnSyscalls(). הפונקציות האלה מקבלות כארגומנט לא רק את מזהה קריאת המערכת, אלא גם מסנן seccomp-bpf גולמי באמצעות פקודות מאקרו של bpf helper מליבת לינוקס. למידע נוסף על BPF, אפשר לעיין במסמכי התיעוד של הליבה. אם אתם כותבים קוד BPF שחוזר על עצמו, ולדעתכם צריך להיות לו wrapper של שימושיות, אתם יכולים להגיש בקשה להוספת תכונה.

בנוסף לפונקציות שקשורות ל-syscall, ‏PolicyBuilder מספק גם כמה פונקציות שקשורות למערכת הקבצים, כמו AddFile() או AddDirectory(), כדי לבצע bind-mount של קובץ או ספרייה בארגז החול. אפשר להשתמש בכלי העזר AddTmpfs() כדי להוסיף אחסון קבצים זמני בתוך ארגז החול.

פונקציה שימושית במיוחד היא AddLibrariesForBinary(), שמוסיפה את הספריות והמקשר (linker) שנדרשים לקובץ בינארי.

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

בדוגמה הבאה, האילוץ sandbox2::PolicyBuilder נוצר ישירות כדי להדגים הגבלה של הארגומנטים write ו-mprotect וחסימה של openat (שאחרת DefaultPolicyBuilder() היה מאפשר):

#include "sandboxed_api/sandbox2/policy.h"
#include "sandboxed_api/sandbox2/policybuilder.h"
#include "sandboxed_api/sandbox2/util/bpf_helper.h"

std::unique_ptr<sandbox2::Policy> CreatePolicy() {
  return sandbox2::PolicyBuilder()
    .AllowSyscall(__NR_read)  // See also AllowRead()
    .AllowTime()              // Allow time, gettimeofday and clock_gettime
    .AddPolicyOnSyscall(__NR_write, {
        ARG(0),        // fd is the first argument of write (argument #0)
        JEQ(1, ALLOW), // allow write only on fd 1
        KILL,          // kill if not fd 1
    })
    .AddPolicyOnSyscall(__NR_mprotect, {
        ARG_32(2), // prot is a 32-bit wide argument, so it's OK to use *_32
                   // macro here
        JNE32(PROT_READ | PROT_WRITE, KILL), // prot must be the RW, otherwise
                                             // kill the process
        ARG(1), // len is a 64-bit argument
        JNE(0x1000, KILL),  // Allow single page syscalls only, otherwise kill
                            // the process
        ALLOW,              // Allow for the syscall to proceed, if prot and
                            // size match
    })
    // Allow the openat() syscall but always return "not found".
    .BlockSyscallWithErrno(__NR_openat, ENOENT)
    .BuildOrDie();
}