開始使用 Sandbox2

本頁將說明如何使用 Sandbox2 建立自己的沙箱環境。您將學習如何定義沙箱政策,以及一些進階但常見的調整項目。請使用此處的資訊,在標頭檔案中的範例和程式碼說明文件一併使用。

1. 選擇沙箱執行器方法

沙箱作業會從執行 Sandboxee 的執行者開始 (請參閱「沙箱執行程式」)。executor.h 標頭檔案包含這項作業所需的 API。這個 API 非常有彈性,可讓您挑選最適合自身用途的 API。以下各節將說明 3 種可供選擇的方法。

方法 1:獨立 - 執行已啟用沙箱功能的二進位檔

這是使用沙箱最簡單的方法,當您想要對沒有原始碼的整個二進位檔進行沙箱時,建議使用這個方法。這也是使用沙箱最安全的方式,因為沒有任何沙箱初始化作業可能導致不良影響。

在下列程式碼片段中,我們會定義要採用沙箱機制的二進位檔路徑,以及必須傳遞至執行系統呼叫的引數。如 executor.h 標頭檔案中,我們未指定 envp 的值,因此會從父項程序複製環境。請注意,第一個引數一律是要執行的程式名稱,我們的程式碼片段不會定義任何其他引數。

這個執行器方法的範例包括:靜態工具

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

方法 2: Sandbox2 Forkserver – 告知執行者何時要採用沙箱機制

這個方法可靈活地在初始化期間解除沙箱機制,然後呼叫 ::sandbox2::Client::SandboxMeHere() 來選擇進入沙箱模式的時機。要求在要開始沙箱時,能夠在程式碼中定義,且必須為單執行緒 (詳情請參閱常見問題)。

在下方的程式碼片段中,我們使用的是上方方法 1 所述的程式碼。不過,為了允許程式在初始化期間,以無沙箱的方式執行,我們呼叫 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);

由於執行者現在已經停用沙箱,直到沙箱通知為止,因此我們必須建立 ::sandbox2::Client 執行個體,設定執行者和 Sandboxee 之間的通訊,然後通知執行者初始化完成,我們要呼叫 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();
  …

這個執行者方法的例子是 crc4,其中 crc4bin.cc 是 Sandboxee,並在應進入沙箱時通知執行者 (crc4sandbox.cc)。

方法 3:自訂 Forkserver – 準備二進位檔、自行等待分支要求和沙箱

這個模式可讓您啟動二進位檔、為沙箱做好準備,並在二進位檔生命週期的特定時刻提供執行程式。

執行程式會將分支要求傳送至您的二進位檔,該二進位檔是透過 ::sandbox2::ForkingClient::WaitAndFork() fork() 傳送。新建立的程序即可與 ::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());

請注意,這個模式相當複雜,僅適用於少數特定情況 (例如記憶體需求有限時)。您將享有 COW 的好處,但缺點是沒有真正的 ASLR。另一個典型的使用例子是,沙箱在處理不受信任的資料前,執行長時間且大量使用 CPU 的初始化作業。

如需此執行器方法的範例,請參閱 custom_fork

2. 建立沙箱政策

取得執行程式後,建議您為沙箱定義沙箱政策。否則, Sandboxee 將僅受到預設 Syscall 政策的保護。

我們的目標是限製沙箱作業的系統呼叫和引數,以及沙箱政策可存取的檔案。您必須詳細瞭解規劃在沙箱中的程式碼所需的系統呼叫。觀察系統呼叫的其中一種方法是使用 Linux 的指令列工具追蹤記錄來執行程式碼。

取得系統呼叫的清單後,即可使用 PolicyBuilder 定義政策。PolicyBuilder 提供許多便利和輔助函式,可讓您執行許多常見作業。下列清單僅列舉部分可用函式:

  • 將任何程序啟動的系統呼叫加入許可清單:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • 將所有已開啟/讀取/寫入* syscall 加入許可清單:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • 將所有離開事件/存取權/狀態相關 syscall 加入許可清單:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • 將任何與睡眠/時間相關的系統呼叫加入許可清單:
    • AllowTime();
    • AllowSleep();

這些便利函式會將所有相關的系統呼叫加入許可清單。相同的政策有利於無法使用某些系統呼叫的不同架構 (例如 ARM64 沒有開啟系統呼叫),但會帶來比必要的系統更多安全風險。舉例來說,AllowOpen() 可讓沙箱呼叫任何開放式相關的系統呼叫。如果只想將特定系統呼叫加入許可清單,可以使用 AllowSyscall(); 一次允許多個系統呼叫。您可以使用 AllowSyscalls()

到目前為止,政策只會檢查 syscall ID。如果您需要進一步強化政策,並想定義僅允許特定引數的系統呼叫的政策,您必須使用 AddPolicyOnSyscall()AddPolicyOnSyscalls()。這些函式不僅將 syscall ID 做為引數,也會使用 Linux kernel 中的 bpf 輔助程式巨集,來則是原始的 seccomp-bpf 篩選器。如要進一步瞭解 BPF,請參閱核心說明文件。如果您發現自己撰寫的重複 BPF 程式碼,且認為應該產生可用性包裝函式,歡迎提出功能要求。

除了 syscall 相關的函式外,PolicyBuilder 也提供多個與檔案系統相關的函式 (例如 AddFile()AddDirectory()),可將檔案/目錄繫結至沙箱。AddTmpfs() 輔助程式可用於在沙箱中新增暫存檔案儲存空間。

AddLibrariesForBinary() 特別有用,可以加入二進位檔所需的程式庫和連接器。

但處理系統呼叫後仍需要手動操作。使用已知二進位檔需求的系統呼叫建立政策,然後透過常見的工作負載執行該政策。如果觸發違規事件,請將系統呼叫加入許可清單,並重複執行該程序。如果發現疑似有風險的違規情事,您也許有危險,而程式能妥善處理錯誤,您可以嘗試使用 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. 調整限制

沙箱政策可防止沙箱政策呼叫特定系統呼叫,進而減少受攻擊面。不過,攻擊者或許還是可以無限期執行程序,或耗盡 RAM 和其他資源,造成不想要的影響。

為因應這項威脅,沙箱預設會在嚴格執行限制下執行。如果這些預設限制會導致程式的正常執行發生問題,您可以在執行程式物件上呼叫 limits(),使用 sandbox2::Limits 類別進行調整。

以下程式碼片段列舉了一些調整上限的範例。所有可用選項都已記錄在 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));

如需 sandbox2::Limits 類別的使用範例,請參閱工具範例。

4. 執行沙箱

在前幾節中,您已備妥沙箱環境、政策、執行者和沙箱。下一步是建立並執行 Sandbox2 物件。

同步執行

沙箱可以同步執行,因此在產生結果之前會進行封鎖。以下程式碼片段示範 Sandbox2 物件及其同步執行作業的例項。如需更詳細的範例,請參閱靜態一節。

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

以非同步方式執行

您也可以非同步執行沙箱,直到出現結果為止。例如,在與 Sandboxee 進行通訊時,這項功能就非常實用。以下程式碼片段示範了這個用途。如需更詳細的範例,請參閱 crc4工具

#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. 與 Sandboxee 通訊

根據預設,執行程式可以透過檔案描述元與 Sandboxee 通訊。這可能會派上用場,例如,只想與沙箱分享檔案,或讀取 Sandboxee 的標準輸出內容。

但您很可能需要執行程式和 Sandboxee 之間更複雜的通訊邏輯。comms API (請參閱 comms.h 標頭檔案) 可用來傳送整數、字串、位元組緩衝區、protobuf 或檔案描述元。

共用檔案描述元

透過 Inter-Process Communication API (請參閱 ipc.h) 即可使用 MapFd()ReceiveFd()

  • 使用 MapFd() 將執行程式中的檔案描述元對應至 Sandboxee。這可用於分享透過執行程式開啟的檔案,以便在 Sandboxee 中使用。使用範例可在「靜態」部分查看。

    // The executor opened /proc/version and passes it to the sandboxee as stdin
    executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
    
  • 使用 ReceiveFd() 建立通訊端配對端點。這可用於讀取沙箱傳送的標準輸出內容或標準錯誤。可以在工具中查看使用範例。

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

使用通訊 API

Sandbox2 提供便利的通訊 API。以簡單又輕鬆的方式,在執行器和 Sandboxee 之間共用整數、字串或位元組緩衝區。以下是 crc4 範例中的部分程式碼片段。

如要開始使用 comms API,您必須先從 Sandbox2 物件取得 comms 物件:

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

一旦通訊對像有可用的通訊物件,即可使用 Send* 系列函式將資料傳送給沙箱。您可以在 crc4 範例中找到 comms API 的使用範例。下方程式碼片段顯示該範例的摘錄。執行工具傳送具有 SendBytes(buf, size)unsigned char buf[size]

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

如要接收 Sandboxee 的資料,請使用其中一個 Recv* 函式。下列程式碼片段摘錄自 crc4 範例。執行工具會以 32 位元無正負號整數接收總和檢查碼:uint32_t crc4;

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

與緩衝區分享資料

另一個資料共用功能,就是使用緩衝區 API 共用大量資料,並避免執行程式和 Sandboxee 之間來回傳送所費大量的大量資料。

執行程式建立緩衝區,並按大小和資料傳遞,或直接從檔案描述元建立緩衝區,然後在執行程式中使用 comms->SendFD() 和沙箱中的 comms->RecvFD() 將其傳遞給沙箱。

在下方程式碼片段中,您會看到執行程式的一面。沙箱會以非同步方式執行,並透過緩衝區與沙箱分享資料:

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

在沙箱的端,您還必須建立緩衝區物件,並從執行者傳送的檔案描述元讀取資料:

// 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. 退出沙箱

視您執行沙箱的方式而定 (請參閱這個步驟),您必須調整終止沙箱的方式,連帶調整沙箱模式。

退出同步執行的沙箱

如果沙箱已同步執行,則只有在沙箱執行完畢時才會傳回「Run」。因此,您不需要採取額外的終止步驟。以下程式碼片段說明這個情況:

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

退出以非同步方式執行的沙箱

如果沙箱是以非同步方式執行,則有兩個終止選項可用。首先,您可以等到沙箱作業完成,並收到最終執行狀態:

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

或者,您也可以隨時終止沙箱,但建議您呼叫 AwaitResult(),因為沙箱可能會因為其他原因而終止:

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

7. 測試

和任何其他程式碼一樣,沙箱實作應進行測試。沙箱測試不是測試程式的正確性,而是要檢查沙箱程式是否能執行,避免發生沙箱違規等問題。這也會確保沙箱政策正確無誤。

沙箱程式的測試方式與在實際工作環境中執行的方式相同,包含正常處理的引數和輸入檔案。

這些測試可以像殼層測試或使用子程序的 C++ 測試一樣簡單。請參考範例來汲取靈感。

結語

感謝您閱讀目前為止的內容,希望您喜歡我們的指南,現在也樂於打造自己的沙箱保護使用者安全。

建立沙箱和政策並非易事,而且容易細微錯誤。為維護安全,建議您請安全專家檢閱您的政策和程式碼。