Windows Internals – Çekirdek Modül Adresini bulma

Herkese selamlar,

Sürücü geliştirmeye ve Kernel istismarlarıyla uğraşmaya başlamamla birlikte, Windows internals ile ilgili detaylara daha fazla girdiğimi söyleyebilirim. Bu yüzden elimden geldiğince Windows Internals ile ilgili uzun veya kısa olmak üzere bilgilendirici yazılar yazmaya gayret edeceğim. Bu özellikle kendim açımdan yararlı olacak ve bu sayede başkalarına da yararı olacağını düşünüyorum.

Bazı zamanlar bazı çekirdek modüllerinin temel adreslerini alma ihtiyacımız olabiliyor. Bunu üç farklı method kullanarak alabileceğimizi belirtmek istiyorum. Yazı içerisinde ntoskrnl ve win32k modüllerini baz alacağımız bazı yerler mevcut olacaktır.

Method 1 : Sorgu Sistem Bilgisi

Performans verilerini kullanarak süreçlerin listesini almak için belgelenmiş (documented) bir yol bulunmasına rağmen, Windows NT Görev Yöneticisi tarafından kullanılmadı. Bunun yerine belgelenmemiş (undocumented) olan ZwQuerySystemInformation fonksiyonu çağrılır. Bu fonksiyon çeşitli sistem bilgileri, özellikleri veya o an çalışan işlemlerin listesine erişim gibi detaylara NTDLL.DLL ile ulaşır. Bunların haricinde ise bazı zamanlar zararlı yazılımlar tarafından Hook işlevi için kullanılıyor.

ZwQuerySystemInformation‘ın kullanışı ile ilgili MSDN sayfalarına baktığımızda :

NTSTATUS WINAPI ZwQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation, _In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

Yukarıdaki ilk parametreye baktığımızda SYSTEM_INFORMATION_CLASS‘ı görüyoruz. Bu parametre bu fonksiyonun hangi tür bilgileri sorgulaması gerektiğini içeriyor.

İkinci parametre olan SystemInformation, bir arabellek için işaretçi görevi görür. Ve istenilmiş olan bilgiyi alır.

Üçüncü parametre olan SystemInformationLenght, alıcı arabellek boyutunu belirtir.

Son parametre olan ReturnLength ise işlevin istenilen bilgilerin gerçek boyutunu yazdığı bir yere, isteğe bağlı bir işaretçidir. Bu boyut SystemInformationLenght parametresinden küçük veya ona eşit olduğu takdirde, işlev bilgilerini SystemInformation ara belleğine kopyalar. Aksi takdirde bir NTSTATUS hata kodu verir ve ReturnLenght‘te istenilen bilgiyi almak için gereken tampon boyutunu çevirir.

status = ZwQuerySystemInformation( SystemModuleInformation, 0, bytes, &bytes );

pMods = (PSYSTEM_MODULE_INFORMATION)ExAllocatePoolWithTag( NonPagedPool, bytes, 'tag');
RtlZeroMemory( pMods, bytes );

status = ZwQuerySystemInformation( SystemModuleInformation, pMods, bytes, &bytes );

Yukarıda ZwQuerySystemInformation ile ilgili olarak kullanım örneğini görüyorsunuz. Burada tüm çekirdek modülleri hakkında bilgi alabilmek için SystemModuleInformation öğesini argüman olarak iletiyoruz.

Öncelikli olarak bu bilginin doğru boyutunu elde etmek, sırasıyla belleği ayırmak ve sistem bilgilerini sorgulamak için SystemInformation tamponuna 0 değerini iletiyoruz.

typedef struct _SYSTEM_MODULE_ENTRY{
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, *PSYSTEM_MODULE_ENTRY;
typedef struct _SYSTEM_MODULE_INFORMATION{
ULONG Count;
SYSTEM_MODULE_ENTRY Module[1];}
SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

Yukarıda görüldüğü üzere _SYSTEM_MODULE_INFORMATION içerisindeki modül alanı, her bir öğenin ilgili modül alanını temsil ettiği bir _SYSTEM_MODULE_ENTRY listesidir.

Çekirdek modüllerinden biri olan ntoskrnl modülü her zaman temel adresi (base address) pMods->Module[0].ImageBase olan ilk giriştir. Diğer modüller için listeyi dolaşıp her bir girişin FullPathName‘i, hedef modülün taban adresini alması ile karşılaştırabiliriz.

PSYSTEM_MODULE_ENTRY pMod = pMods->Modules;


STRING targetModuleName = RTL_CONSTANT_STRING("\systemroot\system32\win32k.sys");

STRING current;
for (ULONG i = 0; i < pMods->NumberOfModules; i++)
{
rRtlInitAnsiString(&current, (PCSZ)pMod[i].FullPathName);
if (0 == RtlCompareString(&targetModuleName, &current, TRUE))
{
g_ModuleBase = pMod[i].ImageBase;
g_ModuleSize = pMod[i].ImageSize;
break;
}
}

Yukarıdaki kodda path yolu “\Systemroot\system32\module-name ” olarak belirtilmiştir.

Alternatif olarak belirli bir modüle ait bazı veri yapıları veya fonksiyon adresleri varsa eğer bunu da modül baz adresini almak için kullanılabilir.

for (ULONG i = 0; i < pMods->NumberOfModules; i++)
{
// System routine is inside module
// checkPtr is an address within a module
if (checkPtr >= pMod[i].ImageBase &&
checkPtr < (PVOID)((PUCHAR)pMod[i].ImageBase + pMod[i].ImageSize))
{
g_KernelBase = pMod[i].ImageBase;
g_KernelSize = pMod[i].ImageSize;
break;
}
}

Method 1.2 – Sorgu Sistem Bilgisi (Aux_Klib)

Yukarıdaki örnekte olduğu gibi Microsoft tarafından aynı işleve sahip sağlanan diğer bir fonksiyon ismi de AuxKlibQueryModuleInformation olup Aux_Klib kütüphanesine aittir. MSDN tarafından sağlanan fonksiyon bilgilerine baktığımızda:

NTSTATUS AuxKlibQueryModuleInformation(
PULONG BufferSize,
ULONG ElementSize,
PVOID QueryInfo
);

Bu fonksiyonu kullanmak istediğimiz zaman ise ilk olarak AuxKlibInitialize‘i çağırmamız gerekiyor.

status = AuxKlibQueryModuleInformation( &bufferSize,
sizeof(AUX_MODULE_EXTENDED_INFO),
0);

Daha sonra ise tıpkı ZwQuerySystemInformation‘da olduğu gibi bilgi ara belleğinin boyutlarını almamız gerekiyor.

moduleBuffer = (PAUX_MODULE_EXTENDED_INFO)ExAllocatePoolWithTag(NonPagedPool, bufferSize, 'tag');

Bir sonraki adımda ise ara bellek için bellek ayırma yaparken yukarıdaki adıma dikkat ediyoruz.

status = AuxKlibQueryModuleInformation(
&bufferSize,
sizeof(AUX_MODULE_EXTENDED_INFO),
moduleBuffer);

Bu adımlardan sonra sorgu modülü listemiz yukarıdaki gibi oluyor.

Daha sonra ise AUX_MODULE_EXTENDED_INFO liste yapısını aldıktan sonra aşağıdaki şekilde deklare ediyoruz.

typedef struct _AUX_MODULE_BASIC_INFO
{
PVOID ImageBase;
} AUX_MODULE_BASIC_INFO, *PAUX_MODULE_BASIC_INFO;

typedef struct _AUX_MODULE_EXTENDED_INFO
{
AUX_MODULE_BASIC_INFO BasicInfo;
ULONG ImageSize;
USHORT FileNameOffset;
UCHAR FullPathName [AUX_KLIB_MODULE_PATH_LEN];
} AUX_MODULE_EXTENDED_INFO, *PAUX_MODULE_EXTENDED_INFO;

Ve yukarıdaki bilgileri kullanarak herhangi bir spesifik sistem modülünü bulabiliriz.

Method 2- Travers Sistem Modülü Listesi

Şuanda sistemlerde hali hazırda yüklü olan tüm çekirdek modüllere yönelik olan global işaretçi dizini PsLoadedModuleList’de belirtilir. Ve burası PatchGuard tarafından korunmaktadır ve herhangi bir hata modülünü tetiklemeden, direkt olarak değiştirilemez. Bu listedeki tüm girişler type _KLDR_DATA_TABLE_ENTRY şeklindedir.

typedef struct _KLDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
PVOID ExceptionTable;
ULONG ExceptionTableSize;
PVOID GpValue;
PNON_PAGED_DEBUG_INFO NonPagedDebugInfo;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT __Unused5;
PVOID SectionPointer;
ULONG CheckSum;
PVOID LoadedImports;
PVOID PatchInformation;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;

Yukarıdaki veri yapısına baktığımızda, bu yapının PsLoadedModuleList‘e ait olduğunu görebiliyoruz. Yukarıdaki yapıya baktığımızda; her bloğun ilk liste alanı InLoadOrderLinks ile bağlantılıdır. Böylece listeyi değiştirebilir ve bir blok için bazı bilgiler alabiliriz (FullDllName veya BaseDllName). Daha sonrada bunları hedef modül ile karşılaştırır ve temel adresi alabiliriz.

PsLoadedModuleList Bulma

(Burada ntokskrnl modülünü baz alacağız) İlk olarak ntoskrnl modülü bulabilir ve daha sonrasında da PsLoadedModuleList‘i bulmak için geri dönebiliriz. Çünkü yukarıdaki yapı sistem modülü listesinin başıdır ve ntoskrnl modülü her zaman listedeki ilk girendir.

İlk olarak ntoskrnl‘nin temel adresini (base address) bulacağız ve daha sonrasında da sistem modülü listesinde bir atılım noktası bulmamız gerekecek. Bunu yapmak içinde kendi sürücü modelimizi kullanabiliriz.(listede)

Sürücülerle ilgili kısa bir bilgi vermek gerekirse eğer; her sürücünün giriş fonksiyonu DriverEntry‘dir ve PDRIVER_OBJECT türünde DriverObject adlı bir parametresi bulunur. PVOID tipine sahip DriverSection alanı aslında KLDR_LOAD_TABLE_ENTRY yapısının bir göstergesidir. Burası sürücü modülünün depoladığı bilgilerin yer aldığı, tam olarak ise sistem modülleri listesine yerleştirilen alandır.

Yukarıdaki temel bilgilerden sonra buradaki temel fikir, sürücümüzün liste bloğundan başlayıp, ntoskrnl modülünü bulmak için listeyi dolaşmak ve sonunda da PsLoadedModuleList‘e ulaşmaktır.

// PsLoadedModuleList adresi alma
for (PLIST_ENTRY pListEntry = pThisModule->InLoadOrderLinks.Flink; pListEntry != &pThisModule->InLoadOrderLinks; pListEntry = pListEntry->Flink)
{
// Ntoskrnl girişi için arama
PKLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD( pListEntry, KLDR_DATA_TABLE_ENTRY, InLoadOrderLinks );
if (kernelBase == pEntry->DllBase)
{
// Ntoskrnl her zaman listeye ilk girendir
// Bir önceki giriş ise PsLoadedModuleList
// Bulunan işaretçinin Ntoskrnl modülüne ait olup-olmadığı kontrol edilir
if ((PVOID)pListEntry->Blink >= pEntry->DllBase && (PUCHAR)pListEntry->Blink < (PUCHAR)pEntry->DllBase + pEntry->SizeOfImage)
{
PsLoadedModuleList = pListEntry->Blink;
break;
}
}
}

Method 3 – Sürücü ismi ile bulma

İşletim sistemi içerisinde yer alan her sürücü bir isme sahiptir (\driver\driver name). Bizlerde sürücü nesnesi işaretçisini (driver object pointer) undocumented fonksiyon olan ObReferenceObjectByName ile alabiliriz.

NTSTATUS
NTSYSAPI
NTAPI
ObReferenceObjectByName(
IN PUNICODE_STRING ObjectPath,
IN ULONG Attributes,
IN PACCESS_STATE PassedAccessState,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN OUT PVOID ParseContext,
OUT PVOID* ObjectPtr);

Buradaki ObjectPath belirtmemiz gereken sürücü ismidir, ikinci olarak AccessMode‘un KernelMode olması gerekecek, üçüncü olarak ObjectPtr imleci belirtilen sürücü modülünün _DRIVER_OBJECT yapısına almak için kullanılır. Bunların haricinde ObjectType parametresi nesnenin hangi türe ait olduğu hakkında bilgi verir ve buradaki sürücü IoDriverObjectType‘dır.

Aşağıdaki örnekte keyboard class sürücüsünü (kbdclass.sys) örnek olarak kullanıyoruz.

// Sürücü tipini açıkca belirtmek gerekiyor

extern POBJECT_TYPE IoDriverObjectType;
UNICODE_STRING kbdName =RTL_CONSTANT_STRING(L"\\Driver\\kbdclass");

PDRIVER_OBJECT pKbdDriverObject;
ObReferenceObjectByName(
&kbdName,
OBJ_CASE_INSENSITIVE,
NULL,
0,
IoDriverObjectType,
KernelMode,
NULL,
&pKbdDriverObject);

Belirtilen sürücünün sürücü nesnesi alındıktan sonra, KLDR_LOAD_TABLE_ENTRY yapısını DriverSection alanı aracılığı ile alabilir ve temel adresi de dahil olmak üzere bazı yararlı bilgiler alabiliriz.

Yazı içerisinde yazım yanlışları veya anlam bütünlüğü olmayan kısımlar gibi hatalar olabilir. Bu yüzden anlaşılmayan noktalar veya yanlışlıkla şayet mevcutsa yorum olarak belirtmenizde fayda olacaktır.

Referanslar

1- https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_driver_object

2- https://docs.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation

3- https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/query.htm

4- http://beefchunk.com/documentation/sys-programming/os-win32/enum_proc/enumproc.html

5- https://www.triplefault.io/2017/07/loading-kernel-symbols-vmm-debugging.html