بدء استخدام Sandbox2

في هذه الصفحة، ستتعرّف على كيفية إنشاء بيئة وضع الحماية الخاصة بك باستخدام Sandbox2. سوف تتعلم كيفية تعريف سياسة وضع الحماية، وبعض التعديلات المتقدمة والشائعة. يمكنك استخدام المعلومات الواردة هنا كدليل إلى جانب الأمثلة ومستندات الرموز في ملفات العناوين.

1- اختيار طريقة تنفيذ وضع الحماية

يبدأ وضع الحماية مع جهة تنفيذ (راجِع Sandbox Executor)، وهو المسؤول عن تشغيل Sandboxee. يحتوي ملف العنوان executor.h على واجهة برمجة التطبيقات المطلوبة لهذا الغرض. وتتميز واجهة برمجة التطبيقات بالمرونة الشديدة وتتيح لك اختيار أفضل ما يناسب حالة الاستخدام. توضّح الأقسام التالية الطرق الثلاث المختلفة التي يمكنك الاختيار من بينها.

الطريقة 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);

الطريقة الثانية: خادم Sandbox2 Forkserver: أخبِر الجهة التنفيذية بالوقت المناسب لوضع الحماية.

توفر هذه الطريقة مرونة إلغاء وضع الحماية أثناء عملية الإعداد، ثم اختيار وقت الدخول في وضع الحماية من خلال الاتصال بـ ::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);

بما أنّ تنفيذ وضع الحماية غير مفعَّل الآن إلى أن يتم إبلاغه من خلال 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) عندما يجب دخول وضع الحماية.

الطريقة 3: خادم Forkserver مخصص: تحضير برنامج ثنائي وانتظار طلبات الشوكة ووضع وضع الحماية بنفسك

يتيح لك هذا الوضع بدء برنامج ثنائي، وإعداده لوضع الحماية، وجعله متاحًا للمسؤول في مرحلة معينة من دورة حياة البرنامج الثنائي.

سيرسل تنفيذ الإجراء طلب شوكة إلى برنامجك الثنائي، والذي سيتم 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. إنشاء سياسة وضع الحماية

بعد أن يكون لديك جهة تنفيذ، ستحتاج على الأرجح إلى تحديد سياسة وضع الحماية لتطبيق Sandboxee. وبخلاف ذلك، لا تتم حماية تطبيق Sandboxee إلا من خلال سياسة Syscall التلقائية.

تهدف سياسة وضع الحماية إلى تقييد مكالمات النظام والوسيطات التي يمكن أن ينشئها مستخدم الحماية، بالإضافة إلى الملفات التي يمكنه الوصول إليها. ستحتاج إلى فهم مفصل لاستدعاءات النظام المطلوبة من خلال الكود الذي تخطط لوضعه في وضع الحماية. تتمثل إحدى طرق مراقبة استدعاءات النظام في تشغيل التعليمات البرمجية باستخدام مسار أداة سطر الأوامر في Linux.

بمجرد حصولك على قائمة باستدعاءات النظام، يمكنك استخدام PolicyBuilder لتحديد السياسة. تتوفّر في "أداة إنشاء السياسات" العديد من الوظائف السهلة والمساعِدة التي تتيح إجراء العديد من العمليات الشائعة. القائمة التالية هي مجرد مقتطف صغير من الدوال المتاحة:

  • يمكنك إضافة أي طلب نظام إلى القائمة المسموح بها لبدء تشغيل العملية:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • يمكنك إضافة أي طلبات أنظمة /read/write* مفتوحة إلى القائمة المسموح بها:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • يمكنك إضافة أي مكالمات نظام ذات صلة بالخروج/الوصول/الحالة إلى القائمة المسموح بها:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • يمكنك إضافة أي طلبات أنظمة متعلّقة بالنوم أو الوقت إلى القائمة المسموح بها:
    • AllowTime();
    • AllowSleep();

تضيف وظائف الراحة هذه أي نظام ذات صلة إلى القائمة المسموح بها. ويتميز ذلك بأنه يمكن استخدام نفس السياسة على بُنى مختلفة حيث لا تكون بعض استدعاءات النظام غير متاحة (على سبيل المثال، لا يحتوي نظام ARM64 على استدعاء نظام مفتوح)، ولكن مع المخاطر الأمنية البسيطة التي تتمثل في تفعيل المزيد من Sycsall مما قد يكون ضروريًا. على سبيل المثال، يُمكِّن LetOpen() تطبيق Sandboxee من استدعاء أي نظام syscall مفتوح. إذا كنت تريد إضافة طلب نظام واحد فقط إلى القائمة المسموح بها، يمكنك استخدام AllowSyscall(); للسماح باستدعاءات أنظمة متعددة في الوقت نفسه، ويمكنك استخدام AllowSyscalls().

حتى الآن، تتحقق السياسة من مُعرّف syscall فقط. إذا كنت بحاجة إلى مزيد من تقوية السياسة وأردت تحديد سياسة تسمح فيها باستدعاء نظام فقط باستخدام وسيطات معيّنة، عليك استخدام AddPolicyOnSyscall() أو AddPolicyOnSyscalls(). لا تأخذ هذه الدوال معرّف طلب النظام كوسيطة فحسب، بل تستخدم أيضًا عامل تصفية seccomp-bpf أولي باستخدام وحدات ماكرو مساعد bpf من نواة Linux. راجِع مستندات النواة (kernel) للحصول على مزيد من المعلومات حول BPF. إذا وجدت نفسك تكتب كود BPF متكررًا تعتقد أنه يجب أن يكون به برنامج تضمين قابلية الاستخدام، فلا تتردد في تقديم طلب ميزة.

بالإضافة إلى الدوال المرتبطة باستدعاء النظام، يوفّر PolicyBuilder أيضًا عددًا من الدوال ذات الصلة بنظام الملفات، مثل AddFile() أو AddDirectory() لربط ملف/دليل في وضع الحماية. يمكن استخدام مساعد AddTmpfs() لإضافة مساحة تخزين مؤقتة للملفات في وضع الحماية.

وهي دالة مفيدة بشكل خاص وهي AddLibrariesForBinary() التي تضيف المكتبات وأداة الربط المطلوبة من خلال البرنامج الثنائي.

لا يزال إيجاد طلبات النظم إلى القائمة المسموح بها يتطلب بعض العمل اليدوي للأسف. أنشئ سياسة باستدعاءات النظام التي تعرفها باحتياجات برنامجك الثنائي وشغلها بأعباء عمل شائعة. وفي حال حدوث انتهاك، يُرجى إضافة طلب النظام إلى القائمة المسموح بها وتكرار العملية. إذا واجهت مخالفة تعتقد أنّها قد تؤدي إلى إضافة موقعك الإلكتروني إلى القائمة المسموح بها وعالج البرنامج الأخطاء بشكل ملائم، يمكنك محاولة عرض رسالة خطأ بدلاً من ذلك باستخدام 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. تعديل الحدود

تمنع سياسة وضع الحماية الشخص من استدعاء أنظمة مكالمات محددة، وبالتالي تقلل من الأجزاء المعرضة للهجوم. ومع ذلك، قد يظل المهاجم قادرًا على إحداث تأثيرات غير مرغوب فيها من خلال تشغيل عملية إلى أجل غير مسمى أو استنفاد ذاكرة الوصول العشوائي والموارد الأخرى.

لمواجهة هذا التهديد، يخضع تطبيق 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، راجِع المثال الأداة.

4. تشغيل Sandbox

في الأقسام السابقة، تم إعداد بيئة وضع الحماية والسياسة والأداة التنفيذية و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();

5. التواصل مع Sandboxee

يمكن للمسؤول التنفيذي تلقائيًا التواصل مع Sandboxee من خلال أدوات وصف الملفات. قد يكون هذا كل ما تحتاج إليه، على سبيل المثال إذا كنت تريد فقط مشاركة ملف مع Sandboxee أو اقرأ الإخراج العادي لـ Sandboxee.

ومع ذلك، أنت على الأرجح بحاجة إلى منطق اتصال أكثر تعقيدًا بين المدير التنفيذي وSandboxee. يمكن استخدام واجهة برمجة تطبيقات comms (راجِع ملف العنوان 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() لإنشاء نقطة نهاية لإقران المقبس. ويمكن استخدام ذلك لقراءة الإخراج العادي أو الأخطاء العادية في Sandboxee. يمكن الاطّلاع على مثال على الاستخدام في الأداة.

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

استخدام واجهة برمجة تطبيقات comms

يوفر Sandbox2 واجهة برمجة تطبيقات للاتصالات ملائمة. هذه طريقة بسيطة وسهلة لمشاركة الأعداد الصحيحة أو السلاسل أو المخازن المؤقتة للبايت بين المنفّذ وSandboxee. في ما يلي بعض مقتطفات الرمز التي يمكنك العثور عليها في مثال crc4.

لبدء استخدام واجهة برمجة التطبيقات comms، يجب أولاً الحصول على كائن comms من كائن Sandbox2:

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

عند توفُّر كائن comms، يمكن إرسال البيانات إلى Sandboxee باستخدام إحدى عائلة دوال الإرسال*. يمكنك العثور على مثال لاستخدام واجهة برمجة تطبيقات comms في المثال 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 */
}

مشاركة البيانات مع الموارد الاحتياطية

ومن الوظائف الأخرى لمشاركة البيانات استخدام واجهة برمجة التطبيقات للتخزين المؤقت لمشاركة كميات كبيرة من البيانات وتجنُّب النُسخ باهظة الثمن التي يتم إرسالها ذهابًا وإيابًا بين تطبيق التنفيذ وبرنامج Sandboxee.

ينشئ التنفيذ المخزن المؤقت، إما حسب الحجم والبيانات التي سيتم تمريرها، أو مباشرةً من أداة وصف الملف، ويمرره إلى Sandboxee باستخدام comms->SendFD() في المنفّذ وcomms->RecvFD() في 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. الخروج من وضع الحماية

استنادًا إلى كيفية تشغيل وضع الحماية (راجع هذه الخطوة)، يجب تعديل طريقة إنهاء وضع الحماية، وبالتالي تشغيل وضع الحماية أيضًا.

الخروج من وضع حماية يعمل بشكل متزامن

إذا كان وضع الحماية يعمل بشكل متزامن، فلن يتم تشغيل وضع الحماية إلا عند انتهاء وضع الحماية. لذلك، ليس عليك اتّخاذ أيّ خطوة إضافية لإنهاء الحساب. يوضح مقتطف الرمز أدناه هذا السيناريو:

Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();

الخروج من وضع حماية يتم تشغيله بشكل غير متزامن

وإذا كان وضع الحماية يعمل بشكل غير متزامن، يتوفر خياران للإنهاء. أولاً، يمكنك الانتظار حتى اكتمال وضع الحماية والحصول على حالة التنفيذ النهائية:

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. Test

مثل أي رمز آخر، يجب أن يحتوي تطبيق وضع الحماية على اختبارات. ليس الهدف من اختبارات وضع الحماية اختبار صحة البرنامج، ولكن بدلاً من ذلك، للتحقق مما إذا كان يمكن تشغيل البرنامج في وضع الحماية دون مشكلات مثل انتهاكات وضع الحماية. يضمن ذلك أيضًا صحة سياسة وضع الحماية.

يتم اختبار البرنامج المحمي في وضع الحماية بالطريقة نفسها التي يتم بها تشغيله في الإنتاج، مع وجود الوسيطات وملفات الإدخال التي يعالجها عادةً.

ويمكن أن تكون هذه الاختبارات بسيطة مثل اختبار واجهة المستخدم أو اختبارات C++ باستخدام العمليات الفرعية. اطّلِع على الأمثلة للحصول على أفكار جديدة.

الخاتمة

شكرًا لقراءتكم هذه الرسالة، ونأمل أن ينال دليلنا إعجابكم، ولا تترددوا الآن في إنشاء أوضاع الحماية الخاصة بكم للمساعدة في الحفاظ على أمان المستخدمين.

يعد إنشاء تطبيقات الحماية والسياسات مهمة صعبة وعرضة للأخطاء الدقيقة. للحفاظ على أمانك، ننصحك بأن تطلب من أحد خبراء الأمان مراجعة سياستك ورمزك.