Korumalı Alan2'yi Kullanmaya Başlama

Bu sayfada, Sandbox2 ile kendi korumalı alan ortamınızı nasıl oluşturacağınızı öğreneceksiniz. Sandbox Politikası'nı nasıl tanımlayacağınızı ve bazı ileri düzey ancak yaygın olarak kullanılan ince ayarları öğreneceksiniz. Üstbilgi dosyalarındaki örnekler ve kod dokümanlarının yanı sıra buradaki bilgileri de rehber olarak kullanın.

1. Bir Sandbox Executor yöntemi seçin

Özel korumalı alan, Sandbox Executor'ı (Sandboxee'yi çalıştırmaktan sorumlu) kullanarak 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, aralarından seçim yapabileceğ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 programı yürütün

Bu, sanal alan oluşturmayı kullanmanın en basit yoludur ve kaynak kodu olmayan bir ikili dosyanın tamamını sanal alan oluşturmak istediğinizde önerilen yöntemdir. Ayrıca, olumsuz etkileri olabilecek korumalı alan dışı başlatma işlemi olmadığından korumalı alanı kullanmanın en güvenli yoludur.

Aşağıdaki kod snippet'inde, korumalı alan oluşturulacak ikilinin yolu ve bir execve sistem çağrısına iletmemiz gereken bağımsız değişkenler tanımlanmaktadır. executor.h başlık 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 snippet'imizin başka bir bağımsız değişken tanımlamadığını unutmayın.

Bu yürütme 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ı alana alınacağını söyleme

Bu yöntem, başlatma sırasında korumalı alan dışında kalma ve ardından ::sandbox2::Client::SandboxMeHere() çağrısı yaparak korumalı alana girme zamanını seçme esnekliği sunar. Bu API'yi kullanmak için, sanal alan oluşturmaya ne zaman başlamak istediğinizi kodda tanımlayabilmeniz gerekir. Ayrıca, tek iş parçacıklı olmalıdır (nedenini SSS bölümünde okuyabilirsiniz).

Aşağıdaki kod snippet'inde, yukarıdaki 1. Yöntem'de belirtilen kodun aynısı kullanılmıştır. Ancak programın başlatma sırasında korumalı alan dışında yürütülmesine 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ü, Sandboxee tarafından bilgilendirilene kadar devre dışı bırakılmış bir sanal alana sahip olduğundan ::sandbox2::Client örneği oluşturmamız, yürütücü ile Sandboxee arasında iletişimi ayarlamamız ve ardından sandbox2_client.SandboxMeHere()'yi çağırarak başlatma işleminin tamamlandığını ve artık sanal alan oluşturmaya başlamak istediğimizi yürütücüye 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önteminin bir örneği crc4'tür. Burada crc4bin.cc, Sandboxee'dir ve yürütücüye (crc4sandbox.cc) ne zaman sanal alana girmesi gerektiğini bildirir.

3. yöntem: Özel Forkserver – Bir ikili dosya hazırlayın, fork isteklerini bekleyin ve kendi sandbox'ınızda çalışın

Bu mod, bir ikiliyi başlatmanıza, onu korumalı alan için 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 dosyanıza bir çatallama isteği gönderir. Bu istek fork() (::sandbox2::ForkingClient::WaitAndFork() aracılığıyla) olur. Yeni oluşturulan işlem, ::sandbox2::Client::SandboxMeHere() ile korumalı alan oluşturmaya 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 belirli birkaç durumda (ör. sıkı bellek gereksinimleriniz olduğunda) geçerli olduğunu unutmayın. COW'dan yararlanabilirsiniz ancak gerçek ASLR olmaması dezavantajdır. Diğer bir tipik kullanım örneği, Sandboxee'nin güvenilmeyen veriler işlenmeden önce çalıştırılabilen 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 konusuna 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ı ile korunur.

Sandbox Policy ile amaç, Sandboxee'nin yapabileceği sistem çağrılarını ve bağımsız değişkenleri, ayrıca erişebileceği dosyaları kısıtlamaktır. Koruma alanına almayı planladığınız kodun gerektirdiği sistem çağrıları hakkında ayrıntılı bilgiye sahip olmanı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şlemin yapılmasına olanak tanıyan birçok kolaylık ve yardımcı işlevle birlikte gelir. Aşağıdaki liste, kullanılabilen işlevlerin yalnızca küçük bir bölümünü içermektedir:

  • İşlem başlatma için herhangi bir syscall'ı izin verilenler listesine ekleme:
    • 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 yöntemin avantajı, belirli sistem çağrılarının kullanılamadığı farklı mimarilerde (ör.ARM64'te OPEN sistem çağrısı yoktur) aynı politikanın kullanılabilmesidir. Ancak bu yöntemin, gerekenden daha fazla sistem çağrısının etkinleştirilmesi gibi küçük bir güvenlik riski vardır. Örneğin, AllowOpen(), Sandboxee'nin açıkla ilgili herhangi bir syscall'i çağırmasına olanak tanır. Yalnızca belirli bir syscall'ı izin verilenler listesine eklemek istiyorsanız AllowSyscall(); kullanabilirsiniz. Birden fazla syscall'ı aynı anda izin verilenler listesine eklemek için AllowSyscalls() kullanabilirsiniz.

Şu ana kadar politika yalnızca syscall tanımlayıcısını kontrol etmektedir. 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, bağımsız değişken olarak yalnızca syscall kimliğini değil, aynı zamanda Linux çekirdeğindeki bpf yardımcı makrolarını kullanan ham bir seccomp-bpf filtresini de alır. BPF hakkında daha fazla bilgi için çekirdek belgelerine bakın. Kullanılabilirlik sarmalayıcısı olması gerektiğini düşündüğünüz tekrar eden BPF kodu yazdığınızı fark ederseniz özellik isteği gönderebilirsiniz.

PolicyBuilder, syscall ile ilgili işlevlerin yanı sıra bir dosyayı/dizini korumalı alana bağlamak için AddFile() veya AddDirectory() gibi dosya sistemiyle ilgili bir dizi işlev de sağlar. AddTmpfs() yardımcı programı, sanal ortamda geçici dosya depolama alanı eklemek için kullanılabilir.

Özellikle yararlı bir işlev olan AddLibrariesForBinary(), ikili dosya için gereken kitaplıkları ve bağlayıcıyı ekler.

İ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 bunu ortak bir iş yüküyle çalıştırın. İhlal tetiklenirse syscall'ı 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 şekilde işliyorsa BlockSyscallWithErrno() ile hata döndürmesini sağlayabilirsiniz.

#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

Sandbox politikası, Sandboxee'nin belirli syscall'leri ç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() işlevini çağırarak sandbox2::Limits sınıfını kullanarak bunları ayarlayabilirsiniz.

Aşağıdaki kod snippet'inde bazı örnek sınır ayarlamaları gösterilmektedir. Kullanılabilir tüm seçenekler limits.h başlık 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 aracın örneğine 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

Koruma 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 (statik) konusuna 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

Ayrıca, sonuç alınana kadar engelleme yapmamak için korumalı alanı eşzamansız olarak da çalıştırabilirsiniz. Bu, örneğin Sandboxee ile iletişim kurarken yararlıdır. Aşağıdaki kod snippet'inde bu kullanım alanı gösterilmektedir. 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. Sandboxee ile iletişim kurma

Varsayılan olarak, yürütücü dosya tanımlayıcıları aracılığıyla Sandboxee ile iletişim kurabilir. Örneğin, yalnızca bir dosyayı Sandboxee ile paylaşmak veya Sandboxee'nin standart çıkışını okumak istiyorsanız bu yeterli olabilir.

Ancak, yürütücü ile Sandboxee arasında daha karmaşık bir iletişim mantığına ihtiyacınız olabilir. İ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

Süreçler Arası İletişim API'sini (bkz. ipc.h) kullanarak MapFd() veya ReceiveFd()'yi 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);
    
  • Socketpair uç noktası oluşturmak için ReceiveFd() öğesini kullanın. Bu, Sandboxee'nin standart çıkışını veya standart hatalarını okumak için kullanılabilir. Örnek kullanımı 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 önce Sandbox2 nesnesinden iletişim nesnesini almanız gerekir:

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

İletişim nesnesi kullanılabilir hale geldiğinde, Send* işlev ailesinden biri kullanılarak Sandboxee'ye veri gönderilebilir. İletişim API'sinin örnek kullanımını crc4 örneğinde bulabilirsiniz. Aşağıdaki kod snippet'inde bu örnekten bir alıntı gösterilmektedir. Vasiyet tenfiz memuru, SendBytes(buf, size) ile birlikte unsigned char buf[size] gönderir:

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

Sandboxee'den 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, 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 iletir.

Aşağıdaki kod snippet'inde uygulayıcının tarafını görebilirsiniz. Korumalı alan, eşzamansız olarak çalışır ve 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());

Sandboxee tarafında da bir arabellek nesnesi oluşturmanız ve yürütücü tarafından gönderilen dosya tanımlayıcısından 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

Korumalı alan 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'inde bu senaryo gösterilmektedir:

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

Eşzamansız olarak çalışan bir korumalı alandan çıkma

Sandbox eşzamansız olarak çalışıyorsa sonlandırma için iki seçenek vardır. İlk olarak, Sandboxee'nin tamamlanmasını bekleyip son 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üre zarfında başka bir nedenden dolayı sonlanabileceğ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 kodlar 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ı alana alınmış programın korumalı alan ihlalleri gibi sorunlar olmadan çalışıp çalışmadığını kontrol etmek için tasarlanmıştır. Bu işlem, korumalı alan politikasının doğru olmasını da sağlar.

Korumalı alan 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ç

Bu kılavuzu okuduğunuz için teşekkür ederiz. Kılavuzumuzu beğendiğinizi ve artık kullanıcılarınızın güvenliğini sağlamak için kendi sanal alanlarınızı oluşturmaya hazır olduğunuzu umuyoruz.

Koruma alanları ve politikalar oluşturmak zor bir görevdir ve küçük hatalara yol açabilir. Güvenli tarafta kalmak için bir güvenlik uzmanının politikanızı ve kodunuzu incelemesini öneririz.