На этой странице вы узнаете, как создать собственную изолированную среду с помощью Sandbox2. Вы узнаете, как определить правила песочницы, и познакомитесь с некоторыми продвинутыми, но распространенными настройками. Используйте приведенную здесь информацию в качестве руководства, а также примеры и документацию по коду в заголовочных файлах.
1. Как выбрать метод выполнения песочницы
Изолированная среда начинается с исполнителя (см. Sandbox Executor), который отвечает за запуск Sandboxee. Заголовочный файл executor.h содержит API, необходимый для этой цели. Этот API очень гибкий и позволяет выбирать, что лучше всего подходит для вашего варианта использования. В следующих разделах описаны три способа, которые можно использовать.
Способ 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 – укажите исполнителю, когда нужно использовать изолированную среду
Этот метод позволяет не использовать изолированную среду во время инициализации, а затем выбрать, когда войти в нее, вызвав ::sandbox2::Client::SandboxMeHere(). Вам нужно будет определить в коде, когда следует запускать песочницу, и она должна быть однопоточной (подробнее об этом рассказывается в часто задаваемых вопросах).
В приведенном ниже фрагменте кода используется тот же код, что и в методе 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. Собственный сервер Forkserver – подготовьте двоичный файл, дождитесь запросов на создание дочернего процесса и самостоятельно создайте изолированную среду
Этот режим позволяет запустить двоичный файл, подготовить его к помещению в изолированную среду и в определенный момент его жизненного цикла сделать его доступным для исполнителя.
Исполнитель отправит запрос на разветвление в ваш двоичный файл, который будет 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());
Учтите, что этот режим довольно сложный и подходит только для некоторых случаев, например когда у вас мало памяти. Вы сможете использовать COW, но не ASLR. Другой типичный пример использования – когда у Sandboxee есть длительная инициализация с интенсивным использованием ЦП, которую можно запустить до обработки ненадежных данных.
Пример метода исполнителя можно найти в custom_fork.
2. Как создать правило для изолированной среды
После того как вы создали исполнитель, вам, скорее всего, потребуется определить правила песочницы для изолируемого процесса. В противном случае Sandboxee будет защищен только правилом Default Syscall Policy.
Цель правил песочницы – ограничить системные вызовы и аргументы, которые может использовать изолированный процесс, а также файлы, к которым он может получить доступ. Вам нужно будет хорошо понимать, какие системные вызовы требуются коду, который вы планируете поместить в изолированную среду. Один из способов отслеживания системных вызовов – запуск кода с помощью инструмента командной строки strace в Linux.
Получив список системных вызовов, вы можете использовать PolicyBuilder, чтобы определить правила.
В PolicyBuilder есть множество удобных вспомогательных функций, которые позволяют выполнять распространенные операции (многие из них уже включены в DefaultPolicyBuilder()). Ниже приведен лишь небольшой список доступных функций.
- Добавьте в белый список любой системный вызов для запуска процесса:
AllowStaticStartup();AllowDynamicStartup();
- Добавьте в белый список все системные вызовы open/read/write*:
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().
В следующем примере показано, как создать sandbox2::PolicyBuilder напрямую, чтобы продемонстрировать ограничение аргументов write и mprotect и блокировку openat (которая в противном случае была бы разрешена DefaultPolicyBuilder()):
#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. Как изменить ограничения
Правила песочницы запрещают изолированному процессу вызывать определенные системные вызовы и, таким образом, уменьшают направления атак. Однако злоумышленник может вызвать нежелательные последствия, запустив процесс на неопределенный срок или исчерпав оперативную память и другие ресурсы.
Чтобы устранить эту угрозу, изолированная среда по умолчанию работает в условиях жестких ограничений на выполнение. Если эти ограничения по умолчанию вызывают проблемы с выполнением вашей программы, вы можете изменить их с помощью класса 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. Запуск изолированной среды
В предыдущих разделах вы подготовили изолированную среду, правила, исполнитель и изолированный процесс. Теперь нужно создать объект Sandbox2 и запустить его.
Синхронное выполнение
Песочница может работать синхронно, то есть блокировать выполнение до получения результата. В приведенном ниже фрагменте кода показано, как создать экземпляр объекта Sandbox2 и выполнить его синхронно. Более подробный пример приведен в разделе Статические объявления.
#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.
Однако, скорее всего, вам потребуется более сложная логика взаимодействия между исполнителем и изолированной средой. API связи (см. заголовочный файл comms.h) можно использовать для отправки целых чисел, строк, буферов байтов, protobuf-файлов или дескрипторов файлов.
Общий доступ к дескрипторам файлов
С помощью Inter-Process Communication API (см. ipc.h) можно использовать MapFd() или ReceiveFd():
Используйте
MapFd(), чтобы сопоставить дескрипторы файлов исполнителя и изолированной среды. Это можно использовать для совместного использования файла, открытого исполнителем, для использования в Песочница. Пример использования можно посмотреть в статическом фиде.// The executor opened /proc/version and passes it to the sandboxee as stdin executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);Используйте
ReceiveFd(), чтобы создать конечную точку socketpair. Это можно использовать для чтения стандартного вывода или стандартных ошибок песочницы. Пример использования можно посмотреть в инструменте.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
Использование API для связи
В Sandbox2 есть удобный API comms. Это простой и удобный способ обмена целыми числами, строками или байтовыми буферами между исполнителем и песочницей. Ниже приведены фрагменты кода, которые можно найти в примере crc4.
Чтобы начать работу с API связи, сначала необходимо получить объект связи из объекта Sandbox2:
sandbox2::Comms* comms = s2.comms();
После того как объект 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 буфера для передачи больших объемов данных и предотвращения создания дорогостоящих копий, которые отправляются туда и обратно между исполнителем и песочницей.
Исполнитель создает буфер, либо указывая его размер и данные, которые нужно передать, либо напрямую из дескриптора файла, и передает его в изолированную среду, используя 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 вернет значение только после завершения работы 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++, использующие дочерние процессы. Посмотрите примеры.
Заключение
Спасибо, что дочитали до конца. Надеемся, что наше руководство вам понравилось и теперь вы сможете создавать собственные песочницы, чтобы защищать своих пользователей.
Создание песочниц и правил – сложная задача, при выполнении которой легко допустить ошибки. Чтобы избежать проблем, рекомендуем обратиться к специалисту по безопасности, который проверит ваши правила и код.