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 di file header.

1. Memilih Metode Eksekutor Sandbox

Sandboxing 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 apa yang paling cocok untuk kasus penggunaan Anda. Bagian berikut menjelaskan 3 metode berbeda yang dapat Anda pilih.

Metode 1: Mandiri – Jalankan biner dengan sandbox yang sudah diaktifkan

Ini adalah cara paling sederhana untuk menggunakan sandbox dan merupakan metode yang disarankan jika Anda ingin melakukan sandbox seluruh biner yang kode sumbernya tidak Anda miliki. Metode ini juga merupakan cara paling aman untuk menggunakan sandbox karena tidak ada inisialisasi di luar sandbox yang dapat menimbulkan efek buruk.

Dalam cuplikan kode berikut, kita menentukan jalur biner yang akan di-sandbox dan argumen yang harus diteruskan ke syscall eksekutif. Seperti yang Anda lihat dalam file header executor.h, kita tidak menentukan nilai untuk envp sehingga kita akan menyalin lingkungan dari proses induk. Ingat, argumen pertama selalu merupakan nama program yang akan dieksekusi, dan cuplikan kita tidak mendefinisikan argumen lain.

Contoh metode eksekutor ini adalah: statis dan alat.

#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 melakukan sandbox selama inisialisasi, lalu memilih waktu untuk memasuki sandbox dengan memanggil ::sandbox2::Client::SandboxMeHere(). Anda harus dapat menentukan kode saat ingin memulai sandbox dan harus 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 dengan cara yang tidak di-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 sekarang eksekutor telah menonaktifkan sandbox hingga diberi tahu oleh Sandboxee, kita harus membuat instance ::sandbox2::Client, menyiapkan komunikasi antara eksekutor dan Sandboxee, lalu memberi tahu eksekutor bahwa inisialisasi 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 masuk ke sandbox.

Metode 3: Forkserver Kustom – Siapkan biner, tunggu permintaan fork, dan sandbox Anda sendiri

Mode ini memungkinkan Anda memulai biner, mempersiapkannya untuk sandbox, dan, pada saat tertentu dalam siklus proses biner Anda, menyediakannya 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 dapat diterapkan dalam beberapa kasus tertentu; misalnya, saat Anda memiliki kebutuhan memori yang ketat. Anda akan mendapatkan manfaat dari COW tetapi memiliki kelemahan bahwa tidak ada ASLR yang sebenarnya. Contoh penggunaan umum lainnya adalah ketika Sandboxee memiliki inisialisasi intensif CPU yang panjang 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 perlu menentukan Kebijakan Sandbox untuk Sandboxee. Jika tidak, Sandboxee hanya dilindungi oleh Kebijakan Syscall Default.

Dengan Kebijakan Sandbox, tujuannya adalah membatasi syscall dan argumen yang dapat dibuat oleh Sandboxee, serta file yang dapat diaksesnya. Anda harus memiliki pemahaman terperinci tentang {i>syscall<i} yang diperlukan oleh kode yang Anda rencanakan untuk {i>sandbox<i}. Salah satu cara untuk mengamati syscall adalah menjalankan kode dengan strace alat command line Linux.

Setelah memiliki daftar syscall, Anda dapat menggunakan PolicyBuilder untuk menentukan kebijakan. PolicyBuilder dilengkapi dengan banyak fungsi bantuan dan kemudahan yang memungkinkan banyak operasi umum. Daftar berikut ini hanya cuplikan kecil dari fungsi yang tersedia:

  • Izinkan syscall apa pun untuk memulai proses:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Izinkan semua syscall terbuka/read/write*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Izinkan semua syscall terkait keluar/akses/status:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • Izinkan syscall terkait tidur/waktu:
    • AllowTime();
    • AllowSleep();

Fungsi praktis ini mengizinkan syscall apa pun yang relevan. Hal ini memiliki keuntungan bahwa kebijakan yang sama dapat digunakan pada arsitektur yang berbeda di mana syscall tertentu tidak tersedia (misalnya ARM64 tidak memiliki syscall OPEN), tetapi dengan risiko keamanan kecil yang memungkinkan lebih banyak syscall daripada yang mungkin diperlukan. Misalnya, AllowOpen() memungkinkan Sandboxee untuk memanggil syscall terkait yang terbuka. Jika hanya ingin mengizinkan satu syscall tertentu, Anda dapat menggunakan AllowSyscall(); untuk mengizinkan beberapa syscall sekaligus. Anda dapat menggunakan AllowSyscalls().

Sejauh ini kebijakan tersebut hanya memeriksa ID syscall. Jika Anda perlu memperkuat kebijakan lebih lanjut dan ingin menentukan kebijakan yang mengizinkan syscall hanya 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 bantuan bpf dari kernel Linux. Lihat dokumentasi kernel untuk informasi selengkapnya tentang BPF. Jika Anda mendapati diri Anda menulis kode BPF berulang yang menurut Anda seharusnya 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 dalam 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.

Namun, membuat syscall ke daftar yang diizinkan masih memerlukan banyak pekerjaan manual. Buat kebijakan dengan syscall yang Anda ketahui kebutuhan biner Anda dan jalankan dengan beban kerja umum. Jika pelanggaran dipicu, izinkan syscall dan ulangi prosesnya. Jika Anda menemukan pelanggaran yang menurut Anda mungkin berisiko untuk 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. Sesuaikan Batas

Kebijakan Sandbox mencegah Sandboxee memanggil syscall tertentu sehingga dapat mengurangi permukaan serangan. Namun, penyerang mungkin masih dapat menimbulkan efek yang tidak diinginkan dengan menjalankan proses tanpa batas waktu atau menghabiskan RAM dan resource lainnya.

Untuk mengatasi ancaman ini, secara default Sandboxee dijalankan dengan batas eksekusi yang ketat. Jika batas default ini menyebabkan masalah untuk eksekusi program 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 contoh alat.

4. Menjalankan Sandbox

Di bagian sebelumnya, Anda telah menyiapkan lingkungan, kebijakan, eksekutor, serta eksekutor dalam sandbox dan Sandboxee. Langkah berikutnya adalah membuat objek Sandbox2 dan menjalankannya.

Berjalan secara sinkron

Sandbox dapat berjalan secara sinkron, sehingga melakukan pemblokiran hingga muncul hasil. Cuplikan kode di bawah ini menunjukkan pembuatan instance 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();

Berjalan secara asinkron

Anda juga dapat menjalankan sandbox secara asinkron, sehingga tidak memblokir hingga ada hasilnya. 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 yang Anda butuhkan, misalnya jika Anda hanya ingin berbagi file dengan Sandboxee, atau membaca output standar Sandboxee.

Namun, kemungkinan besar Anda membutuhkan 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 Deskripsi File

Dengan Inter-Process Communication API (lihat ipc.h), Anda dapat menggunakan MapFd() atau ReceiveFd():

  • Gunakan MapFd() untuk memetakan deskripsi file dari eksekutor ke Sandboxee. Fungsi ini dapat digunakan untuk membagikan file yang dibuka dari eksekutor untuk digunakan di Sandboxee. Contoh penggunaan dapat dilihat di statis.

    // 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 API komunikasi

Sandbox2 menyediakan API komunikasi yang praktis. Ini adalah cara sederhana dan mudah untuk membagikan bilangan bulat, string, atau buffering byte antara eksekutor dan Sandboxee. Berikut adalah beberapa cuplikan kode yang dapat Anda temukan dalam contoh crc4.

Untuk memulai API komunikasi, Anda harus mendapatkan objek komunikasi dari objek Sandbox2 terlebih dahulu:

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

Setelah objek komunikasi tersedia, data dapat dikirim ke Sandboxee menggunakan salah satu kelompok fungsi Kirim*. Anda dapat menemukan contoh penggunaan API komunikasi dalam contoh crc4. Cuplikan kode di bawah ini menunjukkan kutipan dari contoh tersebut. Eksekutor mengirimkan 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 tanpa tanda tangan 32-bit: uint32_t crc4;

if (!(comms->RecvUint32(&crc4))) {
  /* handle error */
}

Berbagi Data dengan {i>Buffer<i}

Fungsi berbagi data lainnya adalah menggunakan API buffer untuk berbagi data dalam jumlah besar dan menghindari salinan yang 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 buffering 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 juga dengan Sandboxee.

Keluar dari sandbox yang berjalan secara sinkron

Jika sandbox telah berjalan secara sinkron, Run hanya akan ditampilkan saat Sandboxee selesai. Oleh karena itu, penghentian ini tidak diperlukan langkah tambahan. Cuplikan kode di bawah ini menunjukkan skenario ini:

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

Keluar dari sandbox yang berjalan secara asinkron

Jika sandbox berjalan secara asinkron, ada dua opsi yang tersedia untuk dihentikan. 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 sebaiknya tetap panggil AwaitResult() karena Sandboxee mungkin dihentikan karena alasan lain pada saat yang sama:

s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();

7. Pengujian

Seperti kode lainnya, implementasi sandbox Anda harus memiliki pengujian. Pengujian sandbox tidak dimaksudkan untuk menguji ketepatan program, tetapi untuk memeriksa apakah program yang di-sandbox dapat berjalan tanpa masalah seperti pelanggaran sandbox. Tindakan ini juga memastikan bahwa kebijakan {i>sandbox<i} sudah benar.

Program dalam sandbox diuji dengan cara yang sama seperti Anda menjalankannya dalam produksi, dengan argumen dan file input yang biasanya akan diproses.

Pengujian ini bisa sesederhana pengujian shell atau pengujian C++ dengan menggunakan subproses. Lihat contoh untuk mendapatkan inspirasi.

Kesimpulan

Terima kasih telah membaca sejauh ini. Semoga Anda menyukai panduan kami dan sekarang merasa diberdayakan untuk membuat sandbox sendiri untuk membantu menjaga keamanan pengguna.

Membuat sandbox dan kebijakan adalah tugas yang sulit dan rentan terhadap error yang samar. Demi keamanan, sebaiknya minta pakar keamanan untuk meninjau kebijakan dan kode Anda.