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 Sandbox Executor
Sandbox dimulai dengan executor (lihat Sandbox Executor), 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 opsi yang paling sesuai dengan kasus penggunaan Anda. Bagian berikut menjelaskan 3 metode berbeda yang dapat Anda pilih.
Metode 1: Mandiri – Mengeksekusi biner dengan sandbox yang sudah diaktifkan
Ini adalah cara paling sederhana untuk menggunakan sandbox dan merupakan metode yang direkomendasikan jika Anda ingin membuat sandbox untuk seluruh biner yang tidak memiliki kode sumber. Cara 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 biner yang akan di-sandbox
dan argumen yang harus kita teruskan ke syscall execve. Seperti yang dapat Anda lihat di file header
executor.h, kami tidak menentukan nilai untuk envp
dan oleh karena itu menyalin lingkungan dari proses induk. Ingat, argumen pertama selalu berupa
nama program yang akan dieksekusi, dan cuplikan kita tidak menentukan argumen
lain.
Contoh metode executor 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, dan kemudian memilih waktu untuk memasuki sandbox dengan memanggil ::sandbox2::Client::SandboxMeHere()
. Anda harus dapat menentukan dalam kode kapan Anda ingin memulai sandbox, dan sandbox harus berupa thread tunggal (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 dieksekusi 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 executor kini memiliki sandbox yang dinonaktifkan hingga diberi tahu oleh
Sandboxee, kita harus membuat instance ::sandbox2::Client
, menyiapkan
komunikasi antara executor dan Sandboxee, lalu memberi tahu executor
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
) saat harus memasuki
sandbox.
Metode 3: Forkserver Kustom – Siapkan biner, tunggu permintaan fork, dan sandbox sendiri
Mode ini memungkinkan Anda memulai biner, menyiapkan biner untuk sandbox, dan, pada saat tertentu dalam siklus proses biner, membuatnya tersedia untuk eksekutor.
Eksekutor akan mengirim permintaan fork ke biner Anda, yang akan fork()
(melalui
::sandbox2::ForkingClient::WaitAndFork()
). 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 kekurangan 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 executor ini, lihat custom_fork.
2. Membuat Kebijakan Sandbox
Setelah memiliki executor, Anda mungkin ingin menentukan Sandbox Policy untuk Sandboxee. Jika tidak, Sandboxee hanya dilindungi oleh Kebijakan Syscall Default.
Dengan Kebijakan Sandbox, tujuannya 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 akan Anda sandbox. Salah satu cara untuk mengamati syscall adalah dengan menjalankan kode menggunakan alat command line strace Linux.
Setelah memiliki daftar panggilan sistem, Anda dapat menggunakan PolicyBuilder untuk menentukan kebijakan. PolicyBuilder dilengkapi dengan banyak fungsi praktis dan helper yang memungkinkan banyak operasi umum. Daftar berikut hanyalah cuplikan kecil dari fungsi yang tersedia:
- Mengizinkan syscall apa pun untuk startup proses:
AllowStaticStartup();
AllowDynamicStartup();
- Mengizinkan semua syscall open/read/write*:
AllowOpen();
AllowRead();
AllowWrite();
- Izinkan syscall terkait keluar/akses/status:
AllowExit();
AllowStat();
AllowAccess();
- Izinkan semua panggilan sistem terkait waktu/tidur:
AllowTime();
AllowSleep();
Fungsi praktis ini memasukkan syscall yang relevan ke dalam daftar yang diizinkan. Hal ini memiliki
keuntungan bahwa kebijakan yang sama dapat digunakan di berbagai arsitektur yang
tidak memiliki syscall tertentu (misalnya, ARM64 tidak memiliki syscall OPEN), tetapi dengan
risiko keamanan kecil karena mengaktifkan lebih banyak syscall daripada yang mungkin diperlukan. Misalnya, AllowOpen() memungkinkan Sandboxee memanggil syscall terkait pembukaan apa pun. Jika
Anda hanya ingin memasukkan satu syscall tertentu ke dalam 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. Lihat 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 memasang file/direktori ke sandbox. Helper AddTmpfs()
dapat digunakan untuk menambahkan penyimpanan file sementara dalam sandbox.
Fungsi yang sangat berguna adalah AddLibrariesForBinary()
yang menambahkan
library dan linker yang diperlukan oleh biner.
Sayangnya, membuat daftar yang diizinkan untuk syscall masih memerlukan sedikit pekerjaan manual. Buat kebijakan dengan syscall yang Anda ketahui dibutuhkan 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()
.
#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 waktu atau menghabiskan RAM dan resource lainnya.
Untuk mengatasi ancaman ini, Sandboxee berjalan dengan 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 executor.
Cuplikan kode di bawah 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 contoh
alat.
4. Menjalankan Sandbox
Di bagian sebelumnya, Anda telah menyiapkan lingkungan sandbox, kebijakan, dan
pelaksana serta Sandboxee. Langkah berikutnya adalah membuat objek Sandbox2
dan menjalankannya.
Menjalankan secara sinkron
Sandbox dapat berjalan secara sinkron, sehingga memblokir hingga ada hasil. Cuplikan kode di bawah menunjukkan instansiasi objek Sandbox2
dan eksekusi sinkronnya. Untuk contoh yang lebih mendetail, lihat
statis.
#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 memblokir 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, executor dapat berkomunikasi dengan Sandboxee melalui deskriptor file. Mungkin hanya ini yang Anda butuhkan, misalnya jika Anda hanya ingin membagikan 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 executor ke Sandboxee. Ini dapat digunakan untuk membagikan file yang dibuka dari executor 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. Ini dapat digunakan untuk membaca output standar atau error standar Sandboxee. Contoh penggunaan dapat dilihat di alat.// 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 mudah. Ini adalah cara sederhana dan mudah untuk membagikan bilangan bulat, string, atau buffer byte antara executor dan Sandboxee. Berikut beberapa cuplikan kode yang dapat Anda temukan dalam contoh crc4.
Untuk mulai menggunakan 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 keluarga Send*. Anda dapat menemukan contoh penggunaan comms API
dalam contoh crc4. Cuplikan kode di bawah 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 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 executor 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 executor. Sandbox berjalan secara asinkron dan membagikan 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 executor:
// 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, maka 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, dua opsi tersedia untuk penghentian. Pertama, Anda cukup 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 tetap
direkomendasikan untuk memanggil AwaitResult()
karena Sandboxee mungkin dihentikan
karena alasan lain untuk sementara waktu:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Tes
Seperti kode lainnya, penerapan sandbox Anda harus memiliki pengujian. Pengujian sandbox tidak dimaksudkan untuk menguji kebenaran program, tetapi untuk memeriksa apakah program yang di-sandbox dapat berjalan tanpa masalah seperti pelanggaran sandbox. Hal ini juga memastikan bahwa kebijakan sandbox sudah benar.
Program yang di-sandbox diuji dengan cara yang sama seperti saat Anda menjalankannya dalam produksi, dengan argumen dan file input yang biasanya diproses.
Pengujian ini bisa sesederhana pengujian shell atau pengujian C++ menggunakan subproses. Lihat contoh untuk mendapatkan inspirasi.
Kesimpulan
Terima kasih telah membaca hingga sejauh ini. Kami harap Anda menyukai panduan kami dan kini merasa mampu membuat sandbox sendiri untuk membantu menjaga keamanan pengguna Anda.
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.