Modern işletim sistemlerinde kullanıcı uygulamaları donanıma doğrudan erişim yetkilerine sahip değildir. Bu sınırlama, sistemin güvenliği ve kararlılığı açısından zorunludur. Uygulama düzeyindeki yazılımlar, işlemci donanımına, bellek yönetimine, dosya sistemine, ağ kaynaklarına ve diğer kritik hizmetlere erişebilmek için işletim sisteminin çekirdeğinde yer alan API’lere, yani sistem çağrılarına (sytemcall veya syscall) başvururlar. Syscall, User-Mod işlemin, Kernel-Mod işletim sistemi servisinden kontrollü bir şekilde hizmet almasını sağlayan kontrollü geçiş mekanizmasıdır. Bu geçiş, genellikle kullanıcı modu ile çekirdek modu arasında CPU'nun işlem ayrıcalık seviyelerinin (ring seviyeleri) değiştirilmesiyle gerçekleşir.
İşlemciler, ring-0 (Kernel mode) ve ring-3 (user-mode) arasında geçişleri sağlamak için özel mekanizmalar sunar. Örneğin x86 mimarisinde INT 0x80 interrupt’ı, eski Linux sistemlerinde syscall tetiklemek için kullanılmıştır. Daha sonra sysenter, sysexit, syscall, sysret gibi daha hızlı geçişleri mümkün kılan talimatlar geliştirilmiştir. Bu talimatlar, işlemcinin kontrolünü kullanıcı modundan çekirdek moduna geçirir, burada işletim sistemi ilgili servis rutinini çalıştırır, ardından sonucu kullanıcı moduna döner.
Bu genel mimari farklı işletim sistemlerinde farklı biçimlerde uygulanır. Unix-benzeri sistemler, POSIX standartlarına uygun bir şekilde bu sistem çağrılarını expose ederken, Windows gibi NT mimarisi üzerine kurulu sistemler bu mekanizmayı daha farklı, kapalı ve katmanlı biçimde uygular.
Windows işletim sistemi, sistem çağrısı mimarisini NT mimarisi etrafında inşa etmiştir. Bu mimari, kullanıcı modu ve çekirdek modu arasında katı bir ayrım gözetir. Windows'taki kullanıcı modunda çalışan uygulamalar, doğrudan kernel modundaki işlevleri çağırmak yerine, ilk olarak Windows API (Win32 API) üzerinden çağrılar yapar. Böylece uygulamalar doğrudan syscall mekanizmasını kullanmak yerine Windows’un sağladığı API’leri kullanarak soyutlama sağlanmış olur; Kernel seviyesinde yapılacak olan değişiklikler, uygulama katmanındaki API’leri etkilemez. Win32 API, genellikle kernel32.dll, user32.dll, advapi32.dll gibi dinamik bağlantı kitaplıklarında tanımlıdır. Bu katman, uygulamaların NT çekirdeğine doğrudan bağlanmasını engeller ve sistemin taşınabilirliğini, güvenliğini ve esnekliğini artırır.
Ancak bu Win32 API’ler, aslında NT içindeki asıl sistem servislerine bağlanmak için bir köprü görevi görür. Bu köprü, Native API veya NT API olarak adlandırılan bir alt seviyedeki API katmanıdır ve ntdll.dll kütüphanesinde tanımlıdır. ntdll.dll, user mode'da çalışır ve native syscall interface'e erişimi doğrudan sağlar. Örneğin, CreateFile çağrısı aslında kernel32.dll içinde tanımlıdır, ancak içsel olarak NtCreateFile fonksiyonunu çağırır ve bu fonksiyon ntdll.dll'de tanımlanmış bir Native API fonksiyonudur.
ntdll.dll içerisinde tanımlı Nt* veya Zw* fonksiyonları, sistem çağrılarına geçiş yapan gerçek noktadır. Bu fonksiyonlar, işlemci mimarisine uygun olarak sistem çağrısını tetikleyen assembly düzeyinde talimatlar içerir. x86/x64 mimarisinde bu genellikle syscall veya sysenter talimatlarıdır. Bu talimatlar tetiklendiğinde işlemci, kontrolü kullanıcı modundan kernel mode’a geçirir.
Sistem çağrısının tetiklenmesiyle birlikte işlemci, MSR (Model-Specific Register) kayıtlarında tanımlı olan syscall handler adresine sıçrar. Bu handler, Windows çekirdeğinde (NTOSKRNL.EXE) tanımlı olan KiSystemCall64 veya KiSystemService gibi bir giriş noktasıdır. Burada CPU bağlamı kaydedilir, işlemci durumu değiştirilir, güvenlik kontrolleri yapılır ve ardından sistem servis numarasına göre KeServiceDescriptorTable adlı bir tablodan fonksiyon işaretçisi çekilerek ilgili kernel mod fonksiyonu çağrılır.
Her sistem çağrısı, NTOSKRNL içinde tanımlı olan servis tablosunda bir indeks numarasına sahiptir. Bu sistem çağrısı numaraları sabit değildir; Windows sürümüne göre değişebilir ve Microsoft tarafından dokümante edilmez. Bu nedenle bu çağrılara doğrudan erişim ya tersine mühendislik ya da dinamik analiz ile mümkündür. Bu tasarım aynı zamanda Windows’un iç mekanizmalarını dış tehditlere karşı soyutlamak için kullanılır.
Windows’ta Zw* ve Nt* fonksiyonları birbirine çok benzer, ancak farkları çağrıldıkları bağlama göre anlam kazanır. Nt* fonksiyonları doğrudan kullanıcı modundan çağrıldığında, sistem çağrısı yapılır; ancak çekirdek modunda çağrılırsa kullanıcı moduna geçmeden doğrudan çekirdek fonksiyonu çağrılır. Zw* fonksiyonları ise her zaman sistem çağrısını tetikleyecek şekilde çalışır. Bu ayrım, kernel-mode kodlar yazılırken önem taşır.
Sistem çağrısı parametreleri, RCX, RDX, R8, R9 ve yığıt (stack) üzerinden gönderilir. Windows çekirdeği, bu parametreleri kontrol eder, geçerliliğini sınar ve gerektiğinde SEH (Structured Exception Handling) mekanizmasıyla koruma sağlar. Sistem çağrısından dönüşte, sonuç değeri RAX register’ına konularak kullanıcı moduna döndürülür.
Windows sistem çağrılarında kullanılan SSDT (System Service Dispatch Table), kernel içindeki ana tablo olup, sistem çağrısı numarası bu tabloya indeks olarak kullanılır. SSDT, çekirdek bellek alanındadır ve koruma altındadır; ancak kernel modunda çalışan rootkit’ler bu tabloyu patch ederek hook mekanizmaları kurabilir. Bu nedenle modern Windows sürümleri PatchGuard (Kernel Patch Protection) ve HVCI gibi güvenlik teknolojileriyle SSDT üzerinde oynanmasına karşı koruma sağlar.
Windows 10 ve sonrası sürümlerde, sistem çağrısı mekanizması daha da gizlenmiştir. Direct syscall yapmak isteyen araştırmacılar genellikle ntdll.dll’yi analiz ederek syscall numaralarını bulmak zorundadır. Bu numaralar build’e göre değiştiğinden, sabit değildir. Bu, hem güvenliği artırır hem de tersine mühendisliği zorlaştırır.
Linux'ta sistem çağrıları çok daha şeffaf ve sabit bir yapıya sahiptir. Her sistem çağrısı sabit bir syscall numarasına sahiptir ve bu numaralar unistd.h gibi başlık dosyalarında açık şekilde tanımlanmıştır. Kullanıcı modu uygulamaları genellikle glibc gibi C kütüphanelerini kullanarak bu çağrılara erişir. glibc, syscall() fonksiyonunu kullanarak doğrudan syscall talimatı ile çekirdek moduna geçiş sağlar. Bu geçiş noktaları açık kaynak olduğundan incelenmesi ve kullanılması kolaydır. Kernel versiyonuna göre syscall listesi güncellenebilir ancak genellikle geriye dönük uyumluluk korunur.
Buna karşın Windows’ta sistem çağrıları belgelenmemiştir ve sabit değildir. Bu nedenle kernel seviyesinde çalışan yazılımlar geliştirirken veya tersine mühendislik yaparken Linux daha kolay analiz edilebilirken, Windows daha kapalı ve katmanlı bir yapıya sahiptir. Ancak bu kapalı yapı, güvenlik açısından bazı avantajlar sağlayabilir; örneğin Windows, SSDT’yi ve diğer kritik yapılara doğrudan erişimi engelleyen çeşitli güvenlik korumaları ile donatılmıştır.
Basit bir örnek üzerinden açıklayalım: CreateFileA, kullanıcıların bir dosya oluşturmak veya mevcut bir dosyayı açmak için kullandığı bir Windows API fonksiyonudur. Kullanıcı bu fonksiyonu çağırdığında, arka planda işletim sistemi devreye girer ve gerekli işlemleri yaparak sabit diskte yeni bir dosya oluşturur veya var olan dosyaya erişim sağlar. Bu süreçte, kullanıcı bu işlemlerin nasıl gerçekleştiğini görmez; çünkü sistem çağrısı mekanizması, donanım ile doğrudan etkileşimi işletim sisteminin kontrolü altında ve tamamen soyutlanmış bir şekilde gerçekleştirir.Şimdi daha derine inip, programı çalıştıralım ve arka planda neler olduğunu görelim.
Programı derleyip bir debugger aracılığıyla çalıştırdıktan sonra, CreateFile fonksiyonuna breakpoint koyarak programın bu fonksiyonu çağırdığı anda yürütmenin durmasını sağladık.
Fonksiyonun içerisine baktığımızda ilk olarak stack frame oluşuyor. Ardından kernelbase ile bir validasyon ya da güvenlik kontrolü yapan bir alt fonksiyonu çağırıyor. TEST ve JE komutlarıyla bu fonksiyonun dönüş değeri kontrol ediliyor: eğer başarısız olursa 76CBEA04 adresine (hata yönlendirmesi gibi) atlanıyor.
Sonraki adımda CreateFileW fonksiyonu çağrılıyor. CreateFileA, ANSI (8-bit karakter) API'dir. Ama aslında asıl işi CreateFileW yapar, bu Unicode versiyonudur. Windows, çoğu API'de A versiyonunu çağıran programları W versiyona yönlendirir. Burada da bunu canlı olarak görüyoruz.
CreateFileW içerisine girdikten sonra da çeşitli validasyonlar yapılır ve bizi NtCreateFile fonksiyonuna yönlendirir. Bu fonksiyon ntdll.dll içerisindeki fonksiyonlardan biridir. Bu fonksiyon, aslında sistem çekirdeğinde tanımlı olan NtCreateFile adlı çekirdek fonksiyonun kullanıcı modundaki kapısıdır.
Fonksiyonun içerisini incelediğimizde syscall numarasının 55 olduğunu görmekteyiz. Fonksiyonun ilk satırında, EAX register’ına 0x55 değeri yüklenir. Bu değer, NtCreateFile fonksiyonu için tanımlanmış sistem çağrısı numarasıdır. Windows çekirdeğinde her sistem çağrısının bir numarası bulunur ve bu numara genellikle EAX register’ına yazılarak kernel’e hangi işlem yapılmak istendiği bildirilir. Bu örnekte 0x55, doğrudan NtCreateFile fonksiyonuna karşılık gelir.
Bir sonraki adımda, EDX register’ına Wow64SystemServiceCall fonksiyonunun adresi atanır. Bu fonksiyon, 32-bit uygulamaların 64-bit Windows çekirdeğiyle iletişim kurmasını sağlayan özel bir sistem servis çağrısı mekanizmasıdır. Wow64 (Windows-on-Windows64) altyapısı, 32-bit uygulamaların 64-bit sistemlerde çalışmasına olanak tanır. Burada EDX, sistem çağrısını gerçekleştirecek olan fonksiyonun adresini içerir.
Devam eden satırda call edx komutu ile, az önce EDX'e atanan sistem çağrısı fonksiyonu çalıştırılır. Bu çağrı gerçekleştiğinde, daha önce EAX'e yüklenmiş olan sistem çağrı numarası kullanılarak kernel modundaki karşılık gelen servis fonksiyonu tetiklenir. Bu işlem, kullanıcı modundan kernel moduna geçişin fiilen yapıldığı noktadır. Böylece NtCreateFile fonksiyonu, sistem düzeyinde dosya oluşturma işlemini başlatır.
Sisteme yapılan çağrı tamamlandıktan sonra fonksiyon, ret 2Ch komutu ile sonlanır. Bu komut, stack’ten 0x2C (yani 44) byte temizleyerek dönüş yapar. Bu, NtCreateFile fonksiyonunun toplamda 44 byte uzunluğunda parametreler aldığını ve bu parametrelerin geri dönüşte temizlenmesi gerektiğini gösterir. Stack temizliği, çağıran fonksiyonun sorumluluğundadır ve bu durum, çağrı protokolünün bir parçası olarak uygulanır.
NtCreateFile'a kadar olan kısmı kullanıcı modunda, yani programın belleğinde gözle görebiliyoruz. Ancak NtCreateFile'dan sonra kernel moduna geçiliyor ve bu geçişten itibaren olanlar senin programının içinde görünmez, çünkü artık işletim sistemi devreye girer. Bu, çok kritik bir eşiktir çünkü artık uygulama kodu çalışmayı bırakır ve işlemci donanım düzeyinde yüksek ayrıcalıklı (ring 0) moda geçerek işletim sistemi kontrolünü ele alır.
Bu geçişin teknik altyapısı işlemciye gömülü özel kayıtlar (Model Specific Registers - MSRs) sayesinde gerçekleştirilir. LSTAR adlı MSR (msr[0xC0000082]), sistem çağrısı sonrası çekirdekte çalıştırılacak ilk talimatın adresini tutar. Yani syscall komutu çalıştırıldığında işlemcinin RIP (Instruction Pointer) register’ına LSTAR'daki adres atanır. Windows sistemlerinde bu adres genellikle KiSystemCall64 fonksiyonuna işaret eder. Bu fonksiyon, sistem çağrısını kernel modunda karşılayan ilk noktadır.
Rdmsr komutunu kullanarak LSTAR içerisindeki adresi görüyoruz. Bu register, kernel moda geçiş sonrası yürütülecek ilk adresi barındırır. Windows sistemlerinde bu adres, tipik olarak ntoskrnl.exe içindeki KiSystemCall64 veya KiSystemCall64Shadow fonksiyonudur. LSTAR register’ındaki adres, sistem başlatıldığında (boot time) işletim sistemi tarafından ayarlanır. Bu adres, KiSystemCall64 fonksiyonuna işaret eder. Bu fonksiyon, CPU’nun kernel moduna geçtiği ilk noktadır. Artık kod, kullanıcı uygulamasının kontrolünden çıkmış ve Windows çekirdeğinin kontrolüne girmiştir.
KiSystemCall64 fonksiyonunun ilk işi, çağrının kullanıcı modundan geldiğini doğrulamak, bazı güvenlik kontrolleri yapmak (örneğin KVA Shadowing – Meltdown mitigasyonu), işlem bağlamını doğru yapılandırmak ve sistem çağrısı numarasına göre ilgili çekirdek fonksiyonunu çağırmaktır.
Burada işlemci içindeki RAX register’ı hayati önem taşır. Çünkü kullanıcı modu tarafından RAX’e yüklenmiş olan sistem çağrısı numarası (örneğin 0x55, ki bu NtCreateFile içindir), KiSystemCall64 tarafından alınır ve bu numara yardımıyla sistem servis tablosu (SSDT) üzerinde bir arama yapılır.
SSDT, yani System Service Descriptor Table, tüm kernel sistem çağrılarının sıralı olarak tutulduğu bir tablodur. Her giriş, çekirdekteki bir sistem fonksiyonunun adresine işaret eder. KiSystemCall64, sistem çağrısı numarasını (örneğin 0x55) bir indeks olarak kullanarak bu tabloda SSDT[0x55] değerine ulaşır. Bu da nt!NtCreateFile fonksiyonunun adresidir.
Bu noktada artık Windows, kullanıcıdan gelen CreateFileA çağrısını gerçek kernel fonksiyonu olan NtCreateFile’a yönlendirmiş olur. NtCreateFile fonksiyonu, IOManager, SecuritySubsystem, ObjectManager gibi çekirdek bileşenlerle birlikte çalışarak dosya oluşturma ya da açma işlemini başlatır.
Sadece KiSystemCall64 fonksiyonu değil, bu sürece eşlik eden bir dizi düşük seviyeli destek fonksiyonu da vardır. Özellikle bazı sistemlerde ya da konfigürasyonlara göre aşağıdaki dizilim çalışır:
- KiSystemServiceUser: Kullanıcı modundan gelen çağrının parametrelerinin doğruluğunu ve hizalamasını kontrol eder. Bu, hatalı parametrelerin kernel moduna taşınmasını engeller.
- KiSystemServiceStart: Kernel çağrısını başlatmak için uygun hazırlığı yapar, sistem çağrısı numarasını çözümler.
- KiSystemServiceRepeat: Bazı sistem çağrılarının tekrar yürütülmesi gerektiğinde devreye girer (örneğin zamanlama ya da kilitlenmiş kaynak nedeniyle).
- KiSystemServiceCopyStart / CopyEnd: Kullanıcı modundan gelen verilerin kernel moduna güvenli şekilde taşınmasını sağlar.
Bu yapı, Microsoft’un yıllar içinde güvenlik ve kararlılık amacıyla inşa ettiği karmaşık bir mimaridir.
İşlem tamamlandıktan sonra NtCreateFile fonksiyonu dönüş yapar ve kontrol tekrar KiSystemCall64’e geçer. Burada çağrının sonucu (örneğin bir NTSTATUS değeri) RAX içine yazılır. Ardından CPU’nun sysret talimatı çalıştırılır.
Bu talimat, işlemciyi tekrar ring 3'e, yani kullanıcı moduna döndürür. Aynı zamanda programın kaldığı noktadan devam etmesini sağlar. sysret, daha önce syscall sırasında yedeklenmiş olan RIP, CS, RSP, SS gibi register’ları geri yükler. Artık programın akışı tekrar yazdığımız main() fonksiyonuna döner ve CreateFileA fonksiyonu çalışmasını tamamlamış olur.