2. Tworzenie zasady piaskownicy
Gdy będziesz mieć wykonawcę, prawdopodobnie zechcesz zdefiniować zasady Piaskownicy dla procesu podrzędnego. W przeciwnym razie Sandboxee jest chroniony tylko przez domyślne zasady wywołań systemowych.
Zasady piaskownicy mają na celu ograniczenie wywołań systemowych i argumentów, które może wykonywać proces podrzędny, 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ć klasy PolicyBuilder, aby zdefiniować zasadę. PolicyBuilder zawiera wiele przydatnych funkcji pomocniczych, które umożliwiają wykonywanie wielu typowych operacji. Poniższa lista to tylko niewielki fragment dostępnych funkcji:
- Umieszczanie na liście dozwolonych dowolnego wywołania systemowego podczas uruchamiania procesu:
AllowStaticStartup();
AllowDynamicStartup();
- Lista dozwolonych dla wszystkich wywołań systemowych open/read/write*:
AllowOpen();
AllowRead();
AllowWrite();
- Dodaj do listy dozwolonych wszystkie wywołania systemowe związane z wyjściem, dostępem lub stanem:
AllowExit();
AllowStat();
AllowAccess();
- Dodaj do listy dozwolonych wszystkie wywołania systemowe związane z uśpieniem lub czasem:
AllowTime();
AllowSleep();
Te funkcje ułatwiające pracę 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łącza więcej wywołań systemowych, niż może być konieczne. Na przykład funkcja AllowOpen() umożliwia procesowi Sandboxee wywoływanie dowolnego powiązanego wywołania systemowego open. Jeśli chcesz dodać do listy dozwolonych tylko jeden konkretny wywołanie systemowe, możesz użyć AllowSyscall();
. Aby dodać do listy dozwolonych kilka wywołań systemowych jednocześnie, możesz użyć AllowSyscalls()
.
Obecnie zasada sprawdza tylko identyfikator wywołania systemowego. Jeśli chcesz jeszcze bardziej wzmocnić zasady i zdefiniować zasady, w których zezwalasz tylko na wywołanie systemowe z określonymi argumentami, musisz użyć AddPolicyOnSyscall()
lub AddPolicyOnSyscalls()
. Funkcje te przyjmują jako argument nie tylko identyfikator wywołania systemowego, ale też surowy filtr seccomp-bpf, który korzysta z makr pomocniczych bpf z jądra systemu Linux. Więcej informacji o BPF znajdziesz w dokumentacji jądra. Jeśli piszesz powtarzalny kod BPF, który Twoim zdaniem powinien mieć otoczkę ułatwiającą korzystanie, możesz przesłać prośbę o dodanie takiej funkcji.
Oprócz funkcji związanych z wywołaniami systemowymi PolicyBuilder udostępnia też szereg funkcji związanych z systemem plików, takich jak AddFile()
czy AddDirectory()
, które umożliwiają zamontowanie pliku lub katalogu w piaskownicy. Za pomocą funkcji AddTmpfs()
możesz dodać tymczasowe miejsce na pliki w piaskownicy.
Szczególnie przydatna jest funkcja AddLibrariesForBinary()
, która dodaje biblioteki i linker wymagane przez plik binarny.
Określenie wywołań systemowych, które mają być dodane do listy dozwolonych, wymaga niestety trochę pracy ręcznej. Utwórz zasady z wywołaniami systemowymi, których potrzebuje Twój plik binarny, i uruchom je 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 w przypadku dodania do listy dozwolonych, a program dobrze radzi sobie z błędami, możesz spróbować spowodować, aby zamiast tego zwracał błąd, używając BlockSyscallWithErrno()
.
#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();
}