در این صفحه، یاد خواهید گرفت که چگونه محیط سندباکس خود را با Sandbox2 ایجاد کنید. نحوه تعریف یک سیاست سندباکس و برخی از ترفندهای پیشرفته اما رایج را خواهید آموخت. از اطلاعات اینجا به عنوان راهنما، در کنار مثالها و مستندات کد در فایلهای هدر، استفاده کنید.
۱. یک روش اجراکنندهی سندباکس انتخاب کنید
سندباکسینگ با یک اجراکننده (به بخش اجراکننده سندباکس مراجعه کنید) آغاز میشود که مسئول اجرای سندباکس است. فایل هدر executor.h شامل API مورد نیاز برای این منظور است. این API بسیار انعطافپذیر است و به شما امکان میدهد تا انتخاب کنید که چه چیزی برای مورد استفاده شما بهتر عمل میکند. بخشهای زیر ۳ روش مختلف را که میتوانید از بین آنها انتخاب کنید، شرح میدهند.
روش ۱: مستقل - اجرای یک فایل باینری با فعال بودن قابلیت sandboxing
این سادهترین روش برای استفاده از سندباکسینگ است و زمانی که میخواهید کل یک فایل باینری را که کد منبع آن را ندارید، سندباکس کنید، روش توصیه شدهای است. همچنین امنترین راه برای استفاده از سندباکسینگ است، زیرا هیچ مقداردهی اولیه بدون سندباکس وجود ندارد که بتواند عوارض جانبی داشته باشد.
در قطعه کد زیر، مسیر فایل باینری که باید در جعبه شنی قرار گیرد و آرگومانهایی که باید به یک فراخوانی سیستمی 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);
روش ۲: Sandbox2 Forkserver – به مجری بگویید چه زمانی سندباکس شود
این روش انعطافپذیری خارج شدن از حالت sandbox در طول مقداردهی اولیه و سپس انتخاب زمان ورود به sandboxing با فراخوانی ::sandbox2::Client::SandboxMeHere() را ارائه میدهد. این روش مستلزم آن است که بتوانید زمان شروع sandboxing را در کد تعریف کنید و باید تکرشتهای باشد (دلیل آن را در سوالات متداول بخوانید).
در قطعه کد زیر، ما از همان کدی که در روش ۱ در بالا توضیح داده شد استفاده میکنیم. با این حال، برای اینکه برنامه بتواند در حین مقداردهی اولیه به صورت بدون سندباکس اجرا شود، تابع 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 همان Sandboxee است و به اجراکننده ( crc4sandbox.cc ) اطلاع میدهد که چه زمانی باید وارد sandbox شود.
روش ۳: سرور چنگال سفارشی - یک فایل باینری آماده کنید، منتظر درخواستهای چنگال باشید و خودتان آن را در سندباکس قرار دهید
این حالت به شما امکان میدهد یک فایل باینری را شروع کنید، آن را برای sandbox آماده کنید و در یک لحظه خاص از چرخه حیات فایل باینری خود، آن را در دسترس مجری قرار دهید.
مجری یک درخواست fork به فایل باینری شما ارسال میکند که fork() را انجام میدهد (از طریق ::sandbox2::ForkingClient::EnterForkLook() ). فرآیند تازه ایجاد شده آمادهی sandbox شدن با ::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 یک مقداردهی اولیه طولانی و CPU-محور دارد که میتواند قبل از پردازش دادههای غیرقابل اعتماد اجرا شود.
برای مثالی از این متد اجراکننده، به custom_fork مراجعه کنید.
۲. یک سیاست جعبه شنی ایجاد کنید
وقتی یک اجراکننده (executor) داشته باشید، احتمالاً میخواهید یک سیاست جعبه شنی (Sandbox Policy) برای Sandboxee تعریف کنید. در غیر این صورت، Sandboxee فقط توسط سیاست فراخوانی پیشفرض (Default Syscall Policy) محافظت میشود.
با استفاده از سیاست جعبه شنی (Sandbox Policy)، هدف محدود کردن فراخوانیهای سیستمی و آرگومانهایی است که Sandboxee میتواند ایجاد کند، و همچنین فایلهایی که میتواند به آنها دسترسی داشته باشد. شما باید درک دقیقی از فراخوانیهای سیستمی مورد نیاز کدی که قصد دارید در جعبه شنی قرار دهید، داشته باشید. یکی از راههای مشاهده فراخوانیهای سیستمی، اجرای کد با ابزار خط فرمان لینوکس (strace) است.
وقتی لیست فراخوانیهای سیستمی را داشتید، میتوانید از PolicyBuilder برای تعریف سیاست استفاده کنید. PolicyBuilder با توابع کمکی و راحتی زیادی ارائه میشود که امکان انجام بسیاری از عملیات رایج را فراهم میکند. لیست زیر تنها گزیدهای کوچک از توابع موجود است:
- هر فراخوانی سیستمی را برای شروع فرآیند مجاز کنید:
-
AllowStaticStartup(); -
AllowDynamicStartup();
-
- لیست کردن هرگونه فراخوانی سیستمی باز /خواندن /نوشتن*:
-
AllowOpen(); -
AllowRead(); -
AllowWrite();
-
- هرگونه فراخوانی سیستمی مربوط به خروج/دسترسی/وضعیت را در لیست مجاز قرار دهید:
-
AllowExit(); -
AllowStat(); -
AllowAccess();
-
- هرگونه فراخوانی سیستمی مرتبط با خواب/زمان را در لیست مجاز قرار دهید:
-
AllowTime(); -
AllowSleep();
-
این توابعِ کاربردی، هر فراخوانی سیستمیِ مرتبط را در لیست قرار میدهند. این مزیت را دارد که میتوان از همان سیاست در معماریهای مختلف که فراخوانیهای سیستمیِ خاصی در دسترس نیستند (مثلاً ARM64 فراخوانی سیستمیِ OPEN ندارد) استفاده کرد، اما با ریسک امنیتی جزئیِ فعال کردنِ تعداد بیشتری از فراخوانیهای سیستمیِ مورد نیاز. به عنوان مثال، AllowOpen() به Sandboxee این امکان را میدهد که هر فراخوانی سیستمیِ مرتبط با باز بودن را فراخوانی کند. اگر فقط میخواهید یک فراخوانی سیستمیِ خاص را در لیست قرار دهید، میتوانید AllowSyscall(); برای مجاز کردن چندین فراخوانی سیستمی به طور همزمان، میتوانید AllowSyscalls() استفاده کنید.
تاکنون این سیاست فقط شناسه syscall را بررسی میکند. اگر نیاز به تقویت بیشتر سیاست دارید و میخواهید سیاستی تعریف کنید که در آن فقط به یک syscall با آرگومانهای خاص اجازه دهید، باید AddPolicyOnSyscall() یا AddPolicyOnSyscalls() استفاده کنید. این توابع نه تنها شناسه syscall را به عنوان آرگومان میگیرند، بلکه یک فیلتر خام seccomp-bpf را با استفاده از ماکروهای کمکی bpf از هسته لینوکس نیز دریافت میکنند. برای اطلاعات بیشتر در مورد BPF به مستندات هسته مراجعه کنید. اگر متوجه شدید که کد BPF تکراری مینویسید که فکر میکنید باید یک usability-wrapper داشته باشد، میتوانید درخواست ویژگی را ثبت کنید.
جدا از توابع مربوط به فراخوانی سیستمی، PolicyBuilder تعدادی تابع مرتبط با سیستم فایل مانند AddFile() یا AddDirectory() را نیز برای اتصال-ماونت کردن یک فایل/دایرکتوری در sandbox ارائه میدهد. از تابع کمکی AddTmpfs() میتوان برای اضافه کردن یک فضای ذخیرهسازی موقت فایل در sandbox استفاده کرد.
یک تابع بسیار مفید AddLibrariesForBinary() است که کتابخانهها و لینکر مورد نیاز یک فایل باینری را اضافه میکند.
متأسفانه، ایجاد فراخوانیهای سیستمی برای allowlist هنوز کمی کار دستی است. یک سیاست با فراخوانیهای سیستمی که نیازهای باینری خود را میدانید ایجاد کنید و آن را با یک بار کاری مشترک اجرا کنید. اگر تخلفی رخ داد، فراخوانی سیستمی را در allowlist قرار دهید و این فرآیند را تکرار کنید. اگر با تخلفی مواجه شدید که فکر میکنید ممکن است برای allowlist خطرناک باشد و برنامه خطاها را به خوبی مدیریت کند، میتوانید با استفاده از 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();
}
۳. محدودیتها را تنظیم کنید
سیاست جعبه شنی (Sandbox Policy) مانع از فراخوانی فراخوانیهای سیستمی خاص توسط Sandboxee میشود و بنابراین سطح حمله را کاهش میدهد. با این حال، یک مهاجم ممکن است همچنان بتواند با اجرای نامحدود یک فرآیند یا استفاده بیش از حد از رم و سایر منابع، اثرات نامطلوبی ایجاد کند.
برای مقابله با این تهدید، Sandboxee به طور پیشفرض تحت محدودیتهای اجرایی شدیدی اجرا میشود. اگر این محدودیتهای پیشفرض باعث ایجاد مشکل برای اجرای قانونی برنامه شما میشوند، میتوانید آنها را با استفاده از کلاس sandbox2::Limits و با فراخوانی limits() روی شیء executor تنظیم کنید.
قطعه کد زیر چند نمونه از تنظیمات محدودیت را نشان میدهد. تمام گزینههای موجود در فایل هدر 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 ، به ابزار مثال مراجعه کنید.
۴. اجرای سندباکس
در بخشهای قبلی، محیط سندباکس، سیاست، و اجراکننده و 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();
اجرا به صورت غیرهمزمان
شما همچنین میتوانید sandbox را به صورت غیرهمزمان اجرا کنید، بنابراین تا زمانی که نتیجهای حاصل نشود، مسدود نمیشود. این مورد، به عنوان مثال، هنگام برقراری ارتباط با Sandboxee مفید است. قطعه کد زیر این مورد استفاده را نشان میدهد، برای مثالهای دقیقتر به 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();
۵. ارتباط با Sandboxee
به طور پیشفرض، اجراکننده میتواند از طریق توصیفگرهای فایل با 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 ارتباطی مناسب ارائه میدهد. این یک روش ساده و آسان برای به اشتراک گذاشتن اعداد صحیح، رشتهها یا بافرهای بایت بین مجری و Sandboxee است. در زیر چند قطعه کد وجود دارد که میتوانید در مثال crc4 پیدا کنید.
برای شروع کار با API مربوط به comms، ابتدا باید شیء comms را از شیء Sandbox2 دریافت کنید:
sandbox2::Comms* comms = s2.comms();
زمانی که شیء comms در دسترس قرار گرفت، دادهها میتوانند با استفاده از یکی از توابع خانواده Send* به Sandboxee ارسال شوند. میتوانید نمونهای از استفاده از 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 است. اجراکننده، مجموع مقابلهای را در یک عدد صحیح بدون علامت ۳۲ بیتی دریافت میکند: uint32_t crc4;
if (!(comms->RecvUint32(&crc4))) {
/* handle error */
}
اشتراکگذاری دادهها با بافرها
یکی دیگر از قابلیتهای اشتراکگذاری دادهها، استفاده از رابط برنامهنویسی کاربردی بافر برای اشتراکگذاری حجم زیادی از دادهها و جلوگیری از ارسال و دریافت کپیهای گرانقیمت بین اجراکننده و Sandboxee است.
اجراکننده یک بافر ایجاد میکند، چه بر اساس اندازه و دادههایی که باید منتقل شوند، و چه مستقیماً از یک توصیفگر فایل، و آن را با استفاده comms->SendFD() در اجراکننده و comms->RecvFD() در Sandboxee به Sandboxee منتقل میکند.
در قطعه کد زیر، میتوانید سمت اجراکننده را مشاهده کنید. sandbox به صورت ناهمگام اجرا میشود و دادهها را از طریق یک بافر با 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 */
۶. خروج از سندباکس
بسته به نحوه اجرای sandbox (به این مرحله مراجعه کنید)، باید نحوه خاتمه دادن به sandbox و در نتیجه Sandboxee را تنظیم کنید.
خروج از یک سندباکس که به صورت همزمان اجرا میشود
اگر sandbox به صورت همزمان اجرا شده باشد، Run فقط زمانی برمیگردد که Sandboxee تمام شده باشد. بنابراین هیچ مرحله اضافی برای خاتمه لازم نیست. قطعه کد زیر این سناریو را نشان میدهد:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
خروج از یک سندباکس که به صورت ناهمزمان اجرا میشود
اگر sandbox به صورت ناهمزمان اجرا شده باشد، دو گزینه برای خاتمه دادن وجود دارد. اول، میتوانید منتظر اتمام 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();
۷. آزمون
مانند هر کد دیگری، پیادهسازی سندباکس شما باید دارای تست باشد. تستهای سندباکس برای آزمایش صحت برنامه نیستند، بلکه برای بررسی این هستند که آیا برنامه سندباکس شده میتواند بدون مشکلاتی مانند نقض سندباکس اجرا شود یا خیر. این همچنین تضمین میکند که سیاست سندباکس صحیح است.
یک برنامهی سندباکس شده به همان روشی که در محیط عملیاتی اجرا میشود، آزمایش میشود، یعنی با آرگومانها و فایلهای ورودی که معمولاً پردازش میکند.
این تستها میتوانند به سادگی یک تست پوسته یا تستهای C++ با استفاده از زیرفرآیندها باشند. برای الهام گرفتن، مثالها را بررسی کنید.
نتیجهگیری
از اینکه تا اینجا را مطالعه کردید متشکریم، امیدواریم از راهنمای ما خوشتان آمده باشد و اکنون احساس قدرت کنید تا بتوانید سندباکسهای خودتان را ایجاد کنید تا به حفظ امنیت کاربرانتان کمک کنید.
ایجاد سندباکسها و سیاستها کاری دشوار و مستعد خطاهای جزئی است. برای حفظ امنیت، توصیه میکنیم از یک متخصص امنیت بخواهید سیاستها و کد شما را بررسی کند.