2. Crea una política de zona de pruebas

Una vez que tengas un ejecutor, probablemente te convenga definir una política de la zona de pruebas para la zona de pruebas. De lo contrario, la zona de pruebas solo estará protegida por la política de Syscall predeterminada.

Con la política de la zona de pruebas, el objetivo es restringir las llamadas de sistema y los argumentos que puede crear la zona de pruebas, así como los archivos a los que puede acceder. Deberás tener una comprensión detallada de las llamadas del sistema que requiere el código que planeas usar en la zona de pruebas. Una forma de observar las llamadas del sistema es ejecutar el código con strace de la herramienta de línea de comandos de Linux.

Una vez que tengas la lista de llamadas de sistema, podrás usar PolicyBuilder para definir la política. PolicyBuilder incluye muchas funciones convenientes y auxiliares que permiten realizar muchas operaciones comunes. La siguiente lista es solo un pequeño extracto de las funciones disponibles:

  • Agrega a la lista de entidades permitidas cualquier llamada del sistema para el inicio del proceso:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Agrega a la lista de entidades permitidas cualquier llamada de sistema /read/write* abierta:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Agrega a la lista de entidades permitidas cualquier llamada de sistema relacionada con el estado, el acceso o la salida:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • Agrega a la lista de entidades permitidas todas las llamadas de sistema relacionadas con el tiempo o el sueño:
    • AllowTime();
    • AllowSleep();

Estas funciones convenientes incluyen en la lista de entidades permitidas todas las llamadas de sistema relevantes. Esto tiene la ventaja de que se puede usar la misma política en arquitecturas diferentes en las que no están disponibles ciertas llamadas del sistema (p.ej., ARM64 no tiene una llamada de sistema ABIERTA), pero con el menor riesgo de seguridad de habilitar más sistemas de los que podrían ser necesarios. Por ejemplo, AllowOpen() permite que Sandboxee llame a cualquier llamada de sistema relacionada abierta. Si solo quieres incluir en la lista de entidades permitidas una llamada del sistema específica, puedes usar AllowSyscall(); para permitir varias llamadas del sistema a la vez y usar AllowSyscalls().

Hasta ahora, la política solo verifica el identificador de llamada del sistema. Si necesitas reforzar aún más la política y quieres definir una política en la que solo permitas una llamada de sistema con argumentos específicos, debes usar AddPolicyOnSyscall() o AddPolicyOnSyscalls(). Estas funciones no solo toman el ID de syscall como argumento, sino también un filtro seccomp-bpf sin procesar usando las macros auxiliares bpf del kernel de Linux. Consulta la documentación del kernel para obtener más información sobre BPF. Si notas que escribes código BPF repetitivo que crees que debería tener un wrapper de usabilidad, puedes enviar una solicitud de función.

Además de las funciones relacionadas con llamadas de sistema, PolicyBuilder también proporciona varias funciones relacionadas con el sistema de archivos, como AddFile() o AddDirectory(), para vincular y activar un archivo o directorio en la zona de pruebas. Se puede usar el ayudante AddTmpfs() para agregar almacenamiento temporal de archivos dentro de la zona de pruebas.

Una función particularmente útil es AddLibrariesForBinary(), que agrega las bibliotecas y el vinculador que requiere un objeto binario.

Lamentablemente, idear las llamadas de sistema para incluirlas en la lista de entidades permitidas sigue siendo un poco de trabajo manual. Crea una política con las llamadas de sistema que conoces para tus necesidades binarias y ejecútala con una carga de trabajo común. Si se activa un incumplimiento, incluye la llamada del sistema en la lista de entidades permitidas y repite el proceso. Si encuentras un incumplimiento que crees que podría ser riesgoso incluirlo en la lista de entidades permitidas y el programa maneja los errores correctamente, puedes intentar que se muestre un error con 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();
}