• 01 TEMMUZ 2020
  • Okunma Sayısı: 251

Fatih ŞENSOY - Karaelmas Malware Hunter Team

iletisim@fatihsensoy.com

Sizlerle birlikte bu yazımızda Windows Internals'ın temel yapı taşları olan user space, user mode, kernel space, kernel mode, virtual memory gibi kavramlara değineceğiz. 

Virtual Memory Bir programı koştuğumuzda (çift tıkladığımızda) o programa ait bir process bellek üzerine oluşturulur. Ve her process’in kendine özgü bir private space’i oluşur (process memory olarak adlandırılır). Process memory, virtual memory’nin bir parçasıdır. Virtual memory aslında gerçekte var olan bir memory alanı değildir, sadece işletim sisteminin memory manager’ı tarafından oluşturulmuş hayali bir alandır.

Peki neden gereklidir?

32-bit mimarisini ele alacak olursak, physical memory’de maksimum 4 GB bellek alanı vardır. Bu 4 GB bellek alanının 2 GB kadar bir boyutu işletim sistemi kernel space tarafından, diğer 2 GB’ı ise user space tarafından kullanılmaktadır. Teknik bakımdan da adresleyecek olursak, user space’in kullandığı aralık 0x00000000 ila 0x7FFFFFFF arası, kernel space’in kullandığı aralık ise 0x80000000 ila 0xFFFFFFFF arasındadır. Oluşan her process kendine özgü bir process memory’sini var olarak düşündüğünde physical memory buna yetmeyecektir. Bunun için virtual memory kullanılmaktadır. Ve virtual memory’de yer alan her process adresi, physical memory’de bir değere eşlenmektedir. Virtual memory, physical memory olmadan işe yaramamaktadır. İşletim sisteminin memory manager’ı physical memory’de var olan bu sorunu virtual memory sayesinde çözerek, belleğin bir kısmını diske yazarak sorunu çözüme kavuşturmaktadır.

Her process’in virtual memory’de kendine has bir process memory olmasına rağmen genellikle her process, kernel space’i ortak olarak kullanmaktadır. Görselde de kernel space ve user space için adreslemelerden de anlaşılacağı üzere 32-bit bir sistemde kernel space ve user space arasında 64 KB’lık bir alanın boş olduğu görülmektedir. Bu alana erişim sağlanamaz. Bu alanın var olmasının sebebi ise kernel space’in yanlışlıkla sınırı geçip user space’i bozmasını engellemektir. X86 mimarisinin bellek alanından bahsetmiştik. Mimariye göre bellek alanı tabii olarak değişkenlik göstermektedir. Örnek olarak x64 mimarisini ele alalım. X64 mimarisinde daha fazla bellek alanı olacağından dolayı daha fazla adres de olacaktır. X64 mimarisinde user space 0x0000000000000000 ila 0x000007FFFFFFFFFF arasını, kernel space ise 0xFFFF080000000000 ila 0xFFFFFFFFFFFFFFFF arasını kaplamaktadır. Memory’deki space’ler arttığından ötürü x86 mimarisinde kernel space ve user space arasında var olan 64 KB’lık boş alan burada daha fazla olmaktadır.

Process Memory Bileşenleri

Her process’in kendine ait bir private space’i olduğunu ve bu private space’in process memory olarak adlandırıldığından bahsetmiştik. Ve process memory’nin de virtual memory içerisinde yer aldığını belirtmiştik. Şimdi ise process memory içerisinde yer alan bileşenlere göz atacağız. Process memory’nin de user space’de yer aldığını ve sınırlı yetkilerde olduğunu unutmayalım.

DLLs: Bir process oluşturulduğunda, o process ile ilişkili tüm DLL’ler process memory’e yüklenir.

PEB: Bu bölge PEB structure’ı taradından sağlanmakta olup, executable’ın nereye yüklendiği, bellektedi DLL’lerin nerede bulunabileceği gibi bilgileri depolar.

Process Heap: Her process’in bir heap’i vardır ve gerekli olursa bu sayı artırılabilir. Process’in aldığı dinamik girdileri belirtir.

Thread Stack: Her thread’in bellekte kendine özel bir Thread Stack alanı vardır. Bu alanda yerel değişkenler, bağımsız değişkenler ve return adresleri bulunur.

Process Environment Variables: Belleğin bu alanı process’in temp directory, home directory, Appdata directory gibi ortam değişkenlerini depolar.

Process Executable: Executable’a çift tıklandığında bir process oluşturulur ve bu process, process memory’de bu alana yüklenir.

Process Memory bileşenleri yukarıda görüldüğü gibidir. Şimdi ise her process’in ortak olarak kullandığı Kernel Memory’e bir göz atalım.

Kernel Memory Bileşenleri

Kernel memory, kernel space’de bulunur ve önceden de bahsettiğim üzere tüm process’ler bu alanı ortak olarak kullanır. User space’deki hiçbir uygulama bu alana direk olarak erişim sağlayamaz.

Görselden de anlaşılacağı üzere Kernel memory’nin bazı içerikleri mevcuttur.

Hal.dll: Hardware Abstraction Layer’ın kısaltması olan HAL, yüklenebilir kernel modülü olan hal.dll’e implement edilmiştir. HAL, işletim sistemini donanımdan soyutlamakta, izole etmektedir. Öncelikli olarak Windows Executive, kernel ve kernel modda çalışan cihaz driver’larına hizmet sağlar. Kernel mod aygıt driver’ları donanım ile doğrudan iletişim yerine hal.dll üzerinden donanımla iletişim sağlamaktadır. Kısacası HAL, bir soyutlayıcıdır. Donanıma erişim için bir köprüdür.

Ntoskrnl.exe: Windows işletim sisteminin kernel imajı olarak bilinir. Kernel ev Executive(yürütüm/yürütme) olmak üzere iki kısıma ayrılır. Executive kısmı, kontrollü bir mekanizma sayesinde user mode uygulamalar tarafından çağrılabilen system service routines adındaki işlemleri uygular. Kernel kısmı low level işletim sistemi hizmetlerini uygular.

Win32k.sys: Kernel mode driver, monitör gibi cihazlara grafik output’u oluşturmak için UI ve GDI(Graphics Device Interface) hizmetlerini uygular.

Kernel space’de yer alan kernel memory’nin bileşenlerini de kısaca bu şekilde özetleyebiliriz.

Malware’lar da bir uygulama olduğu için ve her uygulama da user space’de çalıştığına göre nasıl oluyor da bir zararlı uygulama diskteki bir bölüme yazma işlemi yapabiliyor? Bunun cevabınu User Mode ve Kernel Mode’u karşılaştırıp verebiliriz.

Kernel Mode & User Mode

Bir uygulamayı çalıştırdığımızda bu uygulamanın belleğe yüklendiğini söylemiştik. Bellekte user space’e yüklenen ve user modda çalışan bir uygulama kısıtlı yetkilerinden dolayı kernel space’e direk olarak erişim sağlayamaz. Ve dolayısı ile de donanıma direk olarak erişemez. Bunu dolaylı yoldan yapar. Daha iyi anlamak için gelin bunu örneklendirelim.

Yukarıdaki görselde User Space ve Kernel Space’de var olan bazı öğeleri görüyoruz. Bir ransomware’ın dosyanın içine yazmak için WriteFile() fonksiyonunu kullandığını biliyoruz. WriteFile() API’si ise kernel32.dll içerisinde bulunan bir fonksiyondur.

User modda çağırılan API’ler ntoskrnl.exe’nin kernel executive bölümünde uygulanan system service routineslerini çağırır ve bu sayede donanım ile etkileşime girer.

Yukarıdaki görselde gördüğümüz ntdll.dll aslında bu noktada kritik bir öneme sahiptir. Kendisi user space ve kernel space arasında bir köprü görevi görmektedir. Aynı şekilde user32.dll ise kernel space’de bulunan GUI ile alakalı API’lerle ilgilnen win32k.sys ile bir köprü görevi görmektedir. Ama GUI ile ilgili bir işlem yapmadığımızdan ntdll.dll’e odaklanacağız.

Windows API Şematiği (Call Flow)

Windows işletim sistemini ele alacak olursak, API’lerin DLL’lerin içine implement edildiğini biliyoruz. API’ler ntoskrnl.exe’deki kernel executive bölümünden system service routinlerini (fonksiyonlarını) çağrımaktadır.

Yukarıdaki görselde de görüldüğü üzere yine WriteFile() API’sini kullanan user modda çalışan bir uygulamanın nasıl diskte bir yazma işlemi yaptığına göz atacağız.

Bir uygulamayı çalıştırdığımızda bu uygulama ile ilişiği bulunan tüm DLL’ler process memory’e yüklenir. Bir process oluştuğunda, her process’in bir thread’inin olması zorunluluğu olduğundan main thread’de oluşturulur. Burada dikkat edilmesi gereken ek bir bilgi vermek gerekirse var olan kodu çalıştıran process değil, thread’in kendisidir. Process ve thread user modda, kısıtlı yetkilerle oluşturulmuş oldu.

Programın WriteFile() API’sini çağırdığını düşünelim. Thread, bu API’nin hangi DLL’de ve hangi memory adresinde olduğunu bilmektedir. Bunu ise IAT şeklinde kısaltılan Import Address Table’dan bulmaktadır. Aynı şekilde uygulama runtime zamanında LoadLibrary() API’si ile bir DLL yükleyebilir, GetProcAddress() API’si ile de o DLL içerisinde fonksiyonu belirleyebilir. Uygulama runtime zamanında bir DLL yüklerse IAT bu bilgi ile doldurulmaz.

Thread WriteFile() API’sinin memory’deki yerini IAT sayesinde belirledi. Şimdi ise köprünün kurulacağı kısıma geldik. Kernel32.dll içerisinde bulunan WriteFile() API’si ntdll.dll’deki NtWriteFile() API’sini çağırır. Bu API, ntoskrnl.exe’nin kernel executive kısmındaki system service routine’indeki NtWriteFile ile aynı ada sahiptir. Gerçek NtWriteFile() API’si değildir. Ntdll.dll içerisindeki NtWriteFile() API’sini bir yanılsama olarak düşünebilirsiniz. Daha sonra ise ntdll.dll içerisindeki NtWriteFile() API’si x86 mimairsinde SYSENTER, x64 mimarisindeki SYSCALL insturction’larını çalıştırır ve kodu artık user moddan kernel moda geçirmiş olur.

Şimdi ise thread kernel modda herhangi bir kısıtlama olmadan çalışmaktadır. Orijinal NtWriteFile()’ın bulunduğu ntoskrnl.exe ‘de (system service routine) NtWriteFile() API’sini bulmak zorundadır. User modda birf API’yi IAT sayesinde yapıyor iken kernel modda bunu System Service Descriptor Table (SSDT)’a giderek NtWriteFile() API’sini bulmaktadır. NtWriteFile(), ntoskrnl.exe’nin system service routine’inden çağrıldığında bu istek I/O fonksiyonu olduğu için I/O manager’a yönlendirilmektedir. I/O manager ise isteği direk kernel mod device driver’a yollamakta, kernel mod device driver ise HAL tarafından dışa aktarılan rutinler sayesinde donanım ile etkileşim kurmaktadır. Daha sonrasında ise diske yazma işlemi yapılmaktadır.

 

TÜM MAKALELER