Mulai Menggunakan Sandbox2

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);
    
  • GunakanReceiveFd() 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.