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 kılavuz olarak kullanın.

1. Korumalı alan yürütücü yöntemi seçme

Korumalı alan oluşturma, korumalı alanın çalıştırılmasından sorumlu bir yürütücüyle (korumalı alan yürütücüsü) başlar. executor.h üst dosya, 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 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ı alana alınacağını söyleme

Bu yöntem, başlatma sırasında korumalı alana alınmama ve ardından ::sandbox2::Client::SandboxMeHere() yöntemini çağırarak korumalı alana ne zaman girileceğini seçme 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öntem'de belirtilen kodun aynısını kullanıyoruz. Ancak programın başlatma sırasında korumalı alan dışında çalışması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ö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: İkili program hazırlayın, fork isteklerini bekleyin ve korumalı alanı kendiniz oluşturun.

Bu mod, bir ikili programı başlatmanıza, korumalı alana almaya hazırlamanıza ve ikili programınızın yaşam döngüsünün belirli bir anında yürütücü tarafından kullanılabilir hale getirmenize olanak tanır.

Yürütücü, ikili dosyanıza bir çatallama isteği gönderir. Bu istek fork() (::sandbox2::ForkingClient::EnterForkLoop() 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 birkaç özel 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'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ı ile 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. 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ının listesini aldıktan sonra politikayı tanımlamak için PolicyBuilder'ı kullanabilirsiniz.

PolicyBuilder, birçok yaygın işlemin (birçoğu zaten DefaultPolicyBuilder()'de yer alır) gerçekleştirilmesine olanak tanıyan birçok kolaylık ve yardımcı işlevle birlikte gelir. Aşağıdaki liste, mevcut işlevlerin yalnızca küçük bir alıntısıdır:

  • İş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 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üçlendirmeniz gerekiyorsa ve yalnızca belirli bağımsız değişkenlere sahip bir sistem çağrısına izin verdiğiniz 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 tekrarlayan BPF kodları yazıyorsanız özellik isteğinde bulunabilirsiniz.

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. Korumalı alana geçici bir dosya depolama alanı eklemek için AddTmpfs() yardımcısı kullanılabilir.

Özellikle faydalı 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 bu politikayı yaygın 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 ve programın hataları düzgün şekilde işlediği bir ihlalle karşılaşırsanız BlockSyscallWithErrno() ile bunun yerine hata döndürmesini sağlayabilirsiniz.

Aşağıdaki örnekte, write ve mprotect bağımsız değişkenlerini kısıtlamak ve openat'yi engellemek (aksi takdirde DefaultPolicyBuilder() buna izin verir) için doğrudan sandbox2::PolicyBuilder oluşturulur:

#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 Kullanıcısı'nın belirli sistem çağrılarını yapmasını engeller ve böylece saldırı yüzeyini azaltır. Ancak saldırgan, 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 bir şekilde yürütülmesinde sorunlara neden oluyorsa yürütücü nesnesinde limits() yöntemini çağırarak 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. Mevcut tüm seçenekler limits.h üstbilgi 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 dair bir örnek için araca göz atı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 örneklendirilmesi 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

Ayrıca, sonuç alınana kadar engelleme yapmamak için korumalı alanı eşzamansız olarak da çalıştırabilirsiniz. Bu, örneğin, korumalı alanla iletişim kurarken yararlıdır. Aşağıdaki kod snippet'i bu kullanım alanını göstermektedir. Daha ayrıntılı örnekler için crc4 ve tool sayfalarını inceleyin.

#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 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 (comms.h üst dosyasına bakın) tam sayı, dize, bayt arabelleği, protobuf veya dosya tanımlayıcı göndermek için kullanılabilir.

Dosya Tanımlayıcıları Paylaşma

İşlemler Arası İletişim API'sini (bkz. ipc.h) kullanarak MapFd() veya ReceiveFd() kullanabilirsiniz:

  • Yürütücüden korumalı alana dosya tanımlayıcıları 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çinReceiveFd() 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 önce 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. Comms API'nin örnek kullanımını 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 bir alıntıdır. Yürütücü, denetim toplamını 32 bitlik işaretsiz bir tam sayı olarak alır: uint32_t crc4;

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

Arabelleklerle veri 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 iletilecek verilere göre veya doğrudan bir dosya tanımlayıcıdan bir arabellek oluşturur ve yürütücüde comms->SendFD(), korumalı alanda ise comms->RecvFD() kullanarak korumalı alana iletir.

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 verileri bir arabellek aracılığıyla korumalı alanla 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

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ı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 işlem, korumalı alan politikasının doğru olmasını da sağlar.

Bir 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 makaleyi 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üvende kalmak için bir güvenlik uzmanının politikanızı ve kodunuzu incelemesini öneririz.