2. Tworzenie zasady dotyczącej piaskownicy

Gdy masz już wykonawcę, prawdopodobnie zechcesz zdefiniować zasadę dotyczącą piaskownicy Policy dla piaskownicy. W przeciwnym razie piaskownica jest chroniona tylko przez domyślną zasadę dotyczącą wywołań systemowych.

Zasada dotycząca piaskownicy ma na celu ograniczenie wywołań systemowych i argumentów, które może wykonywać piaskownica, a także plików, do których może mieć dostęp. Musisz dokładnie znać wywołania systemowe wymagane przez kod, który chcesz umieścić w piaskownicy. Jednym ze sposobów obserwowania wywołań systemowych jest uruchomienie kodu za pomocą narzędzia wiersza poleceń strace w systemie Linux.

Gdy masz już listę wywołań systemowych, możesz użyć narzędzia PolicyBuilder aby zdefiniować zasadę.

PolicyBuilder zawiera wiele wygodnych funkcji pomocniczych, które umożliwiają wykonywanie wielu typowych operacji (wiele z nich jest już uwzględnionych w DefaultPolicyBuilder()). Poniższa lista to tylko niewielki fragment dostępnych funkcji:

  • Dodawanie do listy dozwolonych dowolnego wywołania systemowego na potrzeby uruchamiania procesu:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Dodawanie do listy dozwolonych dowolnych wywołań systemowych open/read/write*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Dodawanie do listy dozwolonych dowolnych wywołań systemowych związanych z wyjściem, dostępem lub stanem:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • Dodawanie do listy dozwolonych dowolnych wywołań systemowych związanych z uśpieniem lub czasem:
    • AllowTime();
    • AllowSleep();

Te wygodne funkcje dodają do listy dozwolonych wszystkie odpowiednie wywołania systemowe. Ma to tę zaletę, że tej samej zasady można używać w różnych architekturach, w których niektóre wywołania systemowe są niedostępne (np. ARM64 nie ma wywołania systemowego OPEN), ale wiąże się to z niewielkim ryzykiem związanym z bezpieczeństwem, ponieważ włączonych jest więcej wywołań systemowych niż to konieczne. Na przykład AllowOpen() umożliwia piaskownicy wywoływanie dowolnego wywołania systemowego związanego z otwieraniem. Jeśli chcesz dodać do listy dozwolonych tylko 1 konkretne wywołanie systemowe, możesz użyć AllowSyscall();, a jeśli chcesz dodać do listy dozwolonych kilka wywołań systemowych naraz, możesz użyć AllowSyscalls().

Do tej pory zasada sprawdza tylko identyfikator wywołania systemowego. Jeśli chcesz dodatkowo wzmocnić zasadę i zdefiniować zasadę, w której zezwalasz tylko na wywołanie systemowe z określonymi argumentami, musisz użyć AddPolicyOnSyscall() lub AddPolicyOnSyscalls(). Te funkcje przyjmują jako argument nie tylko identyfikator wywołania systemowego, ale też surowy filtr seccomp-bpf używający makr pomocniczych bpf z jądra systemu Linux. Więcej informacji o BPF znajdziesz w dokumentacji jądra. Jeśli zauważysz, że piszesz powtarzający się kod BPF, który Twoim zdaniem powinien mieć otoczkę ułatwiającą użycie, prześlij prośbę o dodanie funkcji.

Oprócz funkcji związanych z wywołaniami systemowymi narzędzie PolicyBuilder udostępnia też szereg funkcji związanych z systemem plików, takich jak AddFile() czy AddDirectory(), które umożliwiają montowanie pliku lub katalogu w piaskownicy. Funkcja pomocnicza AddTmpfs() umożliwia dodanie tymczasowego miejsca na pliki w piaskownicy.

Szczególnie przydatna jest funkcja AddLibrariesForBinary(), która dodaje biblioteki i linker wymagane przez plik binarny.

Niestety, wymyślanie wywołań systemowych, które należy dodać do listy dozwolonych, nadal wymaga trochę pracy ręcznej. Utwórz zasadę z wywołaniami systemowymi, które Twoim zdaniem są potrzebne plikowi binarnemu, i uruchom ją z typowym zbiorem zadań. Jeśli zostanie wykryte naruszenie, dodaj wywołanie systemowe do listy dozwolonych i powtórz proces. Jeśli napotkasz naruszenie, które Twoim zdaniem może być ryzykowne, aby dodać je do listy dozwolonych, a program obsługuje błędy w sposób prawidłowy, możesz spróbować zwrócić błąd za pomocą BlockSyscallWithErrno().

Poniższy przykład bezpośrednio tworzy sandbox2::PolicyBuilder, aby zademonstrować ograniczenie argumentów write i mprotect oraz zablokowanie openat (które w przeciwnym razie DefaultPolicyBuilder() by zezwoliło):

#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();
}