در این صفحه، با نحوه ایجاد محیط جعبه شنی خودتان با Sandbox2 آشنا خواهید شد. با نحوه تعریف کردن خطمشی جعبه شنی و برخیاز ترفندهای پیشرفته اما رایج آشنا خواهید شد. از اطلاعات اینجا بهعنوان راهنما، همراه با مثالها و اسناد کد در فایلهای سرصفحه استفاده کنید.
۱. انتخاب روش اجرای «جعبه ایمن»
جعبه شنی با یک اجراکننده شروع میشود (به اجراکننده جعبه شنی مراجعه کنید)، که مسئول اجرای Sandboxee است. فایل سرصفحه executor.h شامل میانای برنامهسازی کاربردی موردنیاز برای این هدف است. این API بسیار انعطافپذیر است و به شما امکان میدهد آنچه را که برای مورد استفادهتان مناسبتر است انتخاب کنید. بخشهای زیر ۳ روش مختلف را که میتوانید از بین آنها انتخاب کنید توضیح میدهد.
روش ۱: مستقل – اجرای فایل باینری با فعال بودن قبلی محیط امن
این سادهترین راه برای استفاده از جعبه امن است و روش توصیهشدهای است وقتی میخواهید کل باینری را که کد منبع آن را ندارید در جعبه امن قرار دهید. این روش همچنین ایمنترین راه برای استفاده از جعبه امن است، زیرا هیچ مقداردهی اولیه بدون جعبه امنی وجود ندارد که بتواند اثرات نامطلوب داشته باشد.
در تکهکد زیر، مسیر باینری را که باید در جعبه امن قرار گیرد و
آرگومانهایی را که باید به syscall 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 – به مجری بگویید چه زمانی در جعبه شنی قرار گیرد
این روش انعطافپذیری بدون محدودیت بودن درطول مقداردهی اولیه را ارائه میدهد،
و سپس با فراخوانی ::sandbox2::Client::SandboxMeHere() انتخاب میکند که چه زمانی وارد محیط محدود شود. این روش مستلزم آن است که بتوانید در کد تعریف کنید که چه زمانی میخواهید جعبه امن را شروع کنید، و باید تکرشتهای باشد (دلیل آن را در پرسشگان بخوانید).
در تکهکد زیر، از همان کدی که در «روش ۱» در بالا توضیح داده شد استفاده میکنیم.
بااینحال، برای اینکه برنامه بتواند درطول مقداردهی اولیه بهصورت غیرجعبه شنی اجرا شود،
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);
As the executor now has a disabled sandbox until it is notified by the
Sandboxee, we have to create a ::sandbox2::Client instance, set up
communication between the executor and Sandboxee, and then notify the executor
that our initialization is finished and we want to start sandboxing now by
calling 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 شود.
روش ۳: Custom Forkserver – آماده کردن باینری، منتظر ماندن برای درخواستهای fork، و قرار دادن در جعبه شنی بهصورت مستقل
این حالت به شما امکان میدهد یک فایل اجرایی را شروع کنید، آن را برای جعبه ایمن آماده کنید، و در لحظهای خاص از چرخه حیات فایل اجرایی، آن را برای اجراکننده دردسترس قرار دهید.
مجری درخواست انشعابی به دودویی شما ارسال میکند که 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 یک مقدار اولیه طولانی و CPU-intensive داشته باشد که بتواند قبلاز پردازش دادههای غیرقابلاعتماد اجرا شود.
برای نمونهای از این روش مجری، custom_fork را ببینید.
۲. ایجاد خطمشی جعبه شنی
وقتی مجری داشته باشید، احتمالاً میخواهید خطمشی جعبه شنی را برای «جعبه شنی» تعریف کنید. درغیراینصورت، Sandboxee فقط با خطمشی پیشفرض Syscall محافظت میشود.
هدف «خطمشی جعبه شنی» محدود کردن syscalls و آرگومانهایی است که Sandboxee میتواند ایجاد کند، و همچنین فایلهایی که میتواند به آنها دسترسی داشته باشد. باید درک دقیقی از فراخوانهای سیستمی موردنیاز کد مدنظرتان برای ایجاد محیط امن داشته باشید. یکی از راههای مشاهده syscalls اجرای کد با ابزار خط فرمان strace در Linux است.
پساز اینکه فهرست فراخوانهای سیستمی را دریافت کردید، میتوانید از PolicyBuilder برای تعریف خطمشی استفاده کنید.
PolicyBuilder با بسیاری از توابع کمکی و راحتی ارائه میشود که بسیاری از عملیاتهای رایج را ممکن میسازد (بسیاری از آنها قبلاً در DefaultPolicyBuilder() گنجانده شدهاند). فهرست زیر تنها بخش کوچکی از توابع دردسترس است:
- مجاز کردن هر فراخوان سیستمی برای راهاندازی پردازش:
AllowStaticStartup();AllowDynamicStartup();
- مجاز کردن هرگونه فراخوان سیستمی باز کردن/خواندن/نوشتن*:
AllowOpen();AllowRead();AllowWrite();
- اجازه دادن به همه فراخوانهای سیستمی مربوط به خروج/دسترسی/وضعیت:
AllowExit();AllowStat();AllowAccess();
- هرگونه فراخوانی سیستم مربوط به خواب/زمان را در فهرست مجازها قرار دهید:
AllowTime();AllowSleep();
این عملکردهای راحتی هر فراخوان سیستمی مرتبطی را در فهرست مجازها قرار میدهند. این کار این مزیت را دارد که میتوان از یک خطمشی یکسان در معماریهای مختلفی که در آنها برخیاز فراخوانهای سیستمی دردسترس نیستند (برای نمونه، ARM64 فراخوان سیستمی OPEN ندارد) استفاده کرد، اما با خطر امنیتی جزئی فعال کردن فراخوانهای سیستمی بیشتر از آنچه ممکن است لازم باشد. برای مثال، AllowOpen() به Sandboxee امکان میدهد هر syscall مرتبط بازی را فراخوانی کند. اگر
فقط میخواهید یک فراخوان سیستمی خاص را در فهرست مجازها قرار دهید، میتوانید از AllowSyscall(); استفاده کنید
برای قرار دادن چند فراخوان سیستمی در فهرست مجازها بهطور همزمان میتوانید از AllowSyscalls() استفاده کنید.
تاکنون این خطمشی فقط شناسه syscall را بررسی میکند. اگر نیاز دارید خطمشی را بیشتر تقویت کنید و میخواهید خطمشیای تعریف کنید که در آن فقط به syscall با آرگومانهای خاص اجازه دهید، باید از AddPolicyOnSyscall() یا AddPolicyOnSyscalls() استفاده کنید. این توابع نهتنها شناسه syscall را بهعنوان آرگومان میگیرند، بلکه از فیلتر seccomp-bpf خام بااستفاده از ماکروهای کمکی bpf از هسته Linux نیز استفاده میکنند. برای اطلاعات بیشتر درباره BPF،
مستندسازی هسته
را ببینید. اگر متوجه شدید که کد BPF تکراری مینویسید که فکر میکنید باید پوشاننده قابلیت استفاده داشته باشد، میتوانید درخواست ویژگی ارسال کنید.
جدااز توابع مربوط به syscall، PolicyBuilder تعدادی تابع مربوط به سیستم فایل نیز ارائه میدهد، مثل AddFile() یا AddDirectory()، برای bind-mount کردن فایل/دایرکتوری در sandbox. از دستیار 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();
}
۳. تنظیم کردن محدودیتها
«خطمشی جعبه امن» مانع از این میشود که «جعبه امن» فراخوانهای سیستمی خاصی را فراخوانی کند و بنابراین سطح حمله را کاهش میدهد. بااینحال، مهاجم ممکن است همچنان بتواند با اجرای نامحدود فرایند یا تمام کردن RAM و منابع دیگر، اثرات نامطلوبی ایجاد کند.
برای مقابله با این تهدید، 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، به نمونه
ابزار مراجعه کنید.
۴. اجرای جعبه شنی
در بخشهای قبلی، محیط جعبه شنی، خطمشی، و
مجری و 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();
اجرا بهصورت ناهمگام
همچنین میتوانید جعبه امن را بهصورت ناهمگام اجرا کنید، بنابراین تا زمانی که نتیجهای حاصل نشود، مسدود نمیشود. این کار برای مثال هنگام برقراری ارتباط با 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 را ببینید) میتوان برای ارسال عدد صحیح، رشته، میانگیر بایت، پروتوباف، یا توصیفگر فایل استفاده کرد.
همرسانی توصیفگرهای فایل
بااستفاده از میانای برنامهسازی کاربردی «ارتباط بین فرایندی» (به
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);
استفاده از comms API
Sandbox2 واسط برنامهسازی کاربردی ارتباطات مناسبی ارائه میدهد. این روشی ساده و آسان برای همرسانی کردن اعداد صحیح، رشتهها، یا میانگیرهای بایت بین مجری و Sandboxee است. در زیر چند تکهکد آورده شده است که میتوانید در مثال crc4 پیدا کنید.
برای شروع کار با میانای برنامهسازی کاربردی comms، ابتدا باید شیء comms را از شیء Sandbox2 دریافت کنید:
sandbox2::Comms* comms = s2.comms();
پساز دردسترس قرار گرفتن شیء ارتباطات، میتوان دادهها را بااستفاده از یکی از عملکردهای خانواده Send* به Sandboxee ارسال کرد. میتوانید نمونهای از استفاده از میانای برنامهسازی کاربردی ارتباطات را
در مثال 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 */
}
همرسانی دادهها با میانگیرها
یکی دیگر از عملکردهای همرسانی داده، استفاده از میانگیر API برای همرسانی مقادیر زیادی داده و اجتناب از نسخههای گرانقیمتی است که بین اجراکننده و Sandboxee ردوبدل میشود.
مجری یک «میانگیر» ایجاد میکند، یا براساس اندازه و دادههایی که باید منتقل شوند، یا مستقیماً
از یک توصیفگر فایل، و آن را بااستفاده از comms->SendFD()
در مجری و comms->RecvFD() در Sandboxee به Sandboxee منتقل میکند.
در تکهکد زیر، میتوانید طرف اجراکننده را ببینید. جعبه امن بهصورت ناهمزمان اجرا میشود و دادهها را ازطریق میانگیری با 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 */
۶. درحال خروج از جعبه شنی
بسته به اینکه جعبه امن را چگونه اجرا میکنید (به این مرحله مراجعه کنید)، باید روش خاتمه دادن به جعبه امن و درنتیجه Sandboxee را نیز اصلاح کنید.
درحال خروج از جعبه امنی که بهصورت همزمان اجرا میشود
اگر جعبه شنی بهصورت همزمان اجرا شده باشد، «اجرا» فقط زمانی برمیگردد که 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();
۷. آزمایش
مانند هر کد دیگری، پیادهسازی جعبه امن شما باید آزمایشهایی داشته باشد. آزمایشهای جعبه امن برای آزمایش صحت برنامه نیست، بلکه برای بررسی این است که آیا برنامه جعبه امن میتواند بدون مشکلاتی مانند نقض جعبه امن اجرا شود یا نه. این کار همچنین تضمین میکند که خطمشی جعبه امن درست است.
برنامه محدودشده به همان روشی آزمایش میشود که آن را در تولید اجرا میکنید، با آرگومانها و فایلهای ورودی که معمولاً پردازش میکند.
این آزمایشها میتواند به سادگی یک آزمایش پوسته یا آزمایشهای C++ بااستفاده از زیرفرایندها باشد. برای الهام گرفتن، نمونهها را بررسی کنید.
نتیجهگیری
از اینکه تا اینجا خواندید سپاسگزاریم، امیدواریم راهنمای ما را پسندیده باشید و اکنون احساس توانمندی کنید تا جعبههای ایمنی خودتان را ایجاد کنید و به حفظ امنیت کاربران کمک کنید.
ایجاد کردن جعبههای ایمنی و خطمشیها کار دشواری است و مستعد خطاهای جزئی است. برای اینکه خیالتان راحت باشد، توصیه میکنیم از یک کارشناس امنیتی بخواهید خطمشی و کد شما را بازبینی کند.