2. 샌드박스 정책 만들기

실행기가 있으면 Sandboxee의 샌드박스 정책을 정의하는 것이 좋습니다. 그렇지 않으면 샌드박스만 기본 시스템 호출 정책으로 보호됩니다.

샌드박스 정책의 목표는 샌드박스 대상이 만들 수 있는 시스템 호출과 인수, 액세스할 수 있는 파일을 제한하는 것입니다. 샌드박스로 처리할 코드에 필요한 시스템 호출을 자세히 이해해야 합니다. 시스템 호출을 관찰하는 한 가지 방법은 Linux의 명령줄 도구인 strace로 코드를 실행하는 것입니다.

시스템 호출 목록이 있으면 PolicyBuilder를 사용하여 정책을 정의할 수 있습니다. PolicyBuilder에는 많은 일반적인 작업을 허용하는 다양한 편의 기능과 도우미 기능이 함께 제공됩니다. 다음 목록은 사용 가능한 함수의 일부에 불과합니다.

  • 프로세스 시작을 위해 시스템 호출을 허용 목록에 추가합니다.
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • 열기/읽기/쓰기* syscall을 허용 목록에 추가합니다.
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • 종료/액세스/상태 관련 시스템 호출을 허용 목록에 추가합니다.
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • 절전 모드/시간 관련 시스템 호출을 허용 목록에 추가합니다.
    • AllowTime();
    • AllowSleep();

이러한 편의 함수는 관련 syscall을 허용 목록에 추가합니다. 이렇게 하면 특정 syscall을 사용할 수 없는 다양한 아키텍처에서 동일한 정책을 사용할 수 있다는 장점이 있지만 필요 이상으로 많은 syscall을 사용 설정하는 사소한 보안 위험이 있습니다 (예: ARM64에는 OPEN syscall이 없음). 예를 들어 AllowOpen()을 사용하면 샌드박스에서 열기 관련 syscall을 호출할 수 있습니다. 특정 시스템 호출 하나만 허용 목록에 추가하려면 AllowSyscall();를 사용하고 한 번에 여러 시스템 호출을 허용하려면 AllowSyscalls()를 사용하면 됩니다.

지금까지 정책은 시스템 호출 식별자만 확인합니다. 정책을 더욱 강화해야 하고 특정 인수가 있는 시스템 호출만 허용하는 정책을 정의하려면 AddPolicyOnSyscall() 또는 AddPolicyOnSyscalls()를 사용해야 합니다. 이러한 함수는 시스템 호출 ID를 인수로 사용할 뿐만 아니라 Linux 커널의 bpf 도우미 매크로를 사용하여 원시 seccomp-bpf 필터도 사용합니다. BPF에 관한 자세한 내용은 커널 문서를 참고하세요. 유용성 래퍼가 있어야 한다고 생각되는 반복적인 BPF 코드를 작성하는 경우 기능 요청을 제출하세요.

syscall 관련 함수 외에도 PolicyBuilder는 샌드박스에 파일/디렉터리를 바인드 마운트하는 AddFile() 또는 AddDirectory()과 같은 여러 파일 시스템 관련 함수를 제공합니다. AddTmpfs() 도우미를 사용하여 샌드박스 내에 임시 파일 저장소를 추가할 수 있습니다.

특히 유용한 함수는 바이너리에 필요한 라이브러리와 링커를 추가하는 AddLibrariesForBinary()입니다.

허용 목록에 추가할 시스템 호출을 파악하는 작업은 아직 수동으로 해야 합니다. 바이너리에 필요한 시스템 호출을 사용하여 정책을 만들고 일반적인 워크로드와 함께 실행합니다. 위반이 트리거되면 시스템 호출을 허용 목록에 추가하고 프로세스를 반복합니다. 허용 목록에 추가하는 것이 위험할 수 있다고 생각되는 위반이 발생하고 프로그램에서 오류를 적절하게 처리하는 경우 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();
}