Di halaman ini, Anda akan mempelajari cara membuat lingkungan sandbox Anda sendiri dengan Sandbox2. Anda akan mempelajari cara menentukan Kebijakan Sandbox, dan beberapa penyesuaian lanjutan, tetapi umum. Gunakan informasi di sini sebagai panduan, bersama dengan contoh dan dokumentasi kode dalam file header.
1. Memilih Metode Eksekutor Sandbox
Sandbox dimulai dengan eksekutor (lihat Eksekutor Sandbox), yang bertanggung jawab untuk menjalankan Sandboxee. File header executor.h berisi API yang diperlukan untuk tujuan ini. API ini sangat fleksibel dan memungkinkan Anda memilih yang paling sesuai dengan kasus penggunaan Anda. Bagian berikut menjelaskan 3 metode berbeda yang dapat Anda pilih.
Metode 1: Standalone – Menjalankan program biner dengan sandbox yang sudah diaktifkan
Ini adalah cara paling sederhana untuk menggunakan sandbox dan merupakan metode yang direkomendasikan jika Anda ingin melakukan sandbox pada seluruh program biner yang tidak memiliki kode sumber. Ini juga merupakan cara paling aman untuk menggunakan sandbox, karena tidak ada inisialisasi tanpa sandbox yang dapat menimbulkan efek buruk.
Dalam cuplikan kode berikut, kita menentukan jalur program biner yang akan di-sandbox dan argumen yang harus kita teruskan ke syscall execve. Seperti yang dapat Anda lihat di file header
executor.h, kita tidak menentukan nilai untuk envp dan oleh karena itu menyalin lingkungan
dari proses induk. Ingat, argumen pertama selalu merupakan nama program yang akan dijalankan, dan cuplikan kita tidak menentukan argumen lain.
Contoh metode eksekutor ini adalah: static dan 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);
Metode 2: Sandbox2 Forkserver – Memberi tahu eksekutor kapan harus di-sandbox
Metode ini menawarkan fleksibilitas untuk tidak di-sandbox selama inisialisasi, lalu memilih kapan harus memasuki sandbox dengan memanggil ::sandbox2::Client::SandboxMeHere(). Metode ini mengharuskan Anda dapat menentukan dalam
kode kapan Anda ingin memulai sandbox, dan harus menggunakan satu thread (baca
alasannya di FAQ).
Dalam cuplikan kode berikut, kita menggunakan kode yang sama seperti yang diuraikan dalam Metode 1 di atas. Namun, agar program dapat dijalankan tanpa sandbox selama inisialisasi, kita memanggil 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);
Karena eksekutor sekarang memiliki sandbox yang dinonaktifkan hingga diberi tahu oleh Sandboxee, kita harus membuat instance ::sandbox2::Client, menyiapkan komunikasi antara eksekutor dan Sandboxee, lalu memberi tahu eksekutor bahwa inisialisasi kita telah selesai dan kita ingin memulai sandbox sekarang dengan memanggil 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();
…
Contoh metode eksekutor ini adalah
crc4, dengan crc4bin.cc adalah
Sandboxee dan memberi tahu eksekutor (crc4sandbox.cc) kapan harus memasuki
sandbox.
Metode 3: Forkserver Kustom – Menyiapkan program biner, menunggu permintaan fork, dan melakukan sandbox sendiri
Mode ini memungkinkan Anda memulai program biner, menyiapkannya untuk sandbox, dan, pada momen tertentu dalam siklus proses program biner, membuatnya tersedia untuk eksekutor.
Eksekutor akan mengirimkan permintaan fork ke program biner Anda, yang akan fork() (melalui ::sandbox2::ForkingClient::EnterForkLook()). Proses yang baru dibuat akan siap di-sandbox dengan ::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());
Perlu diingat bahwa mode ini cukup rumit dan hanya berlaku dalam beberapa kasus tertentu; misalnya, saat Anda memiliki persyaratan memori yang ketat. Anda akan mendapatkan manfaat dari COW, tetapi memiliki kelemahan karena tidak ada ASLR yang sebenarnya. Contoh penggunaan umum lainnya adalah saat Sandboxee memiliki inisialisasi yang panjang dan intensif CPU yang dapat dijalankan sebelum data yang tidak tepercaya diproses.
Untuk contoh metode eksekutor ini, lihat custom_fork.
2. Membuat Kebijakan Sandbox
Setelah memiliki eksekutor, Anda mungkin ingin menentukan Sandbox Policy untuk Sandboxee. Jika tidak, Sandboxee hanya dilindungi oleh Kebijakan Syscall Default.
Tujuan Kebijakan Sandbox adalah membatasi syscall dan argumen yang dapat dilakukan Sandboxee, serta file yang dapat diaksesnya. Anda harus memahami secara mendetail syscall yang diperlukan oleh kode yang ingin Anda batasi dengan sandbox. Salah satu cara untuk mengamati syscall adalah dengan menjalankan kode ini menggunakan alat command line, strace, dari Linux.
Setelah memiliki daftar syscall, Anda dapat menggunakan PolicyBuilder untuk menentukan kebijakan.
PolicyBuilder dilengkapi dengan banyak fungsi praktis dan fungsi bantuan yang memungkinkan banyak
operasi umum (yang sebagian besar sudah disertakan dalam
DefaultPolicyBuilder()). Daftar berikut hanya berisi sebagian kecil dari
fungsi yang tersedia:
- Mengizinkan semua syscall untuk startup proses:
AllowStaticStartup();AllowDynamicStartup();
- Mengizinkan semua syscall terbuka/baca/tulis*:
AllowOpen();AllowRead();AllowWrite();
- Mengizinkan semua syscall keluar/akses/status yang terkait:
AllowExit();AllowStat();AllowAccess();
- Mengizinkan semua syscall waktu/tidur yang terkait:
AllowTime();AllowSleep();
Fungsi praktis ini memasukkan semua syscall yang relevan ke daftar yang diizinkan. Keuntungannya, kebijakan yang sama dapat digunakan di berbagai arsitektur yang tidak memiliki syscall tertentu (misalnya, ARM64 tidak memiliki syscall OPEN), tetapi risiko keamanannya kecil karena mengaktifkan lebih banyak syscall daripada yang mungkin diperlukan. Misalnya, AllowOpen() memungkinkan Sandboxee memanggil semua syscall terbuka yang terkait. Jika hanya ingin memasukkan satu syscall tertentu ke daftar yang diizinkan, Anda dapat menggunakan AllowSyscall();
untuk mengizinkan beberapa syscall sekaligus, Anda dapat menggunakan AllowSyscalls().
Sejauh ini, kebijakan hanya memeriksa ID syscall. Jika Anda perlu memperkuat kebijakan lebih lanjut dan ingin menentukan kebijakan yang hanya mengizinkan syscall dengan argumen tertentu, Anda harus menggunakan AddPolicyOnSyscall() atau AddPolicyOnSyscalls(). Fungsi ini tidak hanya menggunakan ID syscall sebagai argumen, tetapi juga filter seccomp-bpf mentah menggunakan makro helper bpf dari kernel Linux. Baca dokumentasi
kernel
untuk mengetahui informasi selengkapnya tentang BPF. Jika Anda menulis kode BPF berulang yang menurut Anda harus memiliki wrapper kegunaan, jangan ragu untuk mengajukan permintaan fitur.
Selain fungsi terkait syscall, PolicyBuilder juga menyediakan sejumlah fungsi terkait sistem file seperti AddFile() atau AddDirectory() untuk mengikat/memasang file/direktori ke sandbox. Helper AddTmpfs() dapat digunakan untuk menambahkan penyimpanan file sementara dalam sandbox.
AddLibrariesForBinary() adalah fungsi yang sangat bermanfaat untuk menambahkan
library dan linker yang diperlukan oleh program biner.
Sayangnya, memasukkan syscall ke daftar yang diizinkan masih memerlukan sedikit pekerjaan manual. Buat kebijakan dengan syscall yang Anda ketahui dibutuhkan program biner Anda dan jalankan dengan workload umum. Jika pelanggaran dipicu, masukkan syscall ke daftar yang diizinkan dan ulangi prosesnya. Jika Anda menemukan pelanggaran yang menurut Anda berisiko untuk dimasukkan ke daftar yang diizinkan dan program menangani error dengan baik, Anda dapat mencoba membuatnya menampilkan error dengan BlockSyscallWithErrno().
Contoh berikut membuat sandbox2::PolicyBuilder secara langsung untuk menunjukkan pembatasan argumen write dan mprotect serta pemblokiran openat (yang akan diizinkan oleh 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. Menyesuaikan Batas
Kebijakan Sandbox mencegah Sandboxee memanggil syscall tertentu dan dengan demikian mengurangi permukaan serangan. Namun, penyerang mungkin masih dapat menyebabkan efek yang tidak diinginkan dengan menjalankan proses tanpa batas atau menghabiskan RAM dan resource lainnya.
Untuk mengatasi ancaman ini, Sandboxee berjalan di bawah batas eksekusi yang ketat secara default. Jika batas default ini menyebabkan masalah pada eksekusi program Anda yang sah, Anda dapat menyesuaikannya menggunakan class sandbox2::Limits dengan memanggil limits() pada objek eksekutor.
Cuplikan kode di bawah ini menunjukkan beberapa contoh penyesuaian batas. Semua opsi yang tersedia didokumentasikan dalam file header 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));
Untuk contoh penggunaan class sandbox2::Limits, lihat alat contoh
tool.
4. Jalankan Sandbox
Di bagian sebelumnya, Anda menyiapkan lingkungan sandbox, kebijakan, dan eksekutor serta Sandboxee. Langkah berikutnya adalah membuat objek Sandbox2 dan menjalankannya.
Menjalankan secara sinkron
Sandbox dapat berjalan secara sinkron, sehingga diblokir hingga ada hasil. Cuplikan kode di bawah menunjukkan pembuatan instance objek Sandbox2 dan eksekusi sinkronnya. Untuk contoh yang lebih mendetail, lihat
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();
Menjalankan secara asinkron
Anda juga dapat menjalankan sandbox secara asinkron, sehingga tidak diblokir hingga ada hasil. Hal ini berguna, misalnya, saat berkomunikasi dengan Sandboxee. Cuplikan kode di bawah menunjukkan kasus penggunaan ini. Untuk contoh yang lebih mendetail, lihat crc4 dan 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. Berkomunikasi dengan Sandboxee
Secara default, eksekutor dapat berkomunikasi dengan Sandboxee melalui deskriptor file. Ini mungkin semua yang Anda butuhkan, misalnya jika Anda hanya ingin berbagi file dengan Sandboxee, atau membaca output standar Sandboxee.
Namun, kemungkinan besar Anda memerlukan logika komunikasi yang lebih kompleks antara eksekutor dan Sandboxee. Comms API (lihat file header comms.h ) dapat digunakan untuk mengirim bilangan bulat, string, buffer byte, protobuf, atau deskriptor file.
Membagikan Deskriptor File
Dengan menggunakan Inter-Process Communication API (lihat
ipc.h),
Anda dapat menggunakan MapFd() atau ReceiveFd():
Gunakan
MapFd()untuk memetakan deskriptor file dari eksekutor ke Sandboxee. Hal ini dapat digunakan untuk berbagi file yang dibuka dari eksekutor untuk digunakan di Sandboxee. Contoh penggunaan dapat dilihat di static.// The executor opened /proc/version and passes it to the sandboxee as stdin executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);Gunakan
ReceiveFd()untuk membuat endpoint socketpair. Hal ini dapat digunakan untuk membaca output standar atau error standar Sandboxee. Contoh penggunaan dapat dilihat di tool.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
Menggunakan comms API
Sandbox2 menyediakan comms API yang praktis. Ini adalah cara yang sederhana dan mudah untuk berbagi bilangan bulat, string, atau buffer byte antara eksekutor dan Sandboxee. Di bawah ini adalah beberapa cuplikan kode yang dapat Anda temukan dalam contoh crc4.
Untuk memulai comms API, Anda harus mendapatkan objek comms dari objek Sandbox2 terlebih dahulu:
sandbox2::Comms* comms = s2.comms();
Setelah objek comms tersedia, data dapat dikirim ke Sandboxee menggunakan salah satu fungsi Send*. Anda dapat menemukan contoh penggunaan comms API
dalam contoh crc4. Cuplikan kode di bawah ini menunjukkan kutipan dari contoh tersebut. Eksekutor mengirim unsigned char
buf[size] dengan SendBytes(buf, size):
if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
/* handle error */
}
Untuk menerima data dari Sandboxee, gunakan salah satu fungsi Recv*. Cuplikan kode
di bawah ini adalah kutipan dari contoh
crc4. Eksekutor menerima checksum dalam bilangan bulat 32-bit tanpa tanda: uint32_t crc4;
if (!(comms->RecvUint32(&crc4))) {
/* handle error */
}
Membagikan Data dengan Buffer
Fungsi berbagi data lainnya adalah menggunakan buffer API untuk berbagi data dalam jumlah besar dan menghindari salinan mahal yang dikirim bolak balik antara eksekutor dan Sandboxee.
Eksekutor membuat Buffer, baik berdasarkan ukuran dan data yang akan diteruskan, atau langsung
dari deskriptor file, dan meneruskannya ke Sandboxee menggunakan comms->SendFD()
di eksekutor dan comms->RecvFD() di Sandboxee.
Dalam cuplikan kode di bawah, Anda dapat melihat sisi eksekutor. Sandbox berjalan secara asinkron dan berbagi data melalui buffer dengan 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());
Di sisi Sandboxee, Anda juga harus membuat objek buffer dan membaca data dari deskriptor file yang dikirim oleh eksekutor:
// 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. Keluar dari sandbox
Bergantung pada cara Anda menjalankan sandbox (lihat langkah ini), Anda harus menyesuaikan cara menghentikan sandbox, dan dengan demikian juga Sandboxee.
Keluar dari sandbox yang berjalan secara sinkron
Jika sandbox telah berjalan secara sinkron, Run hanya akan ditampilkan saat Sandboxee selesai. Oleh karena itu, tidak diperlukan langkah tambahan untuk penghentian. Cuplikan kode di bawah menunjukkan skenario ini:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
Keluar dari sandbox yang berjalan secara asinkron
Jika sandbox telah berjalan secara asinkron, ada dua opsi yang tersedia untuk penghentian. Pertama, Anda hanya perlu menunggu penyelesaian Sandboxee dan menerima status eksekusi akhir:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
Atau, Anda dapat menghentikan Sandboxee kapan saja, tetapi sebaiknya tetap panggil AwaitResult() karena Sandboxee mungkin berhenti karena alasan lain untuk sementara:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Tes
Seperti kode lainnya, implementasi sandbox Anda harus memiliki pengujian. Pengujian sandbox tidak dimaksudkan untuk menguji kebenaran program, tetapi untuk memeriksa apakah program sandbox dapat berjalan tanpa masalah seperti pelanggaran sandbox. Hal ini juga memastikan bahwa kebijakan sandbox sudah benar.
Program sandbox diuji dengan cara yang sama seperti saat Anda menjalankannya dalam produksi, dengan argumen dan file input yang biasanya diproses.
Pengujian ini dapat sesederhana pengujian shell atau pengujian C++ menggunakan sub-proses. Lihat contohnya untuk mendapatkan inspirasi.
Kesimpulan
Terima kasih telah membaca hingga akhir. Kami harap Anda menyukai panduan kami dan kini merasa siap untuk membuat sandbox Anda sendiri guna membantu menjaga keamanan pengguna.
Membuat sandbox dan kebijakan adalah tugas yang sulit dan rentan terhadap error kecil. Untuk tetap aman, sebaiknya minta pakar keamanan meninjau kebijakan dan kode Anda.