در این صفحه، با نحوه ایجاد محیط جعبه شنی خودتان با Sandbox2 آشنا میشوید. با نحوه تعریف کردن خطمشی جعبه شنی و برخیاز ترفندهای پیشرفته اما رایج آشنا خواهید شد. از اطلاعات اینجا بهعنوان راهنما، همراه با مثالها و اسناد کد در فایلهای سرصفحه استفاده کنید.
۱. انتخاب روش Sandbox Executor
جعبه شنی با یک مجری شروع میشود (به مجری جعبه شنی مراجعه کنید) که مسئول اجرای جعبه شنی است. فایل سرایند executor.h حاوی میانای برنامهسازی کاربردی موردنیاز برای این هدف است. این API بسیار انعطافپذیر است و به شما امکان میدهد آنچه را که برای مورد استفادهتان مناسبتر است انتخاب کنید. بخشهای زیر ۳ روش مختلف را که میتوانید از بین آنها انتخاب کنید شرح میدهد.
روش ۱: مستقل – اجرای فایل باینری با فعال بودن قبلی محیط امن
این سادهترین راه برای استفاده از جعبه امن است و روش توصیهشدهای است که میخواهید کل باینری را که کد منبعی برای آن ندارید در جعبه امن قرار دهید. این روش همچنین ایمنترین راه برای استفاده از جعبه امن است، زیرا هیچ مقداردهی اولیه بدون جعبه امنی وجود ندارد که بتواند اثرات نامطلوب داشته باشد.
در تکهکد زیر، مسیر فایل اجرایی را برای قرار گرفتن در محیط امن تعریف میکنیم
و آرگومانهایی را که باید به فراخوان سیستمی 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::EnterForkLook()). فرایند جدید ایجادشده
آماده خواهد بود تا با ::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 میتواند ایجاد کند، و همچنین فایلهایی که میتواند به آنها دسترسی داشته باشد. باید درک دقیقی از syscallهای موردنیاز کدی که قصد دارید در محیط امن اجرا کنید داشته باشید. یکی از روشهای مشاهده فراخوانهای سیستمی، اجرای کد با ابزار خط فرمان strace در Linux است.
وقتی فهرست فراخوانیهای سیستم را دریافت کردید، میتوانید از PolicyBuilder برای تعریف خطمشی استفاده کنید.
PolicyBuilder با بسیاری از توابع کمکی و راحتی ارائه میشود که بسیاری از عملیاتهای رایج را ممکن میسازد (بسیاری از آنها قبلاً در DefaultPolicyBuilder() گنجانده شدهاند). فهرست زیر تنها بخش کوچکی از توابع دردسترس است:
- هر فراخوان سیستمی را برای راهاندازی فرایند در فهرست مجاز قرار دهید:
AllowStaticStartup();AllowDynamicStartup();
- هرگونه فراخوانی سیستم open/read/write* را در فهرست مجاز قرار دهید:
AllowOpen();AllowRead();AllowWrite();
- هرگونه فراخوانی سیستم مربوط به خروج/دسترسی/وضعیت را در فهرست مجازها قرار دهید:
AllowExit();AllowStat();AllowAccess();
- هرگونه فراخوانی سیستم مربوط به خواب/زمان را در فهرست مجازها قرار دهید:
AllowTime();AllowSleep();
این توابع راحتی هر فراخوان سیستمی مربوطه را در فهرست مجاز قرار میدهند. این کار این مزیت را دارد که میتوان از یک خطمشی یکسان در معماریهای مختلفی که در آنها برخیاز فراخوانهای سیستمی دردسترس نیستند (برای نمونه، ARM64 فراخوان سیستمی OPEN ندارد) استفاده کرد، اما با خطر امنیتی جزئی فعال کردن فراخوانهای سیستمی بیشتر از آنچه ممکن است لازم باشد. برای مثال، AllowOpen() به Sandboxee اجازه میدهد هر سیستمفراخوان مربوط به باز کردن را فراخوانی کند. اگر
فقط میخواهید یک فراخوان سیستم خاص را در فهرست مجازها قرار دهید، میتوانید از AllowSyscall(); استفاده کنید
برای قرار دادن چند فراخوان سیستم بهطور همزمان در فهرست مجازها، میتوانید از AllowSyscalls() استفاده کنید.
تاکنون این خطمشی فقط شناسه syscall را بررسی میکند. اگر نیاز دارید خطمشی را بیشتر تقویت کنید و میخواهید خطمشیای تعریف کنید که در آن فقط به syscall با آرگومانهای خاص اجازه دهید، باید از AddPolicyOnSyscall() یا AddPolicyOnSyscalls() استفاده کنید. این توابع نهتنها شناسه syscall را بهعنوان آرگومان میگیرند، بلکه از فیلتر seccomp-bpf خام بااستفاده از ماکروهای کمکی bpf از هسته Linux نیز استفاده میکنند. برای اطلاعات بیشتر درباره BPF،
مستندسازی هسته
را ببینید. اگر متوجه شدید که کد BPF تکراری مینویسید که فکر میکنید باید پوشاننده قابلیت استفاده داشته باشد، میتوانید درخواست ویژگی ارسال کنید.
جدااز توابع مربوط به syscall، PolicyBuilder تعدادی تابع مربوط به سیستم فایل نیز ارائه میدهد، مثل AddFile() یا AddDirectory()، برای bind-mount کردن فایل/دایرکتوری در sandbox. از دستیار AddTmpfs() میتوان برای افزودن فضای ذخیرهسازی فایل موقت در چارچوب امن استفاده کرد.
یکی از عملکردهای بسیار مفید AddLibrariesForBinary() است که
کتابخانهها و پیونددهنده موردنیاز یک باینری را اضافه میکند.
متأسفانه، ارائه syscalls برای فهرست مجازها هنوز کمی کار دستی است. خطمشیای با فراخوانهای سیستمی که میدانید دودوییتان نیاز دارد ایجاد کنید و
آن را با حجم کاری معمول اجرا کنید. اگر نقضی روی داد، فراخوان سیستم را در فهرست مجاز قرار دهید و فرایند را تکرار کنید. اگر با نقضی مواجه شدید که فکر میکنید قرار دادن آن در فهرست مجاز ممکن است خطرناک باشد و برنامه خطاها را بهدرستی مدیریت میکند، میتوانید بااستفاده از 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();
}
۳. تنظیم کردن محدودیتها
«خطمشی جعبه امن» مانع از این میشود که Sandboxee با فراخوانهای سیستمی خاصی تماس بگیرد و بهاینترتیب سطح حمله را کاهش میدهد. بااینحال، مهاجم ممکن است همچنان بتواند با اجرای یک فرایند بهطور نامحدود یا مصرف بیشازحد 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 و اجرای همزمان آن را نشان میدهد. برای مثال دقیقتر، ایستا را ببینید.
#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();
۵. برقراری ارتباط با Sandboxee
بهطور پیشفرض، اجراکننده میتواند ازطریق توصیفگرهای فایل با Sandboxee ارتباط برقرار کند. این ممکن است تمام چیزی باشد که نیاز دارید، برای مثال اگر فقط میخواهید فایلی را با Sandboxee همرسانی کنید یا برونداد استاندارد Sandboxee را بخوانید.
بااینحال، به احتمال زیاد به منطق ارتباطی پیچیدهتری بین اجراکننده و Sandboxee نیاز دارید. از API ارتباطات (فایل سرایند comms.h را ببینید) میتوان برای ارسال عدد صحیح، رشته، میانگیر بایت، پروتوباف، یا توصیفگر فایل استفاده کرد.
همرسانی توصیفگرهای فایل
بااستفاده از «میانای برنامهسازی کاربردی ارتباط بینفرایندی» (به
ipc.h مراجعه کنید)،
میتوانید از MapFd() یا ReceiveFd() استفاده کنید:
از
MapFd()برای نگاشت توصیفگرهای فایل از اجراکننده به Sandboxee استفاده کنید. از این مورد میتوان برای همرسانی فایلی که از اجراکننده باز شده است برای استفاده در Sandboxee استفاده کرد. نمونهای از استفاده را میتوانید در ایستا ببینید.// 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 یک comms API راحت ارائه میدهد. این روشی ساده و آسان برای همرسانی کردن اعداد صحیح، رشتهها، یا میانگیرهای بایت بین مجری و Sandboxee است. در زیر چند تکهکد آورده شده است که میتوانید در مثال crc4 پیدا کنید.
برای شروع کار با API ارتباطات، ابتدا باید شیء ارتباطات را از شیء Sandbox2 دریافت کنید:
sandbox2::Comms* comms = s2.comms();
Once the comms object is available, data can be sent to the Sandboxee using one
of the Send* family of functions. میتوانید نمونهای از استفاده از میانای برنامهسازی کاربردی ارتباطات را
در مثال 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 منتقل میکند.
در تکهکد زیر، میتوانید طرف اجراکننده را ببینید. جعبه امن بهصورت ناهمگام اجرا میشود و دادهها را ازطریق میانگیر با 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 را تنظیم کنید.
درحال خروج از جعبه امنی که بهصورت همزمان اجرا میشود
اگر جعبه امن بهصورت همگام اجرا شده باشد، «اجرا» فقط زمانی برمیگردد که «جعبه امن» تمام شده باشد. بنابراین، هیچ مرحله اضافی برای فسخ لازم نیست. تکهکد زیر این سناریو را نشان میدهد:
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();
۷. آزمایش
مانند هر کد دیگری، پیادهسازی جعبه امن شما باید آزمایشهایی داشته باشد. هدف از آزمایشهای Sandbox آزمایش درستی برنامه نیست، بلکه بررسی این است که آیا برنامه محدودشده میتواند بدون مشکلاتی مانند نقض محدودیتهای Sandbox اجرا شود یا نه. این کار همچنین تضمین میکند که خطمشی جعبه امن درست است.
برنامه جعبه ایمن به همان روشی آزمایش میشود که آن را در تولید اجرا میکنید، با آرگومانها و فایلهای ورودی که معمولاً پردازش میکند.
این آزمایشها میتواند به سادگی یک آزمایش پوسته یا آزمایشهای C++ بااستفاده از زیرفرایندها باشد. برای الهام گرفتن، مثالها را بررسی کنید.
نتیجهگیری
از اینکه تا اینجا خواندید سپاسگزاریم، امیدواریم از راهنمای ما خوشتان آمده باشد و اکنون احساس کنید که میتوانید محیطهای آزمایشی خودتان را برای کمک به حفظ ایمنی کاربران ایجاد کنید.
ایجاد جعبههای امن و خطمشیها کار دشواری است و مستعد خطاهای جزئی است. برای اطمینان از ایمن بودن، توصیه میکنیم از یک متخصص امنیت بخواهید خطمشی و کد شما را بررسی کند.