Bu sayfada, Sandbox2 ile kendi korumalı alan ortamınızı nasıl oluşturacağınızı öğreneceksiniz. Korumalı alan politikasını nasıl tanımlayacağınızı ve bazı gelişmiş ancak yaygın ince ayarları öğreneceksiniz. Üstbilgi dosyalarındaki örnekler ve kod dokümanlarının yanı sıra buradaki bilgileri de kılavuz olarak kullanın.
1. Bir Sandbox Executor yöntemi seçin
Korumalı alan oluşturma, Sandbox Executor'ı (Korumalı Alan Yürütücüsü) çalıştırmaktan sorumlu olan bir yürütücüyle (Sandboxee) başlar. executor.h üstbilgi dosyası, bu amaç için gereken API'yi içerir. API çok esnektir ve kullanım alanınıza en uygun olanı seçmenize olanak tanır. Aşağıdaki bölümlerde, seçebileceğiniz 3 farklı yöntem açıklanmaktadır.
1. yöntem: Bağımsız - Sanal alan oluşturma özelliği zaten etkinleştirilmiş bir ikili dosyayı yürütün
Korumalı alan oluşturmanın en basit yolu budur ve kaynak koduna sahip olmadığınız bir ikili programın tamamını korumalı alana almak istediğinizde önerilen yöntemdir. Ayrıca, olumsuz etkilere yol açabilecek korumalı alan dışı başlatma işlemi olmadığından korumalı alan kullanmanın en güvenli yoludur.
Aşağıdaki kod snippet'inde, korumalı alana alınacak ikili programın yolunu ve bir execve sistem çağrısına iletmemiz gereken bağımsız değişkenleri tanımlıyoruz. executor.h üstbilgi dosyasında görebileceğiniz gibi, envp için bir değer belirtmiyoruz ve bu nedenle ortamı üst işlemden kopyalıyoruz. İlk bağımsız değişkenin her zaman yürütülecek programın adı olduğunu ve kod snippet'imizin başka bir bağımsız değişken tanımlamadığını unutmayın.
Bu yürütücü yöntemine örnek olarak static ve tool verilebilir.
#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. yöntem: Sandbox2 Forkserver - Yürütücüye ne zaman korumalı alan oluşturulacağını söyleme
Bu yöntem, başlatma sırasında korumalı alan dışında kalma ve ardından ::sandbox2::Client::SandboxMeHere() çağırarak korumalı alana girme esnekliği sunar. Bu yöntem, sanal alan oluşturmaya ne zaman başlamak istediğinizi kodda tanımlamanızı gerektirir ve tek iş parçacıklı olmalıdır (nedenini SSS bölümünde okuyun).
Aşağıdaki kod snippet'inde, yukarıdaki 1. yöntemde açıklanan kodun aynısını kullanıyoruz. Ancak programın başlatma sırasında korumalı alan dışında çalıştırılmasına izin vermek için set_enable_sandbox_before_exec(false) işlevini çağırırız.
#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);
Yürütücünün korumalı alanı, Sandboxee tarafından bildirim alınana kadar devre dışı bırakıldığından bir ::sandbox2::Client örneği oluşturmamız, yürütücü ile Sandboxee arasında iletişim kurmamız ve ardından yürütücüye başlatma işleminin tamamlandığını ve sandbox2_client.SandboxMeHere() yöntemini çağırarak korumalı alan oluşturmaya başlamak istediğimizi bildirmemiz gerekir.
// 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();
…
Bu yürütücü yöntemine örnek olarak crc4 verilebilir. Burada crc4bin.cc, korumalı alan uygulayıcısıdır ve korumalı alana girmesi gerektiğinde yürütücüyü (crc4sandbox.cc) bilgilendirir.
3. yöntem: Özel Forkserver: İkili program hazırlayın, fork isteklerini bekleyin ve korumalı alanı kendiniz oluşturun.
Bu mod, bir ikiliyi başlatmanıza, sanal ortamda çalıştırmaya hazırlamanıza ve ikilinizin yaşam döngüsünün belirli bir anında yürütücüye sunmanıza olanak tanır.
Yürütücü, ikili programınıza bir çatallama isteği gönderir. Bu istek, fork() (::sandbox2::ForkingClient::EnterForkLook() aracılığıyla) işlemini gerçekleştirir. Yeni oluşturulan işlem, ::sandbox2::Client::SandboxMeHere() ile korumalı alana alınmaya hazır olur.
#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());
Bu modun oldukça karmaşık olduğunu ve yalnızca birkaç özel durumda (ör. katı bellek gereksinimleriniz olduğunda) geçerli olduğunu unutmayın. COW'dan yararlanabilirsiniz ancak gerçek ASLR olmaması dezavantajdır. Başka bir tipik kullanım örneği, Sandboxee'nin güvenilmeyen veriler işlenmeden önce çalıştırılabilecek uzun ve CPU yoğun bir başlatma işlemine sahip olmasıdır.
Bu yürütücü yönteminin bir örneği için custom_fork'a bakın.
2. Korumalı alan politikası oluşturma
Bir yürütücünüz olduğunda, Sandboxee için bir Sandbox Politikası tanımlamak isteyebilirsiniz. Aksi takdirde Sandboxee yalnızca Varsayılan Syscall Politikası tarafından korunur.
Korumalı alan politikasının amacı, korumalı alanın yapabileceği sistem çağrılarını ve bağımsız değişkenleri, ayrıca erişebileceği dosyaları kısıtlamaktır. Korumalı alana almak istediğiniz kodun gerektirdiği sistem çağrılarını ayrıntılı bir şekilde anlamanız gerekir. Sistem çağrılarını gözlemlemenin bir yolu, kodu Linux'un komut satırı aracı strace ile çalıştırmaktır.
Sistem çağrıları listesini aldıktan sonra politikayı tanımlamak için PolicyBuilder'ı kullanabilirsiniz.
PolicyBuilder, birçok yaygın işlemi (çoğu DefaultPolicyBuilder() içinde yer alır) yapmanıza olanak tanıyan birçok kolaylık ve yardımcı işlevle birlikte gelir. Aşağıdaki liste, kullanılabilir işlevlerin yalnızca küçük bir kısmını içerir:
- İşlem başlatma için tüm sistem çağrılarını izin verilenler listesine ekleyin:
AllowStaticStartup();AllowDynamicStartup();
- Tüm open/read/write* sistem çağrılarına izin verin:
AllowOpen();AllowRead();AllowWrite();
- Çıkış/erişim/durumla ilgili tüm sistem çağrılarını izin verilenler listesine ekleyin:
AllowExit();AllowStat();AllowAccess();
- Uyku/zamanla ilgili tüm sistem çağrılarını izin verilenler listesine ekleyin:
AllowTime();AllowSleep();
Bu kolaylık işlevleri, ilgili tüm sistem çağrılarını izin verilenler listesine ekler. Bu, belirli sistem çağrılarının kullanılamadığı farklı mimarilerde aynı politikanın kullanılabilmesi avantajını sağlar (ör.ARM64'te OPEN sistem çağrısı yoktur). Ancak gerekenden daha fazla sistem çağrısının etkinleştirilmesi küçük bir güvenlik riski oluşturur. Örneğin, AllowOpen(), Sandboxee'nin açık olan tüm sistem çağrılarını çağırmasına olanak tanır. Yalnızca belirli bir sistem çağrısını izin verilenler listesine eklemek istiyorsanız AllowSyscall();, birden fazla sistem çağrısını aynı anda izin verilenler listesine eklemek istiyorsanız AllowSyscalls() kullanabilirsiniz.
Politika şu ana kadar yalnızca syscall tanımlayıcısını kontrol eder. Politikayı daha da güçlendirmek istiyorsanız ve yalnızca belirli bağımsız değişkenlere sahip bir sistem çağrısına izin veren bir politika tanımlamak istiyorsanız AddPolicyOnSyscall() veya AddPolicyOnSyscalls() kullanmanız gerekir. Bu işlevler, sistem çağrısı kimliğini bağımsız değişken olarak almanın yanı sıra Linux çekirdeğindeki bpf yardımcı makrolarını kullanarak ham bir seccomp-bpf filtresi de alır. BPF hakkında daha fazla bilgi için çekirdek
belgelerini inceleyin. Kullanılabilirlik sarmalayıcısı olması gerektiğini düşündüğünüz tekrar eden BPF kodu yazdığınızı fark ederseniz özellik isteğinde bulunabilirsiniz.
PolicyBuilder, sistem çağrısıyla ilgili işlevlerin yanı sıra bir dosya/dizini korumalı alana bağlamak için AddFile() veya AddDirectory() gibi dosya sistemiyle ilgili çeşitli işlevler de sağlar. Korumalı alana geçici bir dosya depolama alanı eklemek için AddTmpfs() yardımcısı kullanılabilir.
Özellikle yararlı bir işlev, bir ikili programın gerektirdiği kitaplıkları ve bağlayıcıyı ekleyen AddLibrariesForBinary() işlevidir.
İzin verilenler listesine eklenecek sistem çağrılarını belirlemek maalesef hâlâ biraz manuel çalışma gerektiriyor. İkili programınızın ihtiyaç duyduğunu bildiğiniz sistem çağrılarını içeren bir politika oluşturun ve bu politikayı yaygın bir iş yüküyle çalıştırın. Bir ihlal tetiklenirse sistem çağrısını izin verilenler listesine ekleyin ve işlemi tekrarlayın. İzin verilenler listesine eklemenin riskli olabileceğini düşündüğünüz bir ihlalle karşılaşırsanız ve program hataları düzgün bir şekilde işliyorsa BlockSyscallWithErrno() ile hata döndürmesini sağlayabilirsiniz.
Aşağıdaki örnek, sandbox2::PolicyBuilder doğrudan oluşturarak write ve mprotect bağımsız değişkenlerinin kısıtlanmasını ve openat'ün engellenmesini (aksi takdirde DefaultPolicyBuilder()'in izin vereceği) gösterir:
#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. Sınırları ayarlama
Korumalı Alan Politikası, korumalı alanın belirli sistem çağrılarını çağırmasını engeller ve böylece saldırı yüzeyini azaltır. Ancak saldırganlar, bir işlemi süresiz olarak çalıştırarak veya RAM'i ve diğer kaynakları tüketerek istenmeyen etkilere neden olabilir.
Bu tehdidi önlemek için Sandboxee varsayılan olarak sıkı yürütme sınırları altında çalışır. Bu varsayılan sınırlar, programınızın meşru şekilde yürütülmesinde sorunlara neden olursa yürütücü nesnesinde limits() çağrısı yaparak sandbox2::Limits sınıfını kullanarak bunları ayarlayabilirsiniz.
Aşağıdaki kod snippet'inde, sınır ayarlamalarıyla ilgili bazı örnekler gösterilmektedir. Kullanılabilen tüm seçenekler limits.h üst dosyasında belgelenmiştir.
// 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 sınıfının kullanımına ilişkin bir örnek için örnek araca bakın.
4. Korumalı alanı çalıştırma
Önceki bölümlerde korumalı alan ortamını, politikayı, yürütücüyü ve Sandboxee'yi hazırladınız. Bir sonraki adım, Sandbox2 nesnesini oluşturup çalıştırmaktır.
Eşzamanlı çalıştırma
Korumalı alan eşzamanlı olarak çalışabilir ve sonuç alınana kadar engelleme yapabilir. Aşağıdaki kod snippet'inde Sandbox2 nesnesinin oluşturulması ve eşzamanlı olarak yürütülmesi gösterilmektedir. Daha ayrıntılı bir örnek için static sayfasına bakın.
#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();
Eşzamansız olarak çalıştırma
Korumalı alanı eşzamansız olarak da çalıştırabilirsiniz. Böylece, sonuç alınana kadar engelleme yapılmaz. Örneğin, Sandboxee ile iletişim kurarken bu özellikten yararlanabilirsiniz. Aşağıdaki kod snippet'i bu kullanım alanını gösterir. Daha ayrıntılı örnekler için crc4 ve tool sayfalarına bakın.
#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. Korumalı alanla iletişim kurma
Varsayılan olarak, yürütücü, dosya tanımlayıcılar aracılığıyla korumalı alanla iletişim kurabilir. Örneğin, yalnızca korumalı alanla dosya paylaşmak veya korumalı alanın standart çıkışını okumak istiyorsanız ihtiyacınız olan tek şey bu olabilir.
Ancak büyük olasılıkla yürütücü ile korumalı alan arasında daha karmaşık bir iletişim mantığına ihtiyacınız vardır. İletişim API'si (bkz. comms.h üstbilgi dosyası), tam sayılar, dizeler, bayt arabellekleri, protobuf'lar veya dosya tanımlayıcıları göndermek için kullanılabilir.
Dosya tanımlayıcılarını paylaşma
İşlemler Arası İletişim API'sini (bkz. ipc.h) kullanarak MapFd() veya ReceiveFd() kullanabilirsiniz:
Yürütücüden Sandboxee'ye dosya tanımlayıcılarını eşlemek için
MapFd()kullanın. Bu, Sandboxee'de kullanılmak üzere yürütücüden açılan bir dosyayı paylaşmak için kullanılabilir. Örnek bir kullanım static içinde görülebilir.// The executor opened /proc/version and passes it to the sandboxee as stdin executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);Soket çifti uç noktası oluşturmak için
ReceiveFd()kullanın. Bu, Sandboxee'nin standart çıkışını veya standart hatalarını okumak için kullanılabilir. Kullanım örneğini araçta görebilirsiniz.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
İletişim API'sini kullanma
Sandbox2, kullanışlı bir comms API sağlar. Bu, yürütücü ile Sandboxee arasında tam sayıları, dizeleri veya bayt arabelleklerini paylaşmanın basit ve kolay bir yoludur. Aşağıda, crc4 örneğinde bulabileceğiniz bazı kod snippet'leri verilmiştir.
İletişim API'sini kullanmaya başlamak için öncelikle Sandbox2 nesnesinden iletişim nesnesini almanız gerekir:
sandbox2::Comms* comms = s2.comms();
İletişim nesnesi kullanılabilir olduğunda, Send* işlev ailesinden biri kullanılarak Sandboxee'ye veri gönderilebilir. İletişim API'sinin kullanımına dair bir örneği crc4 örneğinde bulabilirsiniz. Aşağıdaki kod snippet'inde bu örnekten bir alıntı gösterilmektedir. Yürütücü, SendBytes(buf, size) ile bir unsigned char
buf[size] gönderir:
if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
/* handle error */
}
Korumalı alandan veri almak için Recv* işlevlerinden birini kullanın. Aşağıdaki kod snippet'i, crc4 örneğinden alınmıştır. Yürütücü, sağlama toplamını 32 bitlik işaretsiz bir tam sayı olarak alır: uint32_t crc4;
if (!(comms->RecvUint32(&crc4))) {
/* handle error */
}
Verileri arabelleklerle paylaşma
Diğer bir veri paylaşımı işlevi de büyük miktarda veri paylaşmak ve yürütücü ile Sandboxee arasında gönderilip alınan pahalı kopyaları önlemek için buffer API'yi kullanmaktır.
Yürütücü, boyuta ve aktarılacak verilere göre veya doğrudan bir dosya tanımlayıcıdan bir arabellek oluşturur ve yürütücüde comms->SendFD(), Sandboxee'de ise comms->RecvFD() kullanarak Sandboxee'ye aktarır.
Aşağıdaki kod snippet'inde yürütücünün tarafını görebilirsiniz. Korumalı alan, eşzamansız olarak çalışır ve paylaşılan verileri Sandboxee ile arabellek üzerinden paylaşır:
// 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());
Korumalı alan tarafında da bir arabellek nesnesi oluşturmanız ve yürütücünün gönderdiği dosya tanımlayıcıdan verileri okumanız gerekir:
// 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. Korumalı alandan çıkma
Korumalı alanı nasıl çalıştırdığınıza bağlı olarak (bu adıma bakın) korumalı alanı ve dolayısıyla Sandboxee'yi sonlandırma şeklinizi de ayarlamanız gerekir.
Eşzamanlı olarak çalışan bir korumalı alandan çıkma
Sandbox eşzamanlı olarak çalışıyorsa Run yalnızca Sandboxee tamamlandığında geri döner. Bu nedenle, fesih için ek bir adım gerekmez. Aşağıdaki kod snippet'i bu senaryoyu göstermektedir:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
Eşzamansız olarak çalışan bir korumalı alandan çıkma
Korumalı alan eşzamansız olarak çalışıyorsa sonlandırma için iki seçenek kullanılabilir. İlk olarak, Sandboxee'nin tamamlanmasını bekleyip nihai yürütme durumunu alabilirsiniz:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
Alternatif olarak, Sandboxee'yi istediğiniz zaman sonlandırabilirsiniz ancak Sandboxee bu sırada başka bir nedenle sonlandırılabileceğinden AwaitResult() işlevini çağırmanız önerilir:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Test
Diğer tüm kodlarda olduğu gibi, korumalı alan uygulamanızda da testler olmalıdır. Korumalı alan testleri, programın doğruluğunu test etmek için değil, korumalı alanda çalışan programın korumalı alan ihlalleri gibi sorunlar olmadan çalışıp çalışmadığını kontrol etmek için tasarlanmıştır. Bu sayede korumalı alan politikasının doğru olduğundan da emin olunur.
Korumalı alana alınmış bir program, normalde işleyeceği bağımsız değişkenler ve giriş dosyalarıyla birlikte, üretimde çalıştıracağınız şekilde test edilir.
Bu testler, kabuk testi veya alt süreçleri kullanan C++ testleri kadar basit olabilir. İlham almak için örneklere göz atın.
Sonuç
Buraya kadar okuduğunuz için teşekkür ederiz. Umarız kılavuzumuzu beğenmişsinizdir ve artık kullanıcılarınızı güvende tutmak için kendi korumalı alanlarınızı oluşturabileceğinizi düşünüyorsunuzdur.
Korumalı alan ve politika oluşturmak zor bir iştir ve fark edilmesi zor hatalara açıktır. Güvenli tarafta kalmak için bir güvenlik uzmanının politikanızı ve kodunuzu incelemesini öneririz.