本頁說明如何使用 Sandbox2 建立自己的沙箱環境。您將瞭解如何定義沙箱政策,以及一些常見的進階調整項目。請參考本文資訊和標頭檔案中的範例與程式碼說明文件。
1. 選擇沙箱執行器方法
沙箱作業會從執行器 (請參閱「沙箱執行器」) 開始,負責執行 Sandboxee。executor.h 標頭檔案包含此用途所需的 API。這項 API 非常彈性,可讓您選擇最適合自己用途的項目。以下各節說明瞭 3 種不同的方法,您可以選擇其中一種。
方法 1:獨立執行 - 執行已啟用沙箱的二進位檔
這是最簡單的沙箱化方式,如果您想沙箱化整個沒有原始碼的二進位檔,建議使用這個方法。這也是最安全的沙箱機制使用方式,因為沒有可能產生負面影響的未沙箱化初始化作業。
在下列程式碼片段中,我們會定義要進行沙箱處理的二進位檔路徑,以及必須傳遞至 execve 系統呼叫的引數。如您在 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);
由於執行器現在會停用沙箱,直到 Sandboxee 通知為止,因此我們必須建立 ::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 - 準備二進位檔、等待 fork 要求,並自行沙箱化
這個模式可讓您啟動二進位檔、準備進行沙箱化,並在二進位檔生命週期的特定時刻,將其提供給執行器。
執行器會將分叉要求傳送至二進位檔,該二進位檔會透過 fork()
(經由 ::sandbox2::ForkingClient::WaitAndFork()
) 建立新程序,並準備好透過 ::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。另一個常見的用法範例是,Sandboxee 有耗用大量 CPU 的長時間初始化作業,可在處理不受信任的資料前執行。
如需這個執行器方法的範例,請參閱「custom_fork」。
2. 建立沙箱政策
取得執行器後,您可能會想為 Sandboxee 定義 Sandbox 政策。否則,Sandboxee 只會受到預設系統呼叫政策保護。
沙箱政策的目標是限制 Sandboxee 可發出的系統呼叫和引數,以及可存取檔案。您必須詳細瞭解要將程式碼沙箱化時所需的系統呼叫。如要觀察系統呼叫,其中一種方式是使用 Linux 的指令列工具 strace 執行程式碼。
取得系統呼叫清單後,您可以使用 PolicyBuilder 定義政策。PolicyBuilder 隨附許多便利和輔助函式,可執行多項常見作業。以下僅列出部分可用函式:
- 允許程序啟動的任何系統呼叫:
AllowStaticStartup();
AllowDynamicStartup();
- 將任何 open/read/write* 系統呼叫加入許可清單:
AllowOpen();
AllowRead();
AllowWrite();
- 將任何與結束/存取/狀態相關的系統呼叫加入許可清單:
AllowExit();
AllowStat();
AllowAccess();
- 將任何睡眠/時間相關的系統呼叫加入許可清單:
AllowTime();
AllowSleep();
這些便利函式會將所有相關的系統呼叫加入許可清單。這項做法的優點是,您可以在不同架構中使用相同政策 (某些系統呼叫不適用於這些架構,例如 ARM64 沒有 OPEN 系統呼叫),但缺點是可能會啟用不必要的系統呼叫,造成輕微的安全風險。舉例來說,AllowOpen() 可讓 Sandboxee 呼叫任何與開啟相關的系統呼叫。如要只將一個特定系統呼叫加入允許清單,可以使用 AllowSyscall();
。如要一次加入多個系統呼叫,可以使用 AllowSyscalls()
。
這項政策目前只會檢查系統呼叫 ID。如果您需要進一步強化政策,並想定義只允許使用特定引數的系統呼叫政策,則必須使用 AddPolicyOnSyscall()
或 AddPolicyOnSyscalls()
。這些函式不僅會將系統呼叫 ID 做為引數,也會使用 Linux 核心的 bpf 輔助巨集,將原始 seccomp-bpf 篩選器做為引數。如要進一步瞭解 BPF,請參閱核心說明文件。如果您發現自己編寫的 BPF 程式碼重複,且認為應該有可用性包裝函式,歡迎提出功能要求。
除了與系統呼叫相關的函式,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. 調整限制
沙箱政策可防止 Sandboxee 呼叫特定系統呼叫,進而縮減攻擊範圍。不過,攻擊者仍可能無限期執行程序,或耗盡 RAM 和其他資源,造成不良影響。
為解決這項威脅,Sandboxee 預設會在嚴格的執行限制下執行。如果這些預設限制導致程式無法正常執行,您可以呼叫執行器物件上的 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. 執行沙箱
在先前的章節中,您準備了沙箱環境、政策、執行器和 Sandboxee。下一步是建立 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 和 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. 與 Sandboxee 通訊
依預設,執行器可以透過檔案描述元與 Sandboxee 通訊。舉例來說,如果您只想與 Sandboxee 分享檔案,或讀取 Sandboxee 的標準輸出內容,可能只需要這樣做。
不過,您很可能需要在執行器和 Sandboxee 之間建立更複雜的通訊邏輯。通訊 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()
建立 socketpair 端點。可用於讀取 Sandboxee 的標準輸出或標準錯誤。您可以在工具中查看使用範例。// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
使用通訊 API
Sandbox2 提供方便的 comms API。這是簡單易用的方法,可在執行器和 Sandboxee 之間共用整數、字串或位元組緩衝區。以下是 crc4 範例中的一些程式碼片段。
如要開始使用通訊 API,請先從 Sandbox2 物件取得通訊物件:
sandbox2::Comms* comms = s2.comms();
通訊物件可用後,即可使用 Send* 系列函式,將資料傳送至 Sandboxee。您可以在 crc4 範例中找到通訊 API 的使用範例。以下程式碼片段是該範例的節錄內容。執行器會傳送含有 SendBytes(buf, size)
的 unsigned char
buf[size]
,並提供 SendBytes(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()
和 Sandboxee 中的 comms->RecvFD()
,將緩衝區傳遞至 Sandboxee。
在下方的程式碼片段中,您可以看到執行器的端點。沙箱會以非同步方式執行,並透過緩衝區與 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());
在 Sandboxee 端,您也必須建立緩衝區物件,並從執行器傳送的檔案描述元讀取資料:
// 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. 退出沙箱
視您執行沙箱的方式而定 (請參閱這個步驟),您必須調整終止沙箱的方式,因此也必須調整終止 Sandboxee 的方式。
同步結束沙箱執行作業
如果沙箱是同步執行,Run 只會在 Sandboxee 完成時傳回。因此無須採取額外步驟終止服務。下列程式碼片段說明這個情境:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
結束以非同步方式執行的沙箱
如果沙箱是以非同步方式執行,則有兩種終止方式。首先,您可以等待 Sandboxee 完成,並接收最終執行狀態:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
您也可以隨時終止 Sandboxee,但仍建議呼叫 AwaitResult()
,因為 Sandboxee 可能會因其他原因終止:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. 測試
與任何其他程式碼一樣,沙箱實作項目也應進行測試。沙箱測試並非用來測試程式的正確性,而是檢查沙箱程式是否能順利執行,不會發生沙箱違規等問題。這也能確保沙箱政策正確無誤。
測試沙箱化程式的方式,與在正式環境中執行程式的方式相同,包括使用程式通常會處理的引數和輸入檔案。
這些測試可以簡單到只是殼層測試,也可以是使用子程序的 C++ 測試。如需靈感,請參閱範例。
結論
感謝您閱讀本指南。希望這份指南對您有幫助,讓您更有信心建立自己的沙箱,協助保護使用者安全。
建立沙箱和政策並不容易,而且容易發生細微錯誤。 為確保安全無虞,建議您請安全專家審查政策和程式碼。