Bắt đầu sử dụng Hộp cát 2

Trên trang này, bạn sẽ tìm hiểu cách tạo môi trường hộp cát của riêng mình bằng Sandbox2. Bạn sẽ tìm hiểu cách xác định Chính sách Hộp cát và một số nội dung chỉnh sửa nâng cao nhưng thường gặp. Hãy sử dụng thông tin trong phần hướng dẫn này, cùng với ví dụ và tài liệu về mã trong các tệp tiêu đề.

1. Chọn phương thức của trình thực thi Sandbox

Hộp cát bắt đầu bằng một trình thực thi (xem Trình thực thi hộp cát), trình thực thi này chịu trách nhiệm chạy Sandboxee. Tệp tiêu đề executor.h chứa API cần thiết cho mục đích này. API này rất linh hoạt và cho phép bạn chọn những gì phù hợp nhất với trường hợp sử dụng của mình. Các phần sau đây mô tả 3 phương thức mà bạn có thể chọn.

Phương pháp 1: Độc lập – Thực thi tệp nhị phân đã bật hộp cát

Đây là cách đơn giản nhất để sử dụng hộp cát và là phương pháp được đề xuất khi bạn muốn tạo hộp cát toàn bộ tệp nhị phân mà bạn không có mã nguồn. Đây cũng là cách an toàn nhất để sử dụng hộp cát, vì không có quá trình khởi chạy nào chưa được đặt trong hộp cát có thể gây ra tác động bất lợi.

Trong đoạn mã sau, chúng ta xác định đường dẫn của tệp nhị phân cần được tạo hộp cát và các đối số chúng ta phải truyền đến một lệnh gọi hệ thống thực thi. Như bạn có thể thấy trong tệp tiêu đề executor.h, chúng ta không chỉ định giá trị cho envp nên sẽ sao chép môi trường từ quy trình mẹ. Hãy nhớ rằng đối số đầu tiên luôn là tên của chương trình được thực thi và đoạn mã của chúng ta không định nghĩa bất kỳ đối số nào khác.

Ví dụ về phương thức thực thi này: tĩnhcông cụ.

#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);

Phương pháp 2: Sandbox2 Forkserver – Thông báo cho trình thực thi khi nào được tạo hộp cát

Phương thức này giúp bạn linh hoạt lựa chọn không hộp cát trong quá trình khởi chạy, sau đó chọn thời điểm chuyển sang hộp cát bằng cách gọi ::sandbox2::Client::SandboxMeHere(). Nó yêu cầu bạn phải có khả năng xác định trong mã khi bạn muốn bắt đầu hộp cát và nó phải là một luồng đơn (đọc lý do trong phần Câu hỏi thường gặp).

Trong đoạn mã sau, chúng tôi sử dụng cùng một mã như nêu trong Phương pháp 1 ở trên. Tuy nhiên, để cho phép chương trình thực thi theo cách không có hộp cát trong quá trình khởi chạy, chúng ta gọi 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);

Do trình thực thi này hiện có hộp cát bị tắt cho đến khi nhận được thông báo từ Sandboxee, nên chúng ta phải tạo một thực thể ::sandbox2::Client, thiết lập hoạt động giao tiếp giữa trình thực thi và Sandboxee, sau đó thông báo cho trình thực thi này rằng quá trình khởi chạy đã hoàn tất và chúng ta muốn bắt đầu tạo hộp cát ngay bây giờ bằng cách gọi 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();
  …

Ví dụ về phương thức thực thi này là crc4, trong đó crc4bin.cc là Sandboxee và thông báo cho trình thực thi (crc4sandbox.cc) khi cần truy cập vào hộp cát.

Phương pháp 3: Máy chủ Forkserver tuỳ chỉnh – Chuẩn bị tệp nhị phân, tự chờ các yêu cầu phát triển nhánh và hộp cát

Chế độ này cho phép bạn bắt đầu tệp nhị phân, chuẩn bị hộp cát cho hộp cát và cung cấp cho trình thực thi tại một thời điểm cụ thể trong vòng đời của tệp nhị phân.

Trình thực thi sẽ gửi yêu cầu phát triển nhánh đến tệp nhị phân của bạn, yêu cầu này sẽ fork() (thông qua ::sandbox2::ForkingClient::WaitAndFork()). Quy trình mới tạo sẽ sẵn sàng được đặt trong hộp cát với ::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());

Hãy lưu ý rằng chế độ này khá phức tạp và chỉ áp dụng trong một số trường hợp cụ thể, ví dụ như khi bạn có yêu cầu về bộ nhớ chặt chẽ. Bạn sẽ được hưởng lợi từ COW nhưng có nhược điểm là không có ASLR thực sự. Một ví dụ về cách sử dụng điển hình khác là khi Sandboxee có một quá trình khởi chạy dài, tốn nhiều CPU, có thể chạy trước khi xử lý dữ liệu không đáng tin cậy.

Để xem ví dụ về phương thức thực thi này, hãy tham khảo custom_fork.

2. Tạo chính sách Hộp cát

Sau khi có executor, bạn có thể muốn xác định Chính sách Sandbox cho Sandboxee. Nếu không, Sandboxee chỉ được bảo vệ theo Chính sách Syscall mặc định.

Mục tiêu của chính sách Hộp cát là hạn chế các lệnh gọi hệ thống và đối số mà Sandboxee có thể thực hiện, cũng như các tệp mà Hộp cát có thể truy cập. Bạn cần nắm rõ chi tiết về các syscall bắt buộc phải có theo mã mà bạn định tạo hộp cát. Một cách để quan sát syscall là chạy mã bằng công cụ dòng lệnh strace của Linux.

Sau khi có danh sách các lệnh gọi hệ thống, bạn có thể sử dụng PolicyBuilder để xác định chính sách. PolicyBuilder có nhiều chức năng trợ giúp và tiện lợi cho phép thực hiện nhiều thao tác phổ biến. Danh sách sau đây chỉ là một phần trích dẫn nhỏ về các hàm có sẵn:

  • Đưa mọi lệnh gọi hệ thống (syscall) để khởi động quy trình vào danh sách cho phép:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Đưa bất kỳ lệnh gọi hệ thống nào mở/read/write* vào danh sách cho phép:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Đưa mọi lệnh gọi hệ thống liên quan đến thoát/truy cập/trạng thái vào danh sách cho phép:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • Đưa mọi lệnh gọi hệ thống liên quan đến giấc ngủ/thời gian vào danh sách cho phép:
    • AllowTime();
    • AllowSleep();

Các hàm tiện lợi này đã đưa mọi lệnh gọi hệ thống có liên quan vào danh sách cho phép. Điều này có lợi thế là có thể sử dụng cùng một chính sách trên nhiều cấu trúc khi một số lệnh gọi hệ thống nhất định không hoạt động (ví dụ: ARM64 không có lệnh gọi hệ thống MỞ), nhưng có rủi ro bảo mật nhỏ khi bật nhiều sycsall hơn mức cần thiết. Ví dụ: AllowOpen() cho phép Sandboxee gọi bất kỳ lệnh gọi syscall đang mở nào có liên quan. Nếu chỉ muốn thêm một syscall cụ thể vào danh sách cho phép, bạn có thể dùng AllowSyscall(); để cho phép nhiều syscall cùng lúc có thể dùng AllowSyscalls().

Cho đến nay, chính sách này chỉ kiểm tra giá trị nhận dạng lệnh gọi hệ thống. Nếu cần củng cố chính sách hơn nữa và muốn xác định một chính sách mà trong đó bạn chỉ cho phép lệnh gọi hệ thống có các đối số cụ thể, bạn cần sử dụng AddPolicyOnSyscall() hoặc AddPolicyOnSyscalls(). Các hàm này không chỉ lấy mã nhận dạng syscall làm đối số mà còn sử dụng bộ lọc seccomp-bpf thô bằng cách sử dụng các macro trợ giúp bpf từ nhân Linux. Xem tài liệu về hạt nhân để biết thêm thông tin về BPF. Nếu bạn thấy mình phải viết mã BPF lặp lại mà bạn cho rằng cần có một trình bao bọc khả năng hữu dụng, hãy gửi yêu cầu về tính năng.

Ngoài các hàm liên quan đến syscall, PolicyBuilder cũng cung cấp một số hàm liên quan đến hệ thống tệp như AddFile() hoặc AddDirectory() để liên kết gắn một tệp/thư mục vào hộp cát. Bạn có thể sử dụng trình trợ giúp AddTmpfs() để thêm bộ nhớ tệp tạm thời vào hộp cát.

Một hàm đặc biệt hữu ích là AddLibrariesForBinary() có chức năng thêm các thư viện và trình liên kết mà một tệp nhị phân yêu cầu.

Rất tiếc, việc thiết lập hệ thống lệnh gọi cho danh sách cho phép vẫn còn khá nhiều công việc thủ công. Tạo chính sách với các lệnh gọi hệ thống mà bạn biết rõ nhu cầu nhị phân của mình và chạy chính sách đó với khối lượng công việc chung. Nếu có lỗi vi phạm được kích hoạt, hãy đưa lệnh gọi hệ thống vào danh sách cho phép và lặp lại quy trình này. Nếu gặp phải một lỗi vi phạm mà bạn cho rằng có khả năng gây rủi ro khi đưa vào danh sách cho phép và chương trình này xử lý lỗi một cách linh hoạt, thì bạn có thể thử yêu cầu BlockSyscallWithErrno() trả về lỗi.

#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. Điều chỉnh giới hạn

Chính sách hộp cát ngăn Sandboxee gọi các syscall cụ thể, do đó, giảm bề mặt tấn công. Tuy nhiên, kẻ tấn công vẫn có thể gây ra các hiệu ứng không mong muốn bằng cách chạy một quy trình vô thời hạn hoặc làm cạn kiệt RAM và các tài nguyên khác.

Để giải quyết mối đe doạ này, Sandboxee chạy trong giới hạn thực thi chặt chẽ theo mặc định. Nếu các giới hạn mặc định này gây ra vấn đề cho quá trình thực thi hợp lệ của chương trình, bạn có thể điều chỉnh các giới hạn này bằng cách sử dụng lớp sandbox2::Limits bằng cách gọi limits() trên đối tượng trình thực thi.

Đoạn mã dưới đây cho thấy một số ví dụ về việc điều chỉnh hạn mức. Tất cả các tuỳ chọn có sẵn đều được nêu trong tệp tiêu đề 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));

Để biết ví dụ về cách sử dụng lớp sandbox2::Limits, hãy xem công cụ ví dụ.

4. Chạy Hộp cát

Trong các phần trước, bạn đã chuẩn bị môi trường hộp cát, chính sách, trình thực thi và Sandboxee. Bước tiếp theo là tạo và chạy đối tượng Sandbox2.

Chạy đồng bộ

Hộp cát có thể chạy đồng bộ, do đó sẽ chặn cho đến khi có kết quả. Đoạn mã dưới đây minh hoạ cách tạo thực thể của đối tượng Sandbox2 và quá trình thực thi đồng bộ của đối tượng đó. Để biết ví dụ chi tiết hơn, hãy xem phần tĩnh.

#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();

Chạy không đồng bộ

Bạn cũng có thể chạy hộp cát không đồng bộ, do đó không chặn cho đến khi có kết quả. Điều này rất hữu ích, chẳng hạn như khi giao tiếp với Sandboxee. Đoạn mã dưới đây minh hoạ trường hợp sử dụng này, để biết thêm ví dụ chi tiết, hãy xem crc4công cụ.

#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. Giao tiếp với Sandboxee

Theo mặc định, executor có thể giao tiếp với Sandboxee thông qua chỉ số mô tả tệp. Đây có thể là tất cả những gì bạn cần, chẳng hạn như nếu bạn chỉ muốn chia sẻ một tệp với Sandboxee hoặc đọc đầu ra tiêu chuẩn của Sandboxee.

Tuy nhiên, rất có thể bạn cần phải có logic giao tiếp phức tạp hơn giữa executor và Sandboxee. Bạn có thể dùng API comms (xem tệp tiêu đề comms.h) để gửi số nguyên, chuỗi, vùng đệm byte, protobufs hoặc chỉ số mô tả tệp.

Chia sẻ phần mô tả tệp

Khi sử dụng API giao tiếp giữa các quá trình (xem ipc.h), bạn có thể sử dụng MapFd() hoặc ReceiveFd():

  • Sử dụng MapFd() để liên kết chỉ số mô tả tệp từ trình thực thi đến Sandboxee. Bạn có thể sử dụng tệp này để chia sẻ tệp được mở từ trình thực thi để sử dụng trong Sandboxee. Bạn có thể xem ví dụ về cách sử dụng ở dạng tĩnh.

    // The executor opened /proc/version and passes it to the sandboxee as stdin
    executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
    
  • Sử dụng ReceiveFd() để tạo điểm cuối của socketPair. Bạn có thể dùng giá trị này để đọc dữ liệu đầu ra chuẩn hoặc lỗi chuẩn của Sandboxee. Bạn có thể xem ví dụ về cách sử dụng trong công cụ.

    // The executor receives a file descriptor of the sandboxee stdout
    int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
    

Sử dụng API giao tiếp

Sandbox2 cung cấp một API giao tiếp tiện lợi. Đây là một cách đơn giản và dễ dàng để chia sẻ số nguyên, chuỗi hoặc vùng đệm byte giữa executor và Sandboxee. Dưới đây là một số đoạn mã mà bạn có thể tìm thấy trong ví dụ về crc4.

Để bắt đầu với API giao tiếp, trước tiên, bạn phải lấy đối tượng giao tiếp từ đối tượng Sandbox2:

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

Khi đối tượng giao tiếp sẵn có, dữ liệu có thể được gửi tới Sandboxee bằng một trong các nhóm hàm Send*. Bạn có thể xem ví dụ về cách sử dụng API giao tiếp trong crc4. Đoạn mã dưới đây cho thấy một phần trích dẫn từ ví dụ đó. Trình thực thi gửi một unsigned char buf[size] với SendBytes(buf, size):

if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
  /* handle error */
}

Để nhận dữ liệu từ Sandboxee, hãy sử dụng một trong các hàm Recv*. Đoạn mã dưới đây là phần trích dẫn từ ví dụ về crc4. Trình thực thi nhận giá trị tổng kiểm ở một số nguyên 32 bit chưa ký: uint32_t crc4;

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

Chia sẻ dữ liệu với vùng đệm

Một chức năng chia sẻ dữ liệu khác là sử dụng API bộ đệm để chia sẻ lượng lớn dữ liệu và để tránh các bản sao tốn kém được gửi qua lại giữa trình thực thi và Sandboxee.

Trình thực thi tạo một Vùng đệm, theo kích thước và dữ liệu cần truyền, hoặc trực tiếp từ chỉ số mô tả tệp rồi truyền tệp này cho Sandboxee bằng cách sử dụng comms->SendFD() trong trình thực thi và comms->RecvFD() trong Sandboxee.

Trong đoạn mã dưới đây, bạn có thể thấy phía của executor. Hộp cát chạy không đồng bộ và chia sẻ dữ liệu qua vùng đệm với 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());

Về phía Sandboxee, bạn cũng phải tạo một đối tượng vùng đệm và đọc dữ liệu từ chỉ số mô tả tệp do trình thực thi gửi:

// 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. Thoát khỏi hộp cát

Tuỳ thuộc vào cách bạn chạy hộp cát (xem bước này), bạn phải điều chỉnh cách chấm dứt hộp cát và do đó cũng là Hộp cát.

Thoát khỏi hộp cát đang chạy đồng bộ

Nếu hộp cát đang chạy một cách đồng bộ thì Run (Chạy) sẽ chỉ trả về khi Sandboxee đã hoàn tất. Do đó, bạn không cần phải thực hiện thêm bước nào để chấm dứt. Đoạn mã dưới đây cho thấy trường hợp này:

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

Thoát khỏi hộp cát chạy không đồng bộ

Nếu hộp cát đang chạy không đồng bộ thì có hai tuỳ chọn để kết thúc. Trước tiên, bạn chỉ cần đợi Sandboxee hoàn tất và nhận được trạng thái thực thi cuối cùng:

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

Ngoài ra, bạn có thể tắt Sandboxee bất cứ lúc nào. Tuy nhiên, bạn vẫn nên gọi AwaitResult() vì Sandboxee có thể chấm dứt vì một lý do khác trong thời gian chờ đợi:

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

7. Kiểm thử

Giống như bất kỳ mã nào khác, việc triển khai hộp cát của bạn phải có các bài kiểm thử. Kiểm thử hộp cát không nhằm mục đích kiểm tra tính chính xác của chương trình, mà thay vào đó là kiểm tra xem chương trình có hộp cát có thể chạy mà không gặp các vấn đề như vi phạm hộp cát hay không. Điều này cũng đảm bảo rằng chính sách hộp cát là chính xác.

Chương trình hộp cát được kiểm thử giống như cách bạn chạy chương trình chính thức, với các đối số và tệp đầu vào mà chương trình thường xử lý.

Các quy trình kiểm thử này có thể đơn giản như kiểm thử shell hoặc kiểm thử C++ bằng cách sử dụng các quy trình phụ. Hãy xem ví dụ để tìm cảm hứng.

Kết luận

Cảm ơn bạn đã đọc đến đây, chúng tôi hy vọng bạn thích hướng dẫn của chúng tôi và giờ đây, bạn có động lực để tạo hộp cát của riêng mình nhằm giữ an toàn cho người dùng.

Tạo hộp cát và chính sách là một nhiệm vụ khó khăn và dễ xảy ra các lỗi nhỏ. Để đảm bảo an toàn, bạn nên nhờ một chuyên gia bảo mật xem xét chính sách và mã của bạn.