На этой странице вы узнаете, как создать собственную изолированную среду с помощью Sandbox2. Вы научитесь определять политику песочницы , а также некоторым расширенным, но распространенным настройкам. Используйте информацию здесь в качестве руководства, наряду с примерами и документацией по коду в заголовочных файлах.
1. Выберите метод использования песочницы в качестве исполнителя.
Песочница начинается с исполнителя (см. Исполнитель песочницы ), который отвечает за запуск объекта, находящегося в песочнице . Заголовочный файл executor.h содержит API, необходимый для этой цели. API очень гибкий и позволяет выбрать тот, который лучше всего подходит для вашего случая. В следующих разделах описаны 3 различных метода, из которых вы можете выбрать.
Метод 1: Автономный режим – Запуск исполняемого файла с уже включенной песочницей.
Это самый простой способ использования песочницы и рекомендуемый метод, если вы хотите изолировать весь исполняемый файл, для которого у вас нет исходного кода. Это также самый безопасный способ использования песочницы, поскольку отсутствует инициализация вне песочницы, которая могла бы иметь негативные последствия.
В приведенном ниже фрагменте кода мы определяем путь к исполняемому файлу, который нужно изолировать, и аргументы, которые необходимо передать системному вызову execve. Как видно из заголовочного файла executor.h , мы не указываем значение для envp и, следовательно, копируем окружение из родительского процесса. Помните, что первый аргумент всегда является именем программы, которую нужно выполнить, и наш фрагмент кода не определяет никаких других аргументов.
Примерами такого метода исполнителя являются: static и tool .
#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);
Поскольку исполнитель теперь находится в отключенной песочнице до получения уведомления от принимающей стороны, нам необходимо создать экземпляр ::sandbox2::Client , установить связь между исполнителем и принимающей стороной, а затем уведомить исполнителя о завершении инициализации и желании возобновить работу в песочнице, вызвав метод 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::EnterForkLoop() ). Вновь созданный процесс будет готов к изолированию с помощью ::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());
Следует помнить, что этот режим довольно сложен и применим лишь в нескольких конкретных случаях; например, когда у вас ограниченные требования к памяти. Вы получите преимущества от механизма «кто напишет — увидишь», но при этом будет недостаток в отсутствии реального механизма ASLR. Другой типичный пример использования — когда в Sandboxee выполняется длительная, ресурсоемкая инициализация, которая может быть запущена до обработки недоверенных данных.
Пример использования этого метода исполнителя см. в custom_fork .
2. Создайте политику песочницы.
После того как вы выбрали исполнителя, вам, вероятно, потребуется определить политику песочницы для объекта, находящегося в песочнице. В противном случае объект, находящийся в песочнице, будет защищен только политикой системных вызовов по умолчанию .
Цель политики песочницы — ограничить системные вызовы и аргументы, которые может выполнять изолируемый объект, а также файлы, к которым он может получить доступ. Вам потребуется детальное понимание системных вызовов, необходимых для кода, который вы планируете изолировать. Один из способов отслеживания системных вызовов — запуск кода с помощью инструмента командной строки Linux strace.
Получив список системных вызовов, вы можете использовать PolicyBuilder для определения политики. PolicyBuilder содержит множество удобных и вспомогательных функций, позволяющих выполнять многие распространенные операции. Ниже приведен лишь небольшой список доступных функций:
- Добавьте в список разрешенных системные вызовы для запуска процесса:
-
AllowStaticStartup(); -
AllowDynamicStartup();
-
- Разрешить любые открытые /чтенные /записываемые системные вызовы:
-
AllowOpen(); -
AllowRead(); -
AllowWrite();
-
- Добавьте в список разрешенных системные вызовы, связанные с выходом/доступом/состоянием:
-
AllowExit(); -
AllowStat(); -
AllowAccess();
-
- Добавьте в список разрешенных системные вызовы, связанные со сном/временем:
-
AllowTime(); -
AllowSleep();
-
Эти вспомогательные функции позволяют разрешить любые соответствующие системные вызовы. Преимущество заключается в том, что одна и та же политика может использоваться на разных архитектурах, где некоторые системные вызовы недоступны (например, в ARM64 нет системного вызова OPEN), но с небольшим риском безопасности, связанным с разрешением большего количества системных вызовов, чем может потребоваться. Например, AllowOpen() позволяет пользователю песочницы вызывать любой системный вызов, связанный с Open. Если вы хотите разрешить только один конкретный системный вызов, вы можете использовать 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() для объекта исполнителя.
Приведённый ниже фрагмент кода демонстрирует несколько примеров настройки ограничений. Все доступные параметры описаны в заголовочном файле limits.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. Запустите песочницу.
В предыдущих разделах вы подготовили изолированную среду, политику, исполнителя и объект Sandboxee. Следующий шаг — создание объекта 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();
Выполнять асинхронно
Вы также можете запускать песочницу асинхронно, не блокируя выполнение до получения результата. Это полезно, например, при взаимодействии с объектом, находящимся в песочнице. Приведенный ниже фрагмент кода демонстрирует этот вариант использования; более подробные примеры см. в crc4 и tool .
#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 ) можно использовать для отправки целых чисел, строк, байтовых буферов, protobuf или файловых дескрипторов.
Дескрипторы файлов для совместного доступа
Используя 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()для создания конечной точки socketpair. Это можно использовать для чтения стандартного вывода или стандартных ошибок Sandboxee. Пример использования можно увидеть в инструменте .// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
Использование API связи
Sandbox2 предоставляет удобный API для обмена данными . Это простой и удобный способ обмена целыми числами, строками или буферами байтов между исполнителем и пользователем Sandbox. Ниже приведены фрагменты кода, которые вы можете найти в примере crc4 .
Для начала работы с API связи необходимо получить объект comms из объекта Sandbox2:
sandbox2::Comms* comms = s2.comms();
После того как объект comms станет доступен, данные можно отправлять в Sandboxee, используя одну из функций семейства Send*. Пример использования API comms можно найти в примере 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() в песочнице.
В приведенном ниже фрагменте кода вы можете увидеть сторону исполнителя. Песочница работает асинхронно и обменивается данными с тем, кто находится в песочнице, через буфер:
// 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. Выход из песочницы
В зависимости от способа запуска песочницы (см. этот шаг ), вам необходимо скорректировать способ завершения работы песочницы, а следовательно, и пользователя, находящегося в ней.
Выход из песочницы, работающей в синхронном режиме.
Если песочница работает синхронно, то команда Run вернет управление только после завершения работы песочницы. Следовательно, никаких дополнительных действий для завершения не требуется. Приведенный ниже фрагмент кода демонстрирует этот сценарий:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
Выход из песочницы, работающей асинхронно.
Если песочница работала асинхронно, то для её завершения доступны два варианта. Во-первых, можно просто дождаться завершения работы объекта, находящегося в песочнице, и получить окончательный статус выполнения:
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++ с использованием дочерних процессов. Посмотрите примеры для вдохновения.
Заключение
Спасибо, что дочитали до конца. Надеемся, вам понравилось наше руководство, и теперь вы чувствуете себя уверенно, создавая собственные песочницы для обеспечения безопасности ваших пользователей.
Создание песочниц и политик — сложная задача, чреватая незаметными ошибками. Для большей безопасности мы рекомендуем обратиться к эксперту по безопасности для проверки ваших политик и кода.