Начало работы с Sandbox2

На этой странице вы узнаете, как создать собственную изолированную среду с помощью Sandbox2. Вы узнаете, как определить политику песочницы и некоторые сложные, но распространенные настройки. Используйте приведенную здесь информацию в качестве руководства вместе с примерами и документацией по коду в заголовочных файлах.

1. Выберите метод исполнения песочницы

Песочница начинается с исполнителя (см. Sandbox Executor ), который отвечает за запуск Sandboxee . Заголовочный файл executor.h содержит API, необходимый для этой цели. API очень гибкий и позволяет вам выбрать то, что лучше всего подходит для вашего случая использования. В следующих разделах описаны 3 различных метода, из которых вы можете выбрать.

Способ 1: Автономный: выполнить двоичный файл с уже включенной песочницей.

Это самый простой способ использования песочницы, и его рекомендуется использовать, если вы хотите поместить в песочницу весь двоичный файл, для которого у вас нет исходного кода. Это также самый безопасный способ использования песочницы, поскольку не существует инициализации без изолированной программной среды, которая могла бы иметь неблагоприятные последствия.

В приведенном ниже фрагменте кода мы определяем путь к двоичному файлу, который будет помещен в песочницу, и аргументы, которые мы должны передать системному вызову execve. Как вы можете видеть в заголовочном файле executor.h , мы не указываем значение для envp и поэтому копируем среду из родительского процесса. Помните, что первым аргументом всегда является имя выполняемой программы, и наш фрагмент не определяет никаких других аргументов.

Примерами этого метода исполнителя являются: статический и инструментальный .

#include "sandboxed_api/sandbox2/executor.h"

std::string path = "path/to/binary";
std::vector<std::string> args = {path};  // args[0] will become the sandboxed
                                         // process' argv[0], typically the
                                         // path to the binary.
auto executor = absl::make_unique<sandbox2::Executor>(path, args);

Способ 2: Sandbox2 Forkserver — сообщите исполнителю, когда его следует помещать в песочницу.

Этот метод обеспечивает гибкость, позволяя отключиться от песочницы во время инициализации, а затем выбрать, когда входить в песочницу, вызвав ::sandbox2::Client::SandboxMeHere() . Он требует, чтобы вы могли определить в коде, когда вы хотите запустить песочницу, и он должен быть однопоточным (почему читайте в FAQ ).

В приведенном ниже фрагменте кода мы используем тот же код, что описан в методе 1 выше. Однако, чтобы позволить программе выполняться без изолированной программной среды во время инициализации, мы вызываем set_enable_sandbox_before_exec(false) .

#include "sandboxed_api/sandbox2/executor.h"

std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
executor->set_enable_sandbox_before_exec(false);

Поскольку у исполнителя теперь отключена изолированная программная среда до тех пор, пока он не будет уведомлен об этом от Sandboxee, мы должны создать экземпляр ::sandbox2::Client , настроить связь между исполнителем и Sandboxee, а затем уведомить исполнителя о том, что наша инициализация завершена, и мы хочу запустить песочницу сейчас, вызвав sandbox2_client.SandboxMeHere() .

// main() of sandboxee
int main(int argc, char** argv) {
  gflags::ParseCommandLineFlags(&argc, &argv, false);

  // Set-up the sandbox2::Client object, using a file descriptor (1023).
  sandbox2::Comms comms(sandbox2::Comms::kSandbox2ClientCommsFD);
  sandbox2::Client sandbox2_client(&comms);
  // Enable sandboxing from here.
  sandbox2_client.SandboxMeHere();
  …

Примером этого метода исполнителя является crc4 , где crc4bin.cc — это песочница, которая уведомляет исполнителя ( crc4sandbox.cc ), когда ему следует войти в песочницу.

Способ 3: собственный форк-сервер. Подготовьте двоичный файл, дождитесь запросов на форк и создайте песочницу самостоятельно.

Этот режим позволяет вам запустить двоичный файл, подготовить его к помещению в песочницу и в определенный момент жизненного цикла двоичного файла сделать его доступным исполнителю.

Исполнитель отправит запрос на вилку вашему двоичному файлу, который выполнит fork() (через ::sandbox2::ForkingClient::WaitAndFork() ). Вновь созданный процесс будет готов к изолированию с помощью ::sandbox2::Client::SandboxMeHere() .

#include "sandboxed_api/sandbox2/executor.h"

// Start the custom ForkServer
std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto fork_executor = absl::make_unique<sandbox2::Executor>(path, args);
fork_executor->StartForkServer();

// Initialize Executor with Comms channel to the ForkServer
auto executor = absl::make_unique<sandbox2::Executor>(
    fork_executor->ipc()->GetComms());

Имейте в виду, что этот режим достаточно сложен и применим лишь в нескольких конкретных случаях; например, когда у вас жесткие требования к памяти. Вы получите выгоду от COW, но у вас есть обратная сторона: нет настоящей ASLR. Другим типичным примером использования может быть ситуация, когда Sandboxee имеет длительную и ресурсоемкую инициализацию, которую можно запустить до обработки ненадежных данных.

Пример этого метода исполнителя см. в custom_fork .

2. Создайте политику песочницы

Если у вас есть исполнитель, вы, скорее всего, захотите определить политику песочницы для песочницы. В противном случае Sandboxee защищен только политикой системных вызовов по умолчанию .

Целью политики песочницы является ограничение системных вызовов и аргументов, которые может выполнять песочница, а также файлов, к которым она может получить доступ. Вам необходимо иметь детальное представление о системных вызовах, необходимых для кода, который вы планируете поместить в «песочницу». Один из способов наблюдения за системными вызовами — запустить код с помощью инструмента командной строки Linux strace.

Получив список системных вызовов, вы можете использовать PolicyBuilder для определения политики. PolicyBuilder поставляется со множеством удобных и вспомогательных функций, которые позволяют выполнять множество распространенных операций. Следующий список представляет собой лишь небольшую часть доступных функций:

  • Разрешить любой системный вызов для запуска процесса:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • В список разрешенных системных вызовов открытия /чтения /записи*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Разрешить любые системные вызовы, связанные с выходом/доступом/состоянием:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • В список разрешенных системных вызовов, связанных со сном/временем:
    • AllowTime();
    • AllowSleep();

Эти удобные функции позволяют внести в список любые соответствующие системные вызовы. Это имеет то преимущество, что одна и та же политика может использоваться в разных архитектурах, где определенные системные вызовы недоступны (например, в ARM64 нет системного вызова OPEN), но с небольшим риском безопасности, связанным с включением большего количества системных вызовов, чем может быть необходимо. Например, AllowOpen() позволяет Sandboxee вызывать любой системный вызов, связанный с открытием. Если вы хотите внести в список разрешенных только один конкретный системный вызов, вы можете использовать AllowSyscall(); чтобы разрешить несколько системных вызовов одновременно, вы можете использовать AllowSyscalls() .

Пока что политика проверяет только идентификатор системного вызова. Если вам необходимо еще больше усилить политику и вы хотите определить политику, в которой вы разрешаете системный вызов только с определенными аргументами, вам нужно использовать AddPolicyOnSyscall() или AddPolicyOnSyscalls() . Эти функции не только принимают в качестве аргумента идентификатор системного вызова, но и необработанный фильтр seccomp-bpf, использующий вспомогательные макросы bpf из ядра Linux. Дополнительную информацию о BPF смотрите в документации ядра . Если вы обнаружите, что пишете повторяющийся код BPF, который, по вашему мнению, должен иметь оболочку для удобства использования, не стесняйтесь подавать запрос на добавление функции.

Помимо функций, связанных с системными вызовами, 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();
}

3. Отрегулируйте лимиты

Политика «песочницы» не позволяет «песочнице» вызывать определенные системные вызовы и, таким образом, уменьшает поверхность атаки. Однако злоумышленник все равно может вызвать нежелательные последствия, запуская процесс на неопределенный срок или исчерпав оперативную память и другие ресурсы.

Чтобы устранить эту угрозу, Sandboxee по умолчанию работает с жесткими ограничениями выполнения. Если эти ограничения по умолчанию вызывают проблемы с законным выполнением вашей программы, вы можете настроить их с помощью класса sandbox2::Limits , вызвав limits() для объекта-исполнителя.

В приведенном ниже фрагменте кода показаны некоторые примеры корректировки лимитов. Все доступные параметры описаны в заголовочном файле limit.h .

// Restrict the address space size of the sandboxee to 4 GiB.
executor->limits()->set_rlimit_as(4ULL << 30);
// Kill sandboxee with SIGXFSZ if it writes more than 1 GiB to the filesystem.
executor->limits()->set_rlimit_fsize(1ULL << 30);
// Number of file descriptors which can be used by the sandboxee.
executor->limits()->set_rlimit_nofile(1ULL << 10);
// The sandboxee is not allowed to create core files.
executor->limits()->set_rlimit_core(0);
// Maximum 300s of real CPU time.
executor->limits()->set_rlimit_cpu(300);
// Maximum 120s of wall time.
executor->limits()->set_walltime_limit(absl::Seconds(120));

Пример использования класса sandbox2::Limits смотрите в примере инструмента .

4. Запустите песочницу

В предыдущих разделах вы подготовили изолированную среду, политику, исполнителя и песочницу. Следующий шаг — создать объект Sandbox2 и запустить его.

Запускать синхронно

Песочница может работать синхронно, блокируясь до тех пор, пока не будет результата. Фрагмент кода ниже демонстрирует создание экземпляра объекта Sandbox2 и его синхронное выполнение. Более подробный пример см. в static .

#include "sandboxed_api/sandbox2/sandbox2.h"

sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
sandbox2::Result result = s2.Run();  // Synchronous
LOG(INFO) << "Result of sandbox execution: " << result.ToString();

Запускать асинхронно

Вы также можете запустить песочницу асинхронно, не блокируя ее до тех пор, пока не будет получен результат. Это полезно, например, при общении с Sandboxee. Фрагмент кода ниже демонстрирует этот вариант использования. Более подробные примеры см. в crc4 и инструменте .

#include "sandboxed_api/sandbox2/sandbox2.h"

sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
if (s2.RunAsync()) {
  // Communicate with sandboxee, use s2.Kill() to kill it if needed
  // ...
}
Sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

5. Общение с песочницей

По умолчанию исполнитель может взаимодействовать с Sandboxee через файловые дескрипторы. Это может быть все, что вам нужно, например, если вы просто хотите поделиться файлом с Sandboxee или прочитать стандартный вывод Sandboxee.

Однако вам, скорее всего, понадобится более сложная логика связи между исполнителем и Sandboxee. API связи (см. заголовочный файл comms.h ) можно использовать для отправки целых чисел, строк, байтовых буферов, protobufs или файловых дескрипторов.

Совместное использование файловых дескрипторов

Используя API межпроцессного взаимодействия (см. ipc.h ), вы можете использовать MapFd() или ReceiveFd() :

  • Используйте MapFd() для сопоставления файловых дескрипторов от исполнителя с Sandboxee. Это можно использовать для обмена файлом, открытым из исполнителя, для использования в Sandboxee. Пример использования можно увидеть в static .

    // The executor opened /proc/version and passes it to the sandboxee as stdin
    executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
    
  • Используйте ReceiveFd() для создания конечной точки пары сокетов. Это можно использовать для чтения стандартного вывода Sandboxee или стандартных ошибок. Пример использования можно увидеть в инструменте .

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

Использование API связи

Sandbox2 предоставляет удобный API для связи . Это простой и легкий способ совместного использования целых чисел, строк или байтовых буферов между исполнителем и Sandboxee. Ниже приведены некоторые фрагменты кода, которые вы можете найти в примере crc4 .

Чтобы начать работу с API связи, вам сначала нужно получить объект связи из объекта Sandbox2:

sandbox2::Comms* comms = s2.comms();

Как только объект связи станет доступен, данные можно будет отправить в Sandboxee с помощью одной из функций семейства Send*. Вы можете найти пример использования API связи в примере crc4 . В приведенном ниже фрагменте кода показан отрывок из этого примера. Исполнитель отправляет unsigned char buf[size] с помощью SendBytes(buf, size) :

if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
  /* handle error */
}

Чтобы получить данные от Sandboxee, используйте одну из функций Recv* . Фрагмент кода ниже представляет собой отрывок из примера crc4 . Исполнитель получает контрольную сумму в виде 32-битного целого числа без знака: uint32_t crc4;

if (!(comms->RecvUint32(&crc4))) {
  /* handle error */
}

Совместное использование данных с помощью буферов

Другая функция совместного использования данных — использование буферного API для совместного использования больших объемов данных и предотвращения дорогостоящих копий, которые пересылаются туда и обратно между исполнителем и Sandboxee.

Исполнитель создает буфер либо по размеру и передаваемым данным, либо непосредственно из файлового дескриптора, и передает его в песочницу, используя comms->SendFD() в исполнителе и comms->RecvFD() в песочнице.

В приведенном ниже фрагменте кода вы можете увидеть сторону исполнителя. Песочница работает асинхронно и обменивается данными через буфер с Sandboxee:

// start the sandbox asynchronously
s2.RunAsync();

// instantiate the comms object
sandbox2::Comms* comms = s2.comms();

// random buffer data we want to send
constexpr unsigned char buffer_data[] = /* random data */;
constexpr unsigned int buffer_dataLen = 34;

// create sandbox2 buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::CreateWithSize(1ULL << 20 /* 1Mib */);
std::unique_ptr<sandbox2::Buffer> buffer_ptr = std::move(buffer).value();

// point to the sandbox2 buffer and fill with data
uint8_t* buf = buffer_ptr‑>data();
memcpy(buf, buffer_data, buffer_data_len);

// send the data to the sandboxee
comms‑>SendFd(buffer_ptr‑>fd());

На стороне Sandboxee вам также необходимо создать объект буфера и прочитать данные из файлового дескриптора, отправленного исполнителем:

// establish the communication with the executor
int fd;
comms.RecvFD(&fd);

// create the buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::createFromFd(fd);

// get the data
auto buffer_ptr = std::move(buffer).value();
uint8_t* buf = buffer_ptr‑>data();

/* work with the buf object */

6. Выход из песочницы

В зависимости от того, как вы запускаете песочницу (см. этот шаг ), вам необходимо настроить способ закрытия песочницы, а значит, и самого Sandboxee.

Выход из песочницы, работающей синхронно

Если песочница работала синхронно, то Run вернется только после завершения Sandboxee. Поэтому никаких дополнительных действий для прекращения не требуется. Фрагмент кода ниже демонстрирует этот сценарий:

Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();

Выход из песочницы, работающей асинхронно

Если песочница работала асинхронно, для завершения доступны два варианта. Во-первых, вы можете просто дождаться завершения Sandboxee и получить окончательный статус выполнения:

sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

В качестве альтернативы вы можете завершить работу Sandboxee в любое время, но все равно рекомендуется вызывать AwaitResult() , поскольку в это время Sandboxee может завершиться по другой причине:

s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

7. Тест

Как и любой другой код, ваша реализация в песочнице должна содержать тесты. Тесты в песочнице предназначены не для проверки правильности программы, а для проверки того, может ли изолированная программа работать без проблем, таких как нарушения песочницы. Это также гарантирует правильность политики песочницы.

Программа в песочнице тестируется так же, как и в рабочей среде, с аргументами и входными файлами, которые она обычно обрабатывает.

Эти тесты могут быть такими же простыми, как тест оболочки или тесты C++ с использованием подпроцессов. Посмотрите примеры для вдохновения.

Заключение

Спасибо, что дочитали до этого места. Мы надеемся, что вам понравилось наше руководство, и теперь вы можете создавать свои собственные песочницы, чтобы обеспечить безопасность своих пользователей.

Создание песочниц и политик — сложная задача, в которой возможны незначительные ошибки. Чтобы оставаться в безопасности, мы рекомендуем вам, чтобы эксперт по безопасности проверил вашу политику и код.

,

На этой странице вы узнаете, как создать собственную изолированную среду с помощью Sandbox2. Вы узнаете, как определить политику песочницы и некоторые сложные, но распространенные настройки. Используйте приведенную здесь информацию в качестве руководства вместе с примерами и документацией по коду в заголовочных файлах.

1. Выберите метод исполнения песочницы

Песочница начинается с исполнителя (см. Sandbox Executor ), который отвечает за запуск Sandboxee . Заголовочный файл executor.h содержит API, необходимый для этой цели. API очень гибкий и позволяет вам выбрать то, что лучше всего подходит для вашего случая использования. В следующих разделах описаны 3 различных метода, из которых вы можете выбрать.

Способ 1: Автономный: выполнить двоичный файл с уже включенной песочницей.

Это самый простой способ использования песочницы, и его рекомендуется использовать, если вы хотите поместить в песочницу весь двоичный файл, для которого у вас нет исходного кода. Это также самый безопасный способ использования песочницы, поскольку не существует инициализации без изолированной программной среды, которая могла бы иметь неблагоприятные последствия.

В приведенном ниже фрагменте кода мы определяем путь к двоичному файлу, который будет помещен в песочницу, и аргументы, которые мы должны передать системному вызову execve. Как вы можете видеть в заголовочном файле executor.h , мы не указываем значение для envp и поэтому копируем среду из родительского процесса. Помните, что первым аргументом всегда является имя выполняемой программы, и наш фрагмент не определяет никаких других аргументов.

Примерами этого метода исполнителя являются: статический и инструментальный .

#include "sandboxed_api/sandbox2/executor.h"

std::string path = "path/to/binary";
std::vector<std::string> args = {path};  // args[0] will become the sandboxed
                                         // process' argv[0], typically the
                                         // path to the binary.
auto executor = absl::make_unique<sandbox2::Executor>(path, args);

Способ 2: Sandbox2 Forkserver — сообщите исполнителю, когда его следует помещать в песочницу.

Этот метод обеспечивает гибкость, позволяя отключиться от песочницы во время инициализации, а затем выбрать, когда входить в песочницу, вызвав ::sandbox2::Client::SandboxMeHere() . Он требует, чтобы вы могли определить в коде, когда вы хотите запустить песочницу, и он должен быть однопоточным (почему читайте в FAQ ).

В приведенном ниже фрагменте кода мы используем тот же код, что описан в методе 1 выше. Однако, чтобы позволить программе выполняться без изолированной программной среды во время инициализации, мы вызываем set_enable_sandbox_before_exec(false) .

#include "sandboxed_api/sandbox2/executor.h"

std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
executor->set_enable_sandbox_before_exec(false);

Поскольку у исполнителя теперь отключена изолированная программная среда до тех пор, пока он не будет уведомлен об этом от Sandboxee, мы должны создать экземпляр ::sandbox2::Client , настроить связь между исполнителем и Sandboxee, а затем уведомить исполнителя о том, что наша инициализация завершена, и мы хочу запустить песочницу сейчас, вызвав sandbox2_client.SandboxMeHere() .

// main() of sandboxee
int main(int argc, char** argv) {
  gflags::ParseCommandLineFlags(&argc, &argv, false);

  // Set-up the sandbox2::Client object, using a file descriptor (1023).
  sandbox2::Comms comms(sandbox2::Comms::kSandbox2ClientCommsFD);
  sandbox2::Client sandbox2_client(&comms);
  // Enable sandboxing from here.
  sandbox2_client.SandboxMeHere();
  …

Примером этого метода исполнителя является crc4 , где crc4bin.cc — это песочница, которая уведомляет исполнителя ( crc4sandbox.cc ), когда ему следует войти в песочницу.

Способ 3: собственный форк-сервер. Подготовьте двоичный файл, дождитесь запросов на форк и создайте песочницу самостоятельно.

Этот режим позволяет вам запустить двоичный файл, подготовить его к помещению в песочницу и в определенный момент жизненного цикла двоичного файла сделать его доступным исполнителю.

Исполнитель отправит запрос на вилку вашему двоичному файлу, который выполнит fork() (через ::sandbox2::ForkingClient::WaitAndFork() ). Вновь созданный процесс будет готов к изолированию с помощью ::sandbox2::Client::SandboxMeHere() .

#include "sandboxed_api/sandbox2/executor.h"

// Start the custom ForkServer
std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto fork_executor = absl::make_unique<sandbox2::Executor>(path, args);
fork_executor->StartForkServer();

// Initialize Executor with Comms channel to the ForkServer
auto executor = absl::make_unique<sandbox2::Executor>(
    fork_executor->ipc()->GetComms());

Имейте в виду, что этот режим достаточно сложен и применим лишь в нескольких конкретных случаях; например, когда у вас жесткие требования к памяти. Вы получите выгоду от COW, но у вас есть обратная сторона: нет настоящей ASLR. Другим типичным примером использования может быть ситуация, когда Sandboxee имеет длительную и ресурсоемкую инициализацию, которую можно запустить до обработки ненадежных данных.

Пример этого метода исполнителя см. в custom_fork .

2. Создайте политику песочницы

Если у вас есть исполнитель, вы, скорее всего, захотите определить политику песочницы для песочницы. В противном случае Sandboxee защищен только политикой системных вызовов по умолчанию .

Целью политики песочницы является ограничение системных вызовов и аргументов, которые может выполнять песочница, а также файлов, к которым она может получить доступ. Вам необходимо иметь детальное представление о системных вызовах, необходимых для кода, который вы планируете поместить в «песочницу». Один из способов наблюдения за системными вызовами — запустить код с помощью инструмента командной строки Linux strace.

Получив список системных вызовов, вы можете использовать PolicyBuilder для определения политики. PolicyBuilder поставляется со множеством удобных и вспомогательных функций, которые позволяют выполнять множество распространенных операций. Следующий список представляет собой лишь небольшую часть доступных функций:

  • Разрешить любой системный вызов для запуска процесса:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • В список разрешенных системных вызовов открытия /чтения /записи*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Разрешить любые системные вызовы, связанные с выходом/доступом/состоянием:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • В список разрешенных системных вызовов, связанных со сном/временем:
    • AllowTime();
    • AllowSleep();

Эти удобные функции позволяют внести в список любые соответствующие системные вызовы. Это имеет то преимущество, что одна и та же политика может использоваться в разных архитектурах, где определенные системные вызовы недоступны (например, в ARM64 нет системного вызова OPEN), но с небольшим риском безопасности, связанным с включением большего количества системных вызовов, чем может быть необходимо. Например, AllowOpen() позволяет Sandboxee вызывать любой системный вызов, связанный с открытием. Если вы хотите внести в список разрешенных только один конкретный системный вызов, вы можете использовать AllowSyscall(); чтобы разрешить несколько системных вызовов одновременно, вы можете использовать AllowSyscalls() .

Пока что политика проверяет только идентификатор системного вызова. Если вам необходимо еще больше усилить политику и вы хотите определить политику, в которой вы разрешаете системный вызов только с определенными аргументами, вам нужно использовать AddPolicyOnSyscall() или AddPolicyOnSyscalls() . Эти функции не только принимают в качестве аргумента идентификатор системного вызова, но и необработанный фильтр seccomp-bpf, использующий вспомогательные макросы bpf из ядра Linux. Дополнительную информацию о BPF смотрите в документации ядра . Если вы обнаружите, что пишете повторяющийся код BPF, который, по вашему мнению, должен иметь оболочку для удобства использования, не стесняйтесь подавать запрос на добавление функции.

Помимо функций, связанных с системными вызовами, 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();
}

3. Отрегулируйте лимиты

Политика «песочницы» не позволяет «песочнице» вызывать определенные системные вызовы и, таким образом, уменьшает поверхность атаки. Однако злоумышленник все равно может вызвать нежелательные последствия, запуская процесс на неопределенный срок или исчерпав оперативную память и другие ресурсы.

Чтобы устранить эту угрозу, Sandboxee по умолчанию работает с жесткими ограничениями выполнения. Если эти ограничения по умолчанию вызывают проблемы с законным выполнением вашей программы, вы можете настроить их с помощью класса sandbox2::Limits , вызвав limits() для объекта-исполнителя.

В приведенном ниже фрагменте кода показаны некоторые примеры корректировки лимитов. Все доступные параметры описаны в заголовочном файле limit.h .

// Restrict the address space size of the sandboxee to 4 GiB.
executor->limits()->set_rlimit_as(4ULL << 30);
// Kill sandboxee with SIGXFSZ if it writes more than 1 GiB to the filesystem.
executor->limits()->set_rlimit_fsize(1ULL << 30);
// Number of file descriptors which can be used by the sandboxee.
executor->limits()->set_rlimit_nofile(1ULL << 10);
// The sandboxee is not allowed to create core files.
executor->limits()->set_rlimit_core(0);
// Maximum 300s of real CPU time.
executor->limits()->set_rlimit_cpu(300);
// Maximum 120s of wall time.
executor->limits()->set_walltime_limit(absl::Seconds(120));

Пример использования класса sandbox2::Limits смотрите в примере инструмента .

4. Запустите песочницу

В предыдущих разделах вы подготовили изолированную среду, политику, исполнителя и песочницу. Следующий шаг — создать объект Sandbox2 и запустить его.

Запускать синхронно

Песочница может работать синхронно, блокируясь до тех пор, пока не будет результата. Фрагмент кода ниже демонстрирует создание экземпляра объекта Sandbox2 и его синхронное выполнение. Более подробный пример см. в static .

#include "sandboxed_api/sandbox2/sandbox2.h"

sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
sandbox2::Result result = s2.Run();  // Synchronous
LOG(INFO) << "Result of sandbox execution: " << result.ToString();

Запускать асинхронно

Вы также можете запустить песочницу асинхронно, не блокируя ее до тех пор, пока не будет получен результат. Это полезно, например, при общении с Sandboxee. Фрагмент кода ниже демонстрирует этот вариант использования. Более подробные примеры см. в crc4 и инструменте .

#include "sandboxed_api/sandbox2/sandbox2.h"

sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
if (s2.RunAsync()) {
  // Communicate with sandboxee, use s2.Kill() to kill it if needed
  // ...
}
Sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

5. Общение с песочницей

По умолчанию исполнитель может взаимодействовать с Sandboxee через файловые дескрипторы. Это может быть все, что вам нужно, например, если вы просто хотите поделиться файлом с Sandboxee или прочитать стандартный вывод Sandboxee.

Однако вам, скорее всего, понадобится более сложная логика связи между исполнителем и Sandboxee. API связи (см. заголовочный файл comms.h ) можно использовать для отправки целых чисел, строк, байтовых буферов, protobufs или файловых дескрипторов.

Совместное использование файловых дескрипторов

Используя API межпроцессного взаимодействия (см. ipc.h ), вы можете использовать MapFd() или ReceiveFd() :

  • Используйте MapFd() для сопоставления файловых дескрипторов от исполнителя с Sandboxee. Это можно использовать для обмена файлом, открытым из исполнителя, для использования в Sandboxee. Пример использования можно увидеть в static .

    // The executor opened /proc/version and passes it to the sandboxee as stdin
    executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
    
  • Используйте ReceiveFd() для создания конечной точки пары сокетов. Это можно использовать для чтения стандартного вывода Sandboxee или стандартных ошибок. Пример использования можно увидеть в инструменте .

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

Использование API связи

Sandbox2 предоставляет удобный API для связи . Это простой и легкий способ совместного использования целых чисел, строк или байтовых буферов между исполнителем и Sandboxee. Ниже приведены некоторые фрагменты кода, которые вы можете найти в примере crc4 .

Чтобы начать работу с API связи, вам сначала нужно получить объект связи из объекта Sandbox2:

sandbox2::Comms* comms = s2.comms();

Как только объект связи станет доступен, данные можно будет отправить в Sandboxee с помощью одной из функций семейства Send*. Вы можете найти пример использования API связи в примере crc4 . В приведенном ниже фрагменте кода показан отрывок из этого примера. Исполнитель отправляет unsigned char buf[size] с помощью SendBytes(buf, size) :

if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
  /* handle error */
}

Чтобы получить данные от Sandboxee, используйте одну из функций Recv* . Фрагмент кода ниже представляет собой отрывок из примера crc4 . Исполнитель получает контрольную сумму в виде 32-битного целого числа без знака: uint32_t crc4;

if (!(comms->RecvUint32(&crc4))) {
  /* handle error */
}

Совместное использование данных с помощью буферов

Другая функция совместного использования данных — использование буферного API для совместного использования больших объемов данных и предотвращения дорогостоящих копий, которые пересылаются туда и обратно между исполнителем и Sandboxee.

Исполнитель создает буфер либо по размеру и передаваемым данным, либо непосредственно из файлового дескриптора, и передает его в песочницу, используя comms->SendFD() в исполнителе и comms->RecvFD() в песочнице.

В приведенном ниже фрагменте кода вы можете увидеть сторону исполнителя. Песочница работает асинхронно и обменивается данными через буфер с Sandboxee:

// start the sandbox asynchronously
s2.RunAsync();

// instantiate the comms object
sandbox2::Comms* comms = s2.comms();

// random buffer data we want to send
constexpr unsigned char buffer_data[] = /* random data */;
constexpr unsigned int buffer_dataLen = 34;

// create sandbox2 buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::CreateWithSize(1ULL << 20 /* 1Mib */);
std::unique_ptr<sandbox2::Buffer> buffer_ptr = std::move(buffer).value();

// point to the sandbox2 buffer and fill with data
uint8_t* buf = buffer_ptr‑>data();
memcpy(buf, buffer_data, buffer_data_len);

// send the data to the sandboxee
comms‑>SendFd(buffer_ptr‑>fd());

На стороне Sandboxee вам также необходимо создать объект буфера и прочитать данные из файлового дескриптора, отправленного исполнителем:

// establish the communication with the executor
int fd;
comms.RecvFD(&fd);

// create the buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
     sandbox2::Buffer::createFromFd(fd);

// get the data
auto buffer_ptr = std::move(buffer).value();
uint8_t* buf = buffer_ptr‑>data();

/* work with the buf object */

6. Выход из песочницы

В зависимости от того, как вы запускаете песочницу (см. этот шаг ), вам необходимо настроить способ закрытия песочницы, а значит, и самого Sandboxee.

Выход из песочницы, работающей синхронно

Если песочница работала синхронно, то Run вернется только после завершения Sandboxee. Поэтому никаких дополнительных действий для прекращения не требуется. Фрагмент кода ниже демонстрирует этот сценарий:

Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();

Выход из песочницы, работающей асинхронно

Если песочница работала асинхронно, для завершения доступны два варианта. Во-первых, вы можете просто дождаться завершения Sandboxee и получить окончательный статус выполнения:

sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

В качестве альтернативы вы можете завершить работу Sandboxee в любое время, но все же рекомендуется вызывать AwaitResult() , поскольку в это время Sandboxee может завершиться по другой причине:

s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

7. Тест

Как и любой другой код, ваша реализация в песочнице должна содержать тесты. Тесты в песочнице предназначены не для проверки правильности программы, а для проверки того, может ли изолированная программа работать без проблем, таких как нарушения песочницы. Это также гарантирует правильность политики песочницы.

Программа в песочнице тестируется так же, как и в рабочей среде, с аргументами и входными файлами, которые она обычно обрабатывает.

Эти тесты могут быть такими же простыми, как тест оболочки или тесты C++ с использованием подпроцессов. Посмотрите примеры для вдохновения.

Заключение

Спасибо, что дочитали до этого места. Мы надеемся, что вам понравилось наше руководство, и теперь вы можете создавать свои собственные песочницы, чтобы обеспечить безопасность своих пользователей.

Создание песочниц и политик — сложная задача, в которой возможны незначительные ошибки. Чтобы оставаться в безопасности, мы рекомендуем вам, чтобы эксперт по безопасности проверил вашу политику и код.