في هذه الصفحة، سنتعرّف على كيفية إنشاء بيئة وضع حماية خاصة باستخدام Sandbox2. سنتعرّف على كيفية تحديد سياسة وضع الحماية وبعض التعديلات المتقدّمة والشائعة. يمكنكم استخدام المعلومات الواردة هنا كدليل، إلى جانب الـ أمثلة ووثائق الرمز في ملفات الـ عناوين.
1. اختيار طريقة تنفيذ وضع الحماية
يبدأ وضع الحماية بمنفِّذ (راجِع منفِّذ وضع الحماية)، وهو المسؤول عن تشغيل 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);
الطريقة 2: 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: Custom Forkserver: إعداد ملف ثنائي والانتظار إلى حين طلبات التفرّع ووضع الحماية بنفسك
يتيح لك هذا الوضع بدء ملف ثنائي وإعداده لوضع الحماية، وفي لحظة معيّنة من دورة حياة الملف الثنائي، إتاحته للمنفِّذ.
سيرسل المنفِّذ طلب تفرّع إلى الملف الثنائي، الذي سيستدعي 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 عملية إعداد طويلة ومكثفة لوحدة المعالجة المركزية يمكن تشغيلها قبل معالجة البيانات غير الموثوق بها.
للاطّلاع على مثال على طريقة التنفيذ هذه، يُرجى مراجعة custom_fork.
2. إنشاء سياسة وضع الحماية
بعد توفّر المنفِّذ، من المحتمل أن تريد تحديد Sandbox Policy لـ Sandboxee. وإلا، لن يتم حماية Sandboxee إلا من خلال سياسة استدعاءات النظام التلقائية.
باستخدام "سياسة وضع الحماية"، يكون الهدف هو تقييد استدعاءات النظام والوسيطات التي يمكن أن يجريها Sandboxee، بالإضافة إلى الملفات التي يمكنه الوصول إليها. عليك أن تكون على دراية تفصيلية باستدعاءات النظام المطلوبة للرمز الذي تخطط لوضعه في وضع الحماية. إحدى طرق مراقبة استدعاءات النظام هي تشغيل الرمز باستخدام أداة سطر الأوامر strace من Linux.
بعد الحصول على قائمة استدعاءات النظام، يمكنك استخدام PolicyBuilder لتحديد السياسة.
PolicyBuilder يأتي مزوّدًا بالعديد من الدوال المريحة والمساعدة التي تتيح إجراء العديد من
العمليات الشائعة (العديد منها مضمّنة في
DefaultPolicyBuilder()). القائمة التالية هي مجرد مقتطف صغير من
الدوال المتاحة:
- السماح بأي استدعاء نظام لبدء تشغيل العملية:
AllowStaticStartup();AllowDynamicStartup();
- السماح بأي استدعاءات نظام open/read/write*:
AllowOpen();AllowRead();AllowWrite();
- السماح بأي استدعاءات نظام ذات صلة بالخروج أو الوصول أو الحالة:
AllowExit();AllowStat();AllowAccess();
- السماح بأي استدعاءات نظام ذات صلة بالنوم أو الوقت:
AllowTime();AllowSleep();
تسمح دوال الراحة هذه بأي استدعاء نظام ذي صلة. تتميز هذه الدوال بأنّه يمكن استخدام السياسة نفسها على بُنى مختلفة لا تتوفّر فيها استدعاءات نظام معيّنة (مثلاً، لا تتضمّن بُنية ARM64 استدعاء النظام OPEN)، ولكن مع خطر أمني طفيف يتمثل في تفعيل استدعاءات نظام أكثر مما قد يكون ضروريًا. على سبيل المثال، يتيح AllowOpen() لـ Sandboxee استدعاء أي استدعاء نظام ذي صلة بـ open. إذا
كنت تريد السماح باستدعاء نظام واحد فقط، يمكنك استخدام AllowSyscall();
للسماح باستدعاءات نظام متعددة في آنٍ واحد، يمكنك استخدام AllowSyscalls().
حتى الآن، لا تتحقّق السياسة إلا من معرّف استدعاء النظام. إذا كنت بحاجة إلى تعزيز السياسة بشكل أكبر وأردت تحديد سياسة لا تسمح فيها إلا باستدعاء نظام باستخدام وسيطات معيّنة، عليك استخدام AddPolicyOnSyscall() أو AddPolicyOnSyscalls(). لا تأخذ هاتان الدالتان معرّف استدعاء النظام كـ وسيطة فحسب، بل أيضًا فلتر seccomp-bpf خام باستخدام وحدات الماكرو المساعدة bpf من نواة Linux. يُرجى مراجعة مستندات
النواة
لمزيد من المعلومات عن BPF. إذا كنت تكتب رمز BPF متكرّرًا تعتقد أنّه يجب أن يكون له برنامج تضمين قابل للاستخدام، يمكنك تقديم طلب ميزة.
بالإضافة إلى الدوال ذات الصلة باستدعاءات النظام، يوفّر PolicyBuilder أيضًا عددًا من الدوال ذات الصلة بنظام الملفات، مثل AddFile() أو AddDirectory() لربط ملف أو دليل في وضع الحماية. يمكن استخدام الدالة المساعدة 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();
}
3. تعديل الحدود
تمنع "سياسة وضع الحماية" 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. تشغيل وضع الحماية
في الأقسام السابقة، تم إعداد بيئة وضع الحماية والسياسة والمنفِّذ و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 (راجِع ملف العنوان comms.h ) لإرسال أعداد صحيحة أو سلاسل أو مخازن مؤقتة للوحدات البايت أو protobufs أو واصفات الملفات.
مشاركة واصفات الملفات
باستخدام واجهة برمجة التطبيقات "التواصل بين العمليات" (راجِع
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. يمكن الاطّلاع على مثال على الاستخدام في أداة tool.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
استخدام واجهة برمجة التطبيقات comms
يوفّر Sandbox2 واجهة برمجة تطبيقات comms API مريحة. هذه طريقة بسيطة وسهلة لمشاركة الأعداد الصحيحة أو السلاسل أو المخازن المؤقتة للوحدات البايت بين المنفِّذ وSandboxee. في ما يلي بعض مقتطفات الرمز التي يمكنك العثور عليها في مثال crc4.
للبدء باستخدام واجهة برمجة التطبيقات comms، عليك أولاً الحصول على عنصر comms من عنصر Sandbox2:
sandbox2::Comms* comms = s2.comms();
بعد توفّر عنصر comms، يمكن إرسال البيانات إلى Sandboxee باستخدام إحدى الدوال من مجموعة Send*. يمكن الاطّلاع على مثال على استخدام واجهة برمجة التطبيقات 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 */
}
مشاركة البيانات باستخدام المخازن المؤقتة
من وظائف مشاركة البيانات الأخرى استخدام واجهة برمجة التطبيقات buffer API لمشاركة كميات كبيرة من البيانات وتجنُّب النسخ المكلفة التي يتم إرسالها ذهابًا وإيابًا بين المنفِّذ و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. الخروج من وضع الحماية
اعتمادًا على طريقة تشغيل وضع الحماية (راجِع هذه الخطوة)، عليك تعديل طريقة إنهاء وضع الحماية، وبالتالي أيضًا 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. اختبار
مثل أي رمز آخر، يجب أن يتضمّن تنفيذ وضع الحماية اختبارات. لا تهدف اختبارات وضع الحماية إلى اختبار صحة البرنامج، بل إلى التحقّق مما إذا كان بإمكان البرنامج الذي تم وضعه في وضع الحماية التشغيل بدون مشاكل مثل انتهاكات وضع الحماية. يضمن ذلك أيضًا أنّ "سياسة وضع الحماية" صحيحة.
يتم اختبار برنامج وضع الحماية بالطريقة نفسها التي يتم تشغيله بها في بيئة الإنتاج، باستخدام الوسيطات وملفات الإدخال التي يعالجها عادةً.
يمكن أن تكون هذه الاختبارات بسيطة مثل اختبار shell أو اختبارات C++ باستخدام عمليات فرعية. يمكنكم الاطّلاع على الأمثلة للحصول على أفكار.
الخاتمة
شكرًا على قراءة هذا الدليل، ونأمل أن يكون قد نال إعجابكم وأن تشعروا الآن بالقدرة على إنشاء صناديق وضع الحماية الخاصة بكم للمساعدة في الحفاظ على أمان المستخدمين.
إنشاء صناديق وضع الحماية والسياسات مهمة صعبة وعرضة للأخطاء الدقيقة. للحفاظ على الأمان، ننصحكم بأن يراجع خبير أمان السياسة والرمز.