شروع کار با Sandbox2

در این صفحه یاد می گیرید که چگونه با Sandbox2 محیط سندباکس خود را بسازید. شما یاد خواهید گرفت که چگونه یک خط مشی Sandbox را تعریف کنید و برخی از ترفندهای پیشرفته، اما رایج را یاد خواهید گرفت. از اطلاعات اینجا به عنوان راهنما، در کنار مثال ها و مستندات کد در فایل های هدر استفاده کنید.

1. یک روش اجرای Sandbox را انتخاب کنید

Sandboxing با یک مجری شروع می شود (به Sandbox Executor مراجعه کنید)، که مسئول اجرای Sandboxee است. فایل هدر executor.h حاوی API مورد نیاز برای این منظور است. API بسیار منعطف است و به شما امکان می دهد بهترین گزینه را برای مورد استفاده خود انتخاب کنید. بخش های زیر 3 روش مختلف را که می توانید از بین آنها انتخاب کنید، شرح می دهد.

روش 1: مستقل - یک باینری را با سندباکس از قبل فعال کنید

این ساده‌ترین راه برای استفاده از sandboxing است و زمانی که می‌خواهید یک باینری کامل را که هیچ کد منبعی برای آن ندارید، جعبه سندباکس کنید، توصیه می‌شود. همچنین ایمن‌ترین راه برای استفاده از sandboxing است، زیرا هیچ مقدار اولیه بدون سندباکس وجود ندارد که بتواند اثرات نامطلوبی داشته باشد.

در قطعه کد زیر، مسیر باینری را برای sandbox و آرگومان هایی که باید به یک execve syscall ارسال کنیم، تعریف می کنیم. همانطور که در فایل هدر executor.h مشاهده می کنید، مقداری برای envp تعیین نمی کنیم و بنابراین محیط را از فرآیند والد کپی می کنیم. به یاد داشته باشید، اولین آرگومان همیشه نام برنامه ای است که باید اجرا شود، و قطعه ما هیچ آرگومان دیگری را تعریف نمی کند.

نمونه هایی از این روش اجرا کننده عبارتند از: استاتیک و ابزار .

#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 - به مجری بگویید چه زمانی باید Sandbox شود

این روش انعطاف‌پذیری را ارائه می‌دهد که در طول مقداردهی اولیه، جعبه‌بندی نشده باشد، و سپس با فراخوانی ::sandbox2::Client::SandboxMeHere() انتخاب کنید که چه زمانی وارد sandbox شود. این نیاز دارد که بتوانید زمانی را که می خواهید sandboxing را شروع کنید در کد تعریف کنید، و باید تک رشته ای باشد (چرا در پرسش های متداول بخوانید).

در قطعه کد زیر، از همان کدی استفاده می کنیم که در روش 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);

از آنجایی که اجراکننده اکنون یک جعبه ایمنی غیرفعال دارد تا زمانی که Sandboxee به آن اطلاع داده شود، باید یک نمونه ::sandbox2::Client ایجاد کنیم، ارتباط بین اجراکننده و Sandboxee را تنظیم کنیم و سپس به اجراکننده اطلاع دهیم که مقداردهی اولیه ما به پایان رسیده است و ما می‌خواهید اکنون با فراخوانی sandbox2_client.SandboxMeHere() sandbox را شروع کنید.

// 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 شود.

روش 3: Forkserver سفارشی - یک باینری آماده کنید، منتظر درخواست فورک و جعبه سند باشید.

این حالت به شما امکان می دهد یک باینری را راه اندازی کنید، آن را برای sandboxing آماده کنید و در یک لحظه خاص از چرخه حیات باینری خود، آن را در اختیار مجری قرار دهید.

مجری یک درخواست فورک را به باینری شما ارسال می کند، که fork() (از طریق ::sandbox2::ForkingClient::WaitAndFork() ) ارسال می کند. فرآیند ایجاد شده جدید برای جعبه‌بندی با ::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 را ببینید.

2. یک Sandbox Policy ایجاد کنید

هنگامی که یک مجری دارید، احتمالاً می خواهید یک سیاست Sandbox برای Sandboxee تعریف کنید. در غیر این صورت، Sandboxee فقط توسط خط‌مشی Syscall پیش‌فرض محافظت می‌شود.

با سیاست Sandbox، هدف محدود کردن syscals و آرگومان‌هایی است که Sandboxee می‌تواند ایجاد کند، و همچنین فایل‌هایی که می‌تواند به آن دسترسی داشته باشد. شما باید درک دقیقی از syscals های مورد نیاز کدی که قصد دارید sandbox را وارد کنید داشته باشید. یکی از راه های مشاهده syscalls، اجرای کد با خط فرمان خط ابزار لینوکس است.

هنگامی که لیستی از syscalls را دارید، می توانید از PolicyBuilder برای تعریف خط مشی استفاده کنید. PolicyBuilder با بسیاری از توابع راحتی و کمکی ارائه می شود که امکان انجام بسیاری از عملیات های رایج را فراهم می کند. لیست زیر تنها گزیده ای کوچک از توابع موجود است:

  • Allowlist هر syscall برای راه اندازی فرآیند:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Allowlist هر سیستم باز /خواندن /نوشتن*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Allowlist هر گونه خروجی/دسترسی/وضعیت سیستم مرتبط:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • هر گونه پیام‌های مرتبط با خواب/زمان را فهرست کنید:
    • AllowTime();
    • AllowSleep();

این توابع راحتی هر گونه syscall مرتبط را لیست می کنند. این مزیت را دارد که می‌توان از یک خط‌مشی روی معماری‌های مختلف استفاده کرد، جایی که syscals خاصی در دسترس نیست (مثلا ARM64 هیچ syscall OPEN ندارد)، اما با خطر امنیتی جزئی فعال کردن sycsall‌های بیشتر از آنچه لازم است. برای مثال، AllowOpen() Sandboxee را قادر می‌سازد تا هر سیستمی باز مرتبط را فراخوانی کند. اگر می خواهید فقط یک syscall خاص را در لیست allow قرار دهید، می توانید از AllowSyscall(); برای اجازه دادن به چندین syscalls در یک زمان می توانید از AllowSyscalls() استفاده کنید.

تا کنون این خط مشی فقط شناسه syscall را بررسی می کند. اگر نیاز به تقویت بیشتر این خط مشی دارید و می خواهید سیاستی را تعریف کنید که در آن فقط یک syscall با آرگومان های خاص مجاز باشد، باید از AddPolicyOnSyscall() یا AddPolicyOnSyscalls() استفاده کنید. این توابع نه تنها شناسه syscall را به عنوان آرگومان می گیرند، بلکه یک فیلتر خام seccomp-bpf را نیز با استفاده از ماکروهای کمکی bpf از هسته لینوکس می گیرند. برای اطلاعات بیشتر در مورد BPF به اسناد هسته مراجعه کنید. اگر متوجه شدید که کد BPF تکراری می نویسید که فکر می کنید باید دارای قابلیت استفاده باشد، در صورت تمایل یک درخواست ویژگی را ثبت کنید.

جدا از توابع مربوط به syscall، PolicyBuilder همچنین تعدادی توابع مرتبط با فایل سیستم مانند AddFile() یا AddDirectory() را برای متصل کردن یک فایل/دایرکتوری به sandbox ارائه می کند. کمک کننده AddTmpfs() می تواند برای افزودن یک ذخیره سازی موقت فایل در جعبه شنی استفاده شود.

یک تابع بسیار مفید AddLibrariesForBinary() است که کتابخانه ها و پیوند دهنده مورد نیاز یک باینری را اضافه می کند.

متأسفانه ارائه syscalls به لیست مجاز هنوز کمی کار دستی است. یک خط مشی با syscalls که نیازهای باینری خود را می دانید ایجاد کنید و آن را با حجم کاری مشترک اجرا کنید. اگر تخلفی ایجاد شد، syscall را در لیست allow قرار دهید و این فرآیند را تکرار کنید. اگر با تخلفی مواجه شدید که فکر می‌کنید ممکن است در لیست مجوزها خطرناک باشد و برنامه به‌خوبی خطاها را مدیریت می‌کند، می‌توانید به جای آن با 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. محدودیت ها را تنظیم کنید

خط مشی Sandbox از فراخوانی 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 ، ابزار نمونه را ببینید.

4. Sandbox را اجرا کنید

در قسمت های قبل محیط sandboxed، Policy و executor و 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();

5. ارتباط با Sandboxee

به طور پیش فرض، مجری می تواند با Sandboxee از طریق توصیفگرهای فایل ارتباط برقرار کند. این ممکن است تمام چیزی باشد که به آن نیاز دارید، برای مثال اگر فقط می خواهید یک فایل را با Sandboxee به اشتراک بگذارید، یا خروجی استاندارد Sandboxee را بخوانید.

با این حال، شما به احتمال زیاد نیاز به منطق ارتباطی پیچیده تری بین اجراکننده و Sandboxee دارید. comms API (به فایل هدر comms.h مراجعه کنید) می تواند برای ارسال اعداد صحیح، رشته ها، بافرهای بایت، پروتوباف ها یا توصیفگرهای فایل استفاده شود.

به اشتراک گذاری توصیف کننده های فایل

با استفاده از Inter-Process Communication API (به 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() برای ایجاد یک نقطه پایانی جفت سوکت استفاده کنید. این می تواند برای خواندن خروجی استاندارد Sandboxee یا خطاهای استاندارد استفاده شود. یک مثال استفاده را می توان در ابزار مشاهده کرد.

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

با استفاده از comms API

Sandbox2 یک API comms راحت ارائه می دهد. این یک راه ساده و آسان برای به اشتراک گذاشتن اعداد صحیح، رشته‌ها یا بافرهای بایت بین اجراکننده و Sandboxee است. در زیر چند قطعه کد وجود دارد که می توانید در مثال crc4 بیابید.

برای شروع کار با comms API، ابتدا باید شی comms را از شی Sandbox2 دریافت کنید:

sandbox2::Comms* comms = s2.comms();

هنگامی که شی comms در دسترس است، داده ها را می توان با استفاده از یکی از خانواده توابع Send* به Sandboxee ارسال کرد. می توانید نمونه ای از استفاده از comms 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 برای اشتراک‌گذاری مقادیر زیادی از داده‌ها و جلوگیری از کپی‌های گران‌قیمتی است که بین اجراکننده و 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 */

6. خروج از جعبه شنی

بسته به نحوه اجرای sandbox ( این مرحله را ببینید)، باید نحوه پایان دادن به sandbox و بنابراین Sandboxee را تنظیم کنید.

خروج از جعبه شنی که به طور همزمان اجرا می شود

اگر جعبه شنی به طور همزمان در حال اجرا بوده باشد، 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. تست کنید

مانند هر کد دیگری، پیاده سازی سندباکس شما باید تست هایی داشته باشد. آزمایش‌های جعبه ایمنی برای آزمایش درستی برنامه نیستند، بلکه در عوض بررسی می‌کنند که آیا برنامه Sandbox می‌تواند بدون مشکلاتی مانند نقض جعبه ایمنی اجرا شود یا خیر. این همچنین مطمئن می شود که خط مشی sandbox درست است.

یک برنامه sandboxed به همان روشی که در مرحله تولید اجرا می‌کنید، با آرگومان‌ها و فایل‌های ورودی که معمولاً پردازش می‌کند، آزمایش می‌شود.

این تست ها می توانند به سادگی تست پوسته یا C++ با استفاده از فرآیندهای فرعی باشند. برای الهام گرفتن نمونه ها را بررسی کنید.

نتیجه

از اینکه تا اینجا خواندید متشکریم، امیدواریم از راهنمای ما خوشتان آمده باشد و اکنون احساس قدرت کنید تا جعبه های ماسه ای خود را برای کمک به حفظ امنیت کاربران خود ایجاد کنید.

ایجاد جعبه های ماسه ای و خط مشی ها کار دشواری است و مستعد خطاهای ظریف است. برای حفظ امنیت، توصیه می کنیم از یک متخصص امنیتی بخواهید خط مشی و کد شما را بررسی کند.

،

در این صفحه یاد می گیرید که چگونه با Sandbox2 محیط سندباکس خود را بسازید. شما یاد خواهید گرفت که چگونه یک خط مشی Sandbox را تعریف کنید و برخی از ترفندهای پیشرفته، اما رایج را یاد خواهید گرفت. از اطلاعات اینجا به عنوان راهنما، در کنار مثال ها و مستندات کد در فایل های هدر استفاده کنید.

1. یک روش اجرای Sandbox را انتخاب کنید

Sandboxing با یک مجری شروع می شود (به Sandbox Executor مراجعه کنید)، که مسئول اجرای Sandboxee است. فایل هدر executor.h حاوی API مورد نیاز برای این منظور است. API بسیار منعطف است و به شما امکان می دهد بهترین گزینه را برای مورد استفاده خود انتخاب کنید. بخش های زیر 3 روش مختلف را که می توانید از بین آنها انتخاب کنید، شرح می دهد.

روش 1: مستقل - یک باینری را با سندباکس از قبل فعال کنید

این ساده‌ترین راه برای استفاده از sandboxing است و زمانی که می‌خواهید یک باینری کامل را که هیچ کد منبعی برای آن ندارید، جعبه سندباکس کنید، توصیه می‌شود. همچنین ایمن‌ترین راه برای استفاده از sandboxing است، زیرا هیچ مقدار اولیه بدون سندباکس وجود ندارد که بتواند اثرات نامطلوبی داشته باشد.

در قطعه کد زیر، مسیر باینری را برای sandbox و آرگومان هایی که باید به یک execve syscall ارسال کنیم، تعریف می کنیم. همانطور که در فایل هدر executor.h مشاهده می کنید، مقداری برای envp تعیین نمی کنیم و بنابراین محیط را از فرآیند والد کپی می کنیم. به یاد داشته باشید، اولین آرگومان همیشه نام برنامه ای است که باید اجرا شود، و قطعه ما هیچ آرگومان دیگری را تعریف نمی کند.

نمونه هایی از این روش اجرا کننده عبارتند از: استاتیک و ابزار .

#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 - به مجری بگویید چه زمانی باید Sandbox شود

این روش انعطاف‌پذیری را ارائه می‌دهد که در طول مقداردهی اولیه، جعبه‌بندی نشده باشد، و سپس با فراخوانی ::sandbox2::Client::SandboxMeHere() انتخاب کنید که چه زمانی وارد sandbox شود. این نیاز دارد که بتوانید زمانی را که می خواهید sandboxing را شروع کنید در کد تعریف کنید، و باید تک رشته ای باشد (چرا در پرسش های متداول بخوانید).

در قطعه کد زیر، از همان کدی استفاده می کنیم که در روش 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);

از آنجایی که اجراکننده اکنون یک جعبه ایمنی غیرفعال دارد تا زمانی که Sandboxee به آن اطلاع داده شود، باید یک نمونه ::sandbox2::Client ایجاد کنیم، ارتباط بین اجراکننده و Sandboxee را تنظیم کنیم و سپس به اجراکننده اطلاع دهیم که مقداردهی اولیه ما به پایان رسیده است و ما می‌خواهید اکنون با فراخوانی sandbox2_client.SandboxMeHere() sandbox را شروع کنید.

// 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 شود.

روش 3: Forkserver سفارشی - یک باینری آماده کنید، منتظر درخواست فورک و جعبه سند باشید.

این حالت به شما امکان می دهد یک باینری را راه اندازی کنید، آن را برای sandboxing آماده کنید و در یک لحظه خاص از چرخه حیات باینری خود، آن را در اختیار مجری قرار دهید.

مجری یک درخواست فورک را به باینری شما ارسال می کند، که fork() (از طریق ::sandbox2::ForkingClient::WaitAndFork() ) ارسال می کند. فرآیند ایجاد شده جدید برای جعبه‌بندی با ::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 را ببینید.

2. یک Sandbox Policy ایجاد کنید

هنگامی که یک مجری دارید، احتمالاً می خواهید یک سیاست Sandbox برای Sandboxee تعریف کنید. در غیر این صورت، Sandboxee فقط توسط خط‌مشی Syscall پیش‌فرض محافظت می‌شود.

با سیاست Sandbox، هدف محدود کردن syscals و آرگومان‌هایی است که Sandboxee می‌تواند ایجاد کند، و همچنین فایل‌هایی که می‌تواند به آن دسترسی داشته باشد. شما باید درک دقیقی از syscals های مورد نیاز کدی که قصد دارید sandbox را وارد کنید داشته باشید. یکی از راه های مشاهده syscalls، اجرای کد با خط فرمان خط ابزار لینوکس است.

هنگامی که لیستی از syscalls را دارید، می توانید از PolicyBuilder برای تعریف خط مشی استفاده کنید. PolicyBuilder با بسیاری از توابع راحتی و کمکی ارائه می شود که امکان انجام بسیاری از عملیات های رایج را فراهم می کند. لیست زیر تنها گزیده ای کوچک از توابع موجود است:

  • Allowlist هر syscall برای راه اندازی فرآیند:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Allowlist هر سیستم باز /خواندن /نوشتن*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Allowlist هر گونه خروجی/دسترسی/وضعیت سیستم مرتبط:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • هر گونه پیام‌های مرتبط با خواب/زمان را فهرست کنید:
    • AllowTime();
    • AllowSleep();

این توابع راحتی هر گونه syscall مرتبط را لیست می کنند. این مزیت را دارد که می‌توان از یک خط‌مشی روی معماری‌های مختلف استفاده کرد، جایی که syscals خاصی در دسترس نیست (مثلا ARM64 هیچ syscall OPEN ندارد)، اما با خطر امنیتی جزئی فعال کردن sycsall‌های بیشتر از آنچه لازم است. برای مثال، AllowOpen() Sandboxee را قادر می‌سازد تا هر سیستمی باز مرتبط را فراخوانی کند. اگر می خواهید فقط یک syscall خاص را در لیست allow قرار دهید، می توانید از AllowSyscall(); برای اجازه دادن به چندین syscalls در یک زمان می توانید از AllowSyscalls() استفاده کنید.

تا کنون این خط مشی فقط شناسه syscall را بررسی می کند. اگر نیاز به تقویت بیشتر این خط مشی دارید و می خواهید سیاستی را تعریف کنید که در آن فقط یک syscall با آرگومان های خاص مجاز باشد، باید از AddPolicyOnSyscall() یا AddPolicyOnSyscalls() استفاده کنید. این توابع نه تنها شناسه syscall را به عنوان آرگومان می گیرند، بلکه یک فیلتر خام seccomp-bpf را نیز با استفاده از ماکروهای کمکی bpf از هسته لینوکس می گیرند. برای اطلاعات بیشتر در مورد BPF به اسناد هسته مراجعه کنید. اگر متوجه شدید که کد BPF تکراری می نویسید که فکر می کنید باید دارای قابلیت استفاده باشد، در صورت تمایل یک درخواست ویژگی را ثبت کنید.

جدا از توابع مربوط به syscall، PolicyBuilder همچنین تعدادی توابع مرتبط با فایل سیستم مانند AddFile() یا AddDirectory() را برای متصل کردن یک فایل/دایرکتوری به sandbox ارائه می کند. کمک کننده AddTmpfs() می تواند برای افزودن یک ذخیره سازی موقت فایل در جعبه شنی استفاده شود.

یک تابع بسیار مفید AddLibrariesForBinary() است که کتابخانه ها و پیوند دهنده مورد نیاز یک باینری را اضافه می کند.

متأسفانه ارائه syscalls به لیست مجاز هنوز کمی کار دستی است. یک خط مشی با syscalls که نیازهای باینری خود را می دانید ایجاد کنید و آن را با حجم کاری مشترک اجرا کنید. اگر تخلفی ایجاد شد، syscall را در لیست allow قرار دهید و این فرآیند را تکرار کنید. اگر با تخلفی مواجه شدید که فکر می‌کنید ممکن است در لیست مجوزها خطرناک باشد و برنامه به‌خوبی خطاها را مدیریت می‌کند، می‌توانید به جای آن با 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. محدودیت ها را تنظیم کنید

خط مشی Sandbox از فراخوانی 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 ، ابزار نمونه را ببینید.

4. Sandbox را اجرا کنید

در قسمت های قبل محیط sandboxed، Policy و executor و 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();

5. ارتباط با Sandboxee

به طور پیش فرض، مجری می تواند با Sandboxee از طریق توصیفگرهای فایل ارتباط برقرار کند. این ممکن است تمام چیزی باشد که به آن نیاز دارید، برای مثال اگر فقط می خواهید یک فایل را با Sandboxee به اشتراک بگذارید، یا خروجی استاندارد Sandboxee را بخوانید.

با این حال، شما به احتمال زیاد نیاز به منطق ارتباطی پیچیده تری بین اجراکننده و Sandboxee دارید. comms API (به فایل هدر comms.h مراجعه کنید) می تواند برای ارسال اعداد صحیح، رشته ها، بافرهای بایت، پروتوباف ها یا توصیفگرهای فایل استفاده شود.

به اشتراک گذاری توصیف کننده های فایل

با استفاده از Inter-Process Communication API (به 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() برای ایجاد یک نقطه پایانی جفت سوکت استفاده کنید. این می تواند برای خواندن خروجی استاندارد Sandboxee یا خطاهای استاندارد استفاده شود. یک مثال استفاده را می توان در ابزار مشاهده کرد.

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

با استفاده از comms API

Sandbox2 یک API comms راحت ارائه می دهد. این یک راه ساده و آسان برای به اشتراک گذاشتن اعداد صحیح، رشته‌ها یا بافرهای بایت بین اجراکننده و Sandboxee است. در زیر چند قطعه کد وجود دارد که می توانید در مثال crc4 بیابید.

برای شروع کار با comms API، ابتدا باید شی comms را از شی Sandbox2 دریافت کنید:

sandbox2::Comms* comms = s2.comms();

هنگامی که شی comms در دسترس است، داده ها را می توان با استفاده از یکی از خانواده توابع Send* به Sandboxee ارسال کرد. می توانید نمونه ای از استفاده از comms 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 برای اشتراک‌گذاری مقادیر زیادی از داده‌ها و جلوگیری از کپی‌های گران‌قیمتی است که بین اجراکننده و 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 */

6. خروج از جعبه شنی

بسته به نحوه اجرای sandbox ( این مرحله را ببینید)، باید نحوه پایان دادن به sandbox و بنابراین Sandboxee را تنظیم کنید.

خروج از جعبه شنی که به طور همزمان اجرا می شود

اگر جعبه شنی به طور همزمان در حال اجرا بوده باشد، 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. تست کنید

مانند هر کد دیگری، پیاده سازی سندباکس شما باید تست هایی داشته باشد. آزمایش‌های جعبه ایمنی برای آزمایش درستی برنامه نیستند، بلکه در عوض بررسی می‌کنند که آیا برنامه Sandbox می‌تواند بدون مشکلاتی مانند نقض جعبه ایمنی اجرا شود یا خیر. این همچنین مطمئن می شود که خط مشی sandbox درست است.

یک برنامه sandboxed به همان روشی که در مرحله تولید اجرا می‌کنید، با آرگومان‌ها و فایل‌های ورودی که معمولاً پردازش می‌کند، آزمایش می‌شود.

این تست ها می توانند به سادگی تست پوسته یا C++ با استفاده از فرآیندهای فرعی باشند. برای الهام گرفتن نمونه ها را بررسی کنید.

نتیجه

از اینکه تا اینجا خواندید متشکریم، امیدواریم از راهنمای ما خوشتان آمده باشد و اکنون احساس قدرت کنید تا جعبه های ماسه ای خود را برای کمک به حفظ امنیت کاربران خود ایجاد کنید.

ایجاد جعبه های ماسه ای و خط مشی ها کار دشواری است و مستعد خطاهای ظریف است. برای حفظ امنیت، توصیه می کنیم از یک متخصص امنیتی بخواهید خط مشی و کد شما را بررسی کند.