2. Sandbox-Richtlinie erstellen
Sobald Sie einen Executor haben, sollten Sie wahrscheinlich eine Sandbox-Richtlinie für das Sandboxee definieren. Andernfalls ist die Sandboxee nur durch die Standard-Syscall-Richtlinie geschützt.
Mit der Sandbox-Richtlinie soll eingeschränkt werden, welche Systemaufrufe und Argumente der Sandboxee ausführen kann und auf welche Dateien sie zugreifen kann. Sie müssen die vom Code, den Sie in einer Sandbox ausführen möchten, benötigten Systemaufrufe genau kennen. Eine Möglichkeit, Systemaufrufe zu beobachten, besteht darin, den Code mit dem Linux-Befehlszeilentool „strace“ auszuführen.
Sobald Sie die Liste der Systemaufrufe haben, können Sie die Richtlinie mit PolicyBuilder definieren. PolicyBuilder bietet viele praktische Funktionen und Hilfsfunktionen, die viele gängige Vorgänge ermöglichen. Die folgende Liste ist nur ein kleiner Auszug der verfügbaren Funktionen:
- Alle Systemaufrufe für den Prozessstart auf die Zulassungsliste setzen:
AllowStaticStartup();
AllowDynamicStartup();
- Zulassungsliste für alle offenen/read/write*-Systemaufrufe:
AllowOpen();
AllowRead();
AllowWrite();
- Alle Syscalls im Zusammenhang mit Beenden, Zugriff und Status auf die Zulassungsliste setzen:
AllowExit();
AllowStat();
AllowAccess();
- Zulassungsliste für alle schlaf- und zeitbezogenen Systemaufrufe:
AllowTime();
AllowSleep();
Diese Hilfsfunktionen fügen alle relevanten Syscalls auf die Zulassungsliste. Das hat den Vorteil, dass dieselbe Richtlinie für verschiedene Architekturen verwendet werden kann, in denen bestimmte Systemaufrufe nicht verfügbar sind (z. B. hat ARM64 keinen OPEN-Systemaufruf). Es besteht jedoch das geringe Sicherheitsrisiko, dass mehr Systemaufrufe als nötig aktiviert werden. Mit AllowOpen() kann der Sandboxee beispielsweise jeden Open-bezogenen Systemaufruf aufrufen. Wenn Sie nur einen bestimmten Systemaufruf auf die Zulassungsliste setzen möchten, können Sie AllowSyscall();
verwenden. Wenn Sie mehrere Systemaufrufe gleichzeitig zulassen möchten, können Sie AllowSyscalls()
verwenden.
Bisher wird in der Richtlinie nur die Syscall-Kennung geprüft. Wenn Sie die Richtlinie weiter verschärfen und eine Richtlinie definieren möchten, in der Sie nur einen Systemaufruf mit bestimmten Argumenten zulassen, müssen Sie AddPolicyOnSyscall()
oder AddPolicyOnSyscalls()
verwenden. Diese Funktionen verwenden nicht nur die Syscall-ID als Argument, sondern auch einen rohen seccomp-bpf-Filter mit den bpf-Hilfsmakros aus dem Linux-Kernel. Weitere Informationen zu BPF finden Sie in der Kernel-Dokumentation. Wenn Sie wiederholt BPF-Code schreiben, für den Sie sich einen Wrapper wünschen, können Sie gerne einen Funktionsantrag stellen.
Neben syscall-bezogenen Funktionen bietet der PolicyBuilder auch eine Reihe von dateisystembezogenen Funktionen wie AddFile()
oder AddDirectory()
, um eine Datei oder ein Verzeichnis in die Sandbox einzubinden. Mit dem AddTmpfs()
-Helfer können Sie einen temporären Dateispeicher in der Sandbox hinzufügen.
Eine besonders nützliche Funktion ist AddLibrariesForBinary()
, mit der die für eine Binärdatei erforderlichen Bibliotheken und der Linker hinzugefügt werden.
Das Erstellen der Zulassungsliste für die Syscalls erfordert leider immer noch etwas manuellen Aufwand. Erstellen Sie eine Richtlinie mit den Systemaufrufen, die Ihr Binärprogramm benötigt, und führen Sie sie mit einer gängigen Arbeitslast aus. Wenn ein Verstoß ausgelöst wird, fügen Sie den Systemaufruf auf die Zulassungsliste und wiederholen Sie den Vorgang. Wenn Sie auf einen Verstoß stoßen, der Ihrer Meinung nach riskant für die Zulassungsliste ist, und das Programm Fehler ordnungsgemäß behandelt, können Sie versuchen, mit BlockSyscallWithErrno()
einen Fehler zurückzugeben.
#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();
}