bekir karul : Sistem Bildirim Rutinlerinin Özeti

Selamlar.

5 saat önce yazılardan birinde bir dostumuz sistemteki bildirim rutinleri ile ilgili bir yazı yazarsak faydalı olacağını belirtmiş, zaten bir süredir motivasyon eksikliği yüzünden yazı yazmıyordum fakat bu mesajı görünce tekrar heveslendim ve ne kadar süreceğini bilmediğim için direk yazmaya başlıyorum. Sanırım Azerbaycan’dan yazan bir dostumuz, buradan Azerbaycan’daki okuyuculara da selam ve sevgilerimi gönderiyorum onun aracılığıyla…

Sistemdeki bildirim rutinleri dediğimizde ne demek istiyoruz? Şöyle ki, işletim sisteminde malumunuz gerçekleşen birtakım olaylar var, mesela işlem oluşturulması, işlemcik oluşturulması, kayıt defteri işlemleri, bilgisayarın kapatılması, nesneler üzerindeki işlemler gibi. İşletim sistemi geliştiricileri bu olayların olması sırasında geliştiricilere bu olayları takip edebilecekleri veya ona göre birtakım işlemler yapabilecekleri bir mekanizma sunmuşlar, bunlara da sistem bildirimleri diyoruz. Bu bildirimler bir çok uygulama tarafında kullanılır, örneğin VMware gibi sanallaştırma yazılımından tutun da, hemen hemen tüm anti-x uygulamaları bu sistem çağrılarını kullanmaktadırlar. Bunların dışında örneğin zararlı yazılım geliştiricileri de bu bildirimleri kendi çıkarları doğrultusunda kullanmaktalar. Mesela çok basit bir örnek verelim, diyelim ki siz bir zararlı yazılım geliştirdiniz ve “hıyar” isimli işlem sizin çalışmasını istemediğiniz bir işlem. Bu durumda siz bir adet işlem bildirim rutini kayıt ettiriyorsunuz, Windows her yeni işlem oluşturulduğunda sizin bildirim rutininizi çağırıyor ve siz bu rutin içerisinden başlatılan işlemi inceleyip kendi kararınıza göre sonlandırabiliyorsunuz. Bu arada, şunu da ekleyeyim bu bildirim rutinleri her Windows sürümünde yok. Ama Vista ve sonrası sürümlerde olduklarını söylesem yanlış söylemim olmam herhalde. Peki daha önceki sürümlerde ne yapıyorlardı? O zaman da zamanın izin verdiği taklalar atarak işletim sisteminin işleyişine müdahale edebiliyorduk.

Yukarıda da değindiğimiz gibi bu bildirim rutinleri birkaç başlık altında toplanabilir. Şimdi mümkün olduğunca bu farklı başlıkları ele alalım bakalım neler varmış. Fakat şunu da ekleyeyim bu aşağıdaki başlıkların her birinden ayrı ve uzun bir yazı da çıkabilir. Çünkü bu rutinleri geliştirirken dikkat edilmesi gereken kısıtlamalar falan da var. Yine bazı dikkat edilmesi gereken keskin noktalar da mevcut, ileride gerekirse onlardan da bahseden bir yazı yazarım inşallah.

İşlem Bildirim Rutinleri

İşlem bildirim rutinleri adından da anlaşılabileceği gibi herhangi bir işlem oluşturulduğundan meydana gelen bildirim rutinleridir. Şu anda bildiğim kadarıyla bu bildirim rutinlerinin üç farklı sürümü var. Bunlar kronolojik sırayla şu şekilde : PsSetCreateProcessNotifyRoutine, PsSetCreateProcessNotifyRoutineEx ve PsSetCreateProcessNotifyRoutineEx2.

PsSetCreateProcessNotifyRoutine

Tarihsel sırayla gidelim. İlk fonksiyonumuz Windows 2000‘den itiraben kullanılabiliyor ve bizden bir adet işlem oluşturulduğunda çağırılacak fonksiyon istiyor. Bu fonksiyonun PASSIVE_LEVEL seviyesinde ve kritik bir alan içerisinde(yani normal seviyesine sahip çekirdek APC çağrıları devre dışı iken) çağırılacağını akılda tutmakta yarar var. Aşağıda ilk fonksiyonun prototipini görüyorsunuz. Burada NotifyRoutine bildirim oluştuğunda çağırılacak olan fonksiyon. Remove ise sürücünüzü kaldırdığınızda bu bildirim rutinlerini de kaldırmanız gerektiği için bunu yaparken TRUE değerinin vermeniz gereken parametre. Bu arada lafı açılmışken ekleyelim, buradaki tüm bildirim rutinlerini çekirdek modundaki bir sürücüden ekliyorsunuz. Sürücünüz hafızadan kalkarken eklediğiniz tüm bildirim rutinlerini de silmek sizin göreviniz. Eğer bunu yapmazsanız kısa bir zaman içerisinde bilgisayarın mavi ekran hatası vereceği sanıyorum izahtan varestedir.

NTSTATUS PsSetCreateProcessNotifyRoutine(
  _In_ PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
  _In_ BOOLEAN                        Remove
);

Bizden yazmamız beklenen bildirim rutininin şekli ise aşağıdaki gibi:

PCREATE_PROCESS_NOTIFY_ROUTINE SetCreateProcessNotifyRoutine;

void SetCreateProcessNotifyRoutine(
  _In_ HANDLE  ParentId,
  _In_ HANDLE  ProcessId,
  _In_ BOOLEAN Create
)
{ ... }

Burada Create isimli BOOLEAN tipine sahip parametre işlem oluşturulduğunda TRUE oluyor, işlem sonlandırılırken ise FALSE oluyor. Bu sonlandırılmadan kasıt ise yanlış hatırlamıyorsam işlem içerisindeki son işlemcik de sonlandırıldığında demekti. ParentId işlemi oluşturan işlemin belirteci, ProcessId ise şu anda oluşturulan işleme tahsis edilen işlem belirteci.

Hemen küçük basit bir örnek vereyim:

//
//  Sürücünüz ilklenirken çalışan kodlar
//
//  Burada fonksiyon ya STATUS_SUCCESS geri dönecek, -ki bu 
//  fonksiyonun kayıt ettirildiğini gösterir- ya da bunun dışında
//  STATUS_INVALID_PARAMETER geri dönecek. Bu durumda ya kayıt etmeye
//  çalıştığınız rutin zaten kayıtlı, ya da sistem bildirim rutini limit
//  sayısına(64 olması lazım) ulaştı demektir.
//

NtStatus = PsSetCreateProcessNotifyRoutine(Bildirimci, FALSE);
if(!NT_SUCCESS(NtStatus))
{
	DEBUG("İşlem bildirimleri oluşturulamadı!");
	pDContext->ProcessNotifyEnabled = FALSE;
	goto RETURN;
}

//
// Sürücünüz kaldırılırken çalışan kodlar
//

if(pDContext->ProcessNotifyEnabled == TRUE)
{
	NtStatus = PsSetCreateProcessNotifyRoutine(Bildirimci, TRUE);
	...
	...
}

//
// Bildirim rutini
//
void Bildirimci(
    _In_ HANDLE  ParentId,
    _In_ HANDLE  ProcessId,
    _In_ BOOLEAN Create)
{
	if(Create == TRUE)
	{
		DEBUG("Yeni bir işlem oluşturuldu! PID : %x", ProcessId);
	}

}

PsSetCreateProcessNotifyRoutineEx

Bir diğer türdeki işlem bildirim rutinleri için kullanacağımız fonksiyon ise PsSetCreateProcessNotifyRoutineEx. Bu fonksiyon da yine yukarıdakine benzer, yalnızca aşağıda bildirim rutininin şekline bakarsanız diğerinden farklı olduğunu görebilirsiniz. Fakaaat, burada şu ayrıntı önemli. Bu Ex son ekine sahip olan sürümü ekleyebilmeniz için sürücü dosyanızın IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY değerine sahip olması gerekiyor. Ki, bu da sürücünüzün dijital imzaya sahip olması gerektiği anlamına geliyor. Eğer bu imza olmazsa kayıt işlemi sırasında fonksiyon kuvvetle muhtemel STATUS_ACCESS_DENIED değerini geri dönecektir. Ve yine şunu da ekleyelim bu bildirim rutini Windows Vista SP1 ve Windows Server 2008’den ve sonraki sistemlerde kullanılabilir. Son olarak, bu bildirim rutinleri de bir öncekinde söylediğimiz koşullar altında çağırılmaktadırlar.

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  _In_ BOOLEAN                           Remove
);

Bu türdeki bildirim rutinlerinde kullanılan fonksiyonun tipine bakarsak neyin farklı olduğunu daha net bir şekilde görebiliriz.

PCREATE_PROCESS_NOTIFY_ROUTINE_EX SetCreateProcessNotifyRoutineEx;

void SetCreateProcessNotifyRoutineEx(
  _In_        HANDLE                 ParentId,
  _In_        HANDLE                 ProcessId,
  _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{ ... }

Buradaki ilk iki parametre diğeriyle aynı fakat son parametrenin CreateInfo gibi farklı ve bir yapıya gösterici olduğunu görüyoruz. Peki bu yapı nasıl? Şöyle:

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;              // Bu yapının boyutu
  union {
    ULONG  Flags;                        // Ayrılmış, kullanmayın deniyor
    struct {
      ULONG FileOpenNameAvailable  :1;   // Eğer bu alan TRUE ise ImageFileName alanı kesinlikle NULL değil demektir
      ULONG IsSubsystemProcess   :1;     // Eğer altsistem Win32 dışında bir şey ise TRUE olur
      ULONG Reserved  :30;
    };
  };
  HANDLE              ParentProcessId;  // Bu işlemi oluşturan işlemin belirteci
  CLIENT_ID           CreatingThreadId; // Bu işlemi oluşturan işlemin/işlemciğin belirteci
  struct _FILE_OBJECT  *FileObject;     // Çalıştırılan dosyanın nesnesine gösterici
  PCUNICODE_STRING    ImageFileName;    // Çalıştırılan dosyanın adı/yolu
  PCUNICODE_STRING    CommandLine;      // İşlemin komut satırı
  NTSTATUS            CreationStatus;   // Oluşturulma durumunun sonucu
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

Buradaki tüm alanları tek tek açıklamaya girmek istemiyorum lakin bu açıklama kısmına girersek mesela dosya bildirim rutinlerinin alanlarını açıklamak 5874 gün sürer. O nedenle bu kısmı bir alıştırma olarak okuyuculara bırakıyorum fakat yine de kısa özetleri yukarıda bıraktım. Zaten buradaki alanların isimleri ne oldukları konusunda da az çok fikir veriyor. Ama elimizdeki imkanı belirtmek adına sadece bir tane örnek vereyim. Yapı içerisindeki CreationStatus alanını STATUS_ACCESS_DENIED yaparsanız oluşturulan işlem çalışmadan sonlandırılacaktır. Bu tür bildirim rutinleri de tıpkı öncekinde yaptığımız şekilde kayıt ettiriliyor. Sadece kullanılan bildirim kayıt eden fonksiyon ve bildirim fonksiyonunun tipi değişiyor. Ve ayrıca tekrar üstünde durmakta fayda var, sürücünüzün imzalı olması gerekiyor.

PsSetCreateProcessNotifyRoutineEx2

Evet, işlem bildirimleri için kullanılan son dostumuza geldik. Bu bildirim rutini tipi bildiğim kadarıyla en son eklenen(Windows 10(1703)) işlem bildirim rutini çeşidi. Ha, şunu da söyleyeyim, Windows’un hiç bize anlatmadığı yani ortalığa çıkmayan bildirim rutinleri de her zaman olabilir. Microsoft bu konuda oldukça ağzı sıkıdır. Yani tarzanca söylersek “undocumented” birçok şey hala çekirdeğin içerisinde duruyor haberiniz olsun :) Neyse, devam edelim. Bu tipte de sürücünüzün dijital imzaya sahip olması gerekiyor. Ve aksi durumda önceki bildirim rutini tipindeki hatayı alıyorsunuz. Bu bildirimi kayıt eden fonksiyonun tipi şu şekilde:

NTSTATUS PsSetCreateProcessNotifyRoutineEx2(
  _In_ PSCREATEPROCESSNOTIFYTYPE  NotifyType,
  _In_ PVOID                     NotifyInformation,
  _In_ BOOLEAN                   Remove
);

Burada gördüğünüz gibi diğerlerinden farklı olarak iki parametre var. Ve asıl ilginç olan ise NotifyInformation alanının PVOID tipine sahip olması. E diğer parametrenin de bir tip gösteren değer olduğunu düşünürsek bu fonksiyona ileride yeni tipler ekleyebilirler anlamına geliyor. Hatta şu an biraz incelesek belki de dışarı söylenmemiş olan tiplerin olduğunu görebiliriz. Bu tip alanı için kullanabildiğimiz değerler şimdilik şöyle:

typedef enum _PSCREATEPROCESSNOTIFYTYPE { 
  PsCreateProcessNotifySubsystems  = 0 // İşlem bildirim rutini tüm altsistemler için çağırılacak demek
} PSCREATEPROCESSNOTIFYTYPE;

Eğer bu NotifyType alanına PsCreateProcessNotifySubsystems değerini verirseniz NotifyInformation alanı üstteki bildirim tipinde olduğu gibi PCREATE_PROCESS_NOTIFY_ROUTINE_EX yapısı ile çalışacak bir fonksiyon tipi bekliyor demektir. İleriki zamanlarda buraya daha farklı fonksiyonların ekleneceği de kuvvetle muhtemel o nedenle arada bir göz atmakta yarar olabilir.

İşlemcik Bildirim Rutinleri

Bir diğer bildirim rutinimiz ise işlemcikler için. -Bu arada işlemcik diyorum da, eğer takip ediyorsanız bu kelimeyi Thread kelimesinin karşılığı olarak kullandığımı sanırım fark etmişsinizdir.- Bu bildirim rutinleri yine anlaşılabileceği gibi bir işlem kendi içerisinde bir işlemcik oluşturduğunda çağırılıyor. Yahut bir işlem başka bir işlem içerisinde bir işlemcik oluşturduğunda. Hmm. Bu durumda insanın kafasına birtakım katakulliler geliyor zira bir işlem neden başka bir işlemin içerisinde işlemcik oluşturur ki?

Bu bildirimler için yine beni bildiğim kadarıyla iki farklı tip var. Bunları kayıt ettirmek için sırasıyla PsSetCreateThreadNotifyRoutine ve PsSetCreateThreadNotifyRoutineEx fonksiyonlarını kullanıyoruz. Bu iki tip PASSIVE_LEVEL veya APC_LEVEL seviyesinde çağırılabilir bu nedenle farklı IRQL seviyelerinde yapabilecekleriniz hakkında bilginiz olursa sürücünüzün sağlığı için iyi olur sahiden. Yine tarihsel bir sırayla gidip bu fonksiyonları ve bu fonksiyonlar ile kayıt ettirilen bildirim rutinlerini inceleyelim.

PsSetCreateThreadNotifyRoutine

Fonksiyonun tipi şu şekilde:

NTSTATUS PsSetCreateThreadNotifyRoutine(
  _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

Burada ilk dikkat çeken şey işlem bildirim rutinlerinde olan Remove parametresinin olmaması. E bu işlemcik rutinlerini silmemize gerek anlamına mı geliyor? Elbette hayır. Bunun için ise farklı bir fonksiyon olan PsRemoveCreateThreadNotifyRoutine fonksiyonu kullanıyoruz. Yukarıda işlemcik bildirim rutini için verilen PCREATE_THREAD_NOTIFY_ROUTINE tipine yakından bakalım:

PCREATE_THREAD_NOTIFY_ROUTINE SetCreateThreadNotifyRoutine;

void SetCreateThreadNotifyRoutine(
  _In_ HANDLE  ProcessId,
  _In_ HANDLE  ThreadId,
  _In_ BOOLEAN Create
)
{ ... }

İlk işlem bildirim rutininin hemen hemen aynısı gördüğünüz gibi. Yalnızca burada ParentId gibi bir şey yok doğal olarak. Onun yerinde oluşturulan işlemciğin belirteci ve bu işlemciği oluşturan işlemin belirteci var. Create parametresi ise yine işlem bildirim rutininde olduğunu gibi eğer işlemcik oluşturuluyorsa TRUE, sonlandırılıyorsa FALSE oluyor. Bu arada, burada şunu diyebilirsiniz “yav üç tane malayani parametre bizim ne işimize yarayacak?!” ama öyle değil. Burada yalnızca ThreadId bile kullanmayı bilen için dağları aşmak demektir… Bunun için de sistem seviyesindeki bilginizin artması gerekiyor tabi. Bu da zamanla, çalışmayla ve sabırla ve bir de opsiyonel olarak iyi niyetle olacak bir şey. Zaten sistem seviyesinde bilginiz olmadan bu tür rutinlerin varlığı sizi Sartre benzeri bir anlamsızlığa sürükleyebilir hehe.

PsSetCreateThreadNotifyRoutineEx

Diğer tipimizi kayıt ettiren fonksiyon ise bu arkadaş. Sonunda bulunan Ex‘in genişletilmiş anlamına gelen “extended“dan geldiğini hatırlıyorum sanki. Ordan gelmiş olmasa bile yine de bu fonksiyonların sonundaki Ex‘i gayet güzel açıklıyor bence. Bu tip için dikkat etmeniz gereken şey bu türdeki bildirim rutinlerinin Windows 10‘dan ve sonrasında kullanılabiliyor olması. Açıkçası ben bunu daha öncesinde de var diye biliyordum ama şu an deneyebileceğim bir sanal makine olmadığı için test edemedim.

Fonksiyonun tipi aşağıdaki gibi:

NTSTATUS PsSetCreateThreadNotifyRoutineEx(
  _In_ PSCREATETHREADNOTIFYTYPE NotifyType,
  _In_ PVOID                    NotifyInformation
);

Bu tipi de silmek için yine PsRemoveCreateThreadNotifyRoutine fonksiyonunu kullanıyoruz. Bu fonksiyonda da görüldüğü üzere farklı tipler belirleyebiliyoruz NotifyType parametresi ile. Bunlar şu şekilde:

typedef enum _PSCREATETHREADNOTIFYTYPE { 
  PsCreateThreadNotifyNonSystem   = 0,  // Aşağıda açıkladım 
  PsCreateThreadNotifySubsystems  = 1   // Tüm altsistemler için çağırılıyor
} PSCREATETHREADNOTIFYTYPE;

Burada eğer NotifyType parametresi PsCreateThreadNotifySubsystems ise, bu fonksiyon PsSetCreateThreadNotifyRoutine ile aynı çalışıyor. Fakat eğer bu NotifyType parametresi PsCreateThreadNotifyNonSystem olursa o zaman olay biraz daha farklılaşıyor. Teknik anlamda değişen şey şu, bizim bildirim rutinimizin çalıştırıldığı yer ve zaman değişiyor. İlk durumda bildirim rutinimiz işlemciği oluşturan işlemciğin içerisinde çağırılıyor. İkinci durumda ise bildirim rutinimiz oluşturulan işlemciğin içerisinde çağırılıyor. Bu fark bildirim rutininizde yapmak istediğiniz işlemlere göre size ek kolaylık sağlayabilir.

Çalıştırılabilir Dosya Yükleme Bildirim Rutinleri

Bu rutinler sistemki bir çalıştırılabilir dosya hafızaya yüklendiği veya disk ile eşlendiği(“mapping”) zaman çalıştırılıyor. Örneğin .exe, .dll, .sys uzantılı dosyaları bunlar arasında gösterebiliriz. Bunun için de yine iki farklı fonksiyon kullanabiliyoruz bunlar PsSetLoadImageNotifyRoutine ve PsSetLoadImageNotifyRoutineEx.

PsSetLoadImageNotifyRoutine

Windows 2000 ve sonrasında kullanılabilen bu fonksiyonun tipi aşağıdaki gibi:

NTSTATUS PsSetLoadImageNotifyRoutine(
  _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

Bu fonksiyonla ilgili önemli bir ayrıntı şu, Windows 8.1 öncesi sistemde aynı anda kayıt edilebilecek en fazla bildirim rutini sekiz. Bu nedenle bu fonksiyonu çağırdığınızda STATUS_INSUFFICIENT_RESOURCES geri dönebilir. Hele ki sisteminizde birden fazla güvenlik yazılımı varsa bu hatayı alma olasılığınız oldukça artıyor diyebiliriz. Fakat Microsoft, Windows 8.1 ile gelen bir güncelleme ile bu limiti 64’e çıkardı bilginiz olsun. Bir de, bu güncelleme aynı zamanda Windows 7’ye de yüklenebiliyor diye biliyorum.

Yine bu fonksiyon ile eklediğiniz çalıştırılabilir dosya bildirim rutinlerini silmek için PsRemoveLoadImageNotifyRoutine fonksiyonu kullanabilirsiniz. Bu fonksiyona diğerlerinde olduğu gibi kayıt ettirdiğiniz bildirim rutini fonksiyonunu parametre olarak veriyorsunuz. Bildirim rutinimizin PLOAD_IMAGE_NOTIFY_ROUTINE sahip tipini genişletirsek şöyle bir şey görüyoruz:

PLOAD_IMAGE_NOTIFY_ROUTINE SetLoadImageNotifyRoutine;

void SetLoadImageNotifyRoutine(
  _In_opt_ PUNICODE_STRING FullImageName,
  _In_     HANDLE          ProcessId,
  _In_     PIMAGE_INFO     ImageInfo
)
{ ... }

Burada yine çok işe yarayan üç adet parametre görüyoruz. Bunlardan ilk ikisi isimlerinden de anlaşılabileceği gibi dosyanın ismini ve bu dosyayı işleyen işlemin belirteci. Burada dikkat etmeniz gereken en önemli şeylerden biri FullImageName alanının NULL değer olabileceği. O nedenle sanki bu alan hiç NULL olmayacakmış gibi hareket etmemek gerekiyor. Hatta unutmayın ki çekirdek modundasınız, kullanacağınız tüm göstericilere dikkat etmeniz gerekiyor elbette.

Son parametre olan ImageInfo veri yapısı ise şu şekilde tanımlanmış.

typedef struct _IMAGE_INFO {
  union {
    ULONG Properties;
    struct {
    };
    ULONG ImageAddressingMode  :8;  // Her zaman IMAGE_ADDRESSING_MODE_32BIT
    ULONG SystemModeImage  :1;      // Eğer yüklenen bir sürücü ise TRUE
    ULONG ImageMappedToAllPids  :1; // Her zaman 0
    ULONG ExtendedInfoPresent  :1;  // Aşağıda açıkladım
    ULONG MachineTypeMismatch  :1;  // Her zaman 0
    ULONG ImageSignatureLevel  :4;  // SE_SIGNING_LEVEL_* değerlerinden biri
    ULONG ImageSignatureType  :3;   // SE_IMAGE_SIGNATURE_TYPE tiplerinden biri
    ULONG ImagePartialMap  :1;
    ULONG Reserved  :12;            // Eğer dosyanın tamamı disk ile eşlenmiyorsa 0'dan farklı bir değer 
  };
  ULONG  ImageBase;                 // Dosyanın hafızadaki başlangıç adresi
  ULONG  ImageSelector;             // Her zaman 0
  SIZE_T ImageSize;                 // Dosyanın hafızadaki boyutu
  ULONG  ImageSectionNumber;        // Her zaman 0
} IMAGE_INFO, *PIMAGE_INFO;

Yapıdaki alanlarla ilgili kısa özetleri yukarıda belirttim fakar özellikle şuna değinelim, eğer ExtendedInfoPresent alanı TRUE ise bu veri yapısı aslında IMAGE_INFO_EX isimli biraz daha büyük bir veri yapısının bir parçası demektir. O veri yapısı da şu şekilde:

typedef struct _IMAGE_INFO_EX {
  SIZE_T              Size;         // Bu yapının boyutu
  IMAGE_INFO          ImageInfo;    // IMAGE_INFO
  struct _FILE_OBJECT  *FileObject; // Dosyanın nesnesine gösterici
} IMAGE_INFO_EX, *PIMAGE_INFO_EX;

Bu durum olduğunda bu veri yapısına ulaşmak için CONTAINING_RECORD makrosunu kullanabilirsiniz.

PsSetLoadImageNotifyRoutineEx

Bu fonksiyon da bir öncekinin birazcık genişletilmişi. Windows 10 ve sonrasında kullanılabiliyor. Aşağıdaki tipe sahip:

NTSTATUS  PsSetLoadImageNotifyRoutineEx(
  _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine,
  _In_ ULONG_PTR                  Flags
);

Bu fonksiyonumuzun ilk parametresi yine öncekindeki gibi bir bildirim rutini. Değişen şey ise ikinci bir parametrenin gelmiş olması yani Flags. Bu alan için şu anda MSDN üzerinde anlatılmış sadece bir değer var, o da PS_IMAGE_NOTIFY_CONFLICTING_ARCHITECTURE. Bu bildirim rutinlerinin mimarisi ne olursa olsun tüm dosyalar için çağırılacağını belirtiyor.

Özellikle bu iki tip için şunu söyleyelim, bu rutinler içerisindeyken kullanıcı modundan hafıza tahsis etmek, serbest bırakmak, diski hafızaya eşlemek gibi işlemler çok tehlikeli. Lakin bu işlemler sırasında kullanılan bir hızlı mutex nesnesi sorun çıkartıyor. Bu konudan ağzı yanan da epey insan vardır (bknz: anti-malware için sürücü yazan bekir).

İşlem, İşlemcik ve Çalıştırılabilir Dosya Bildirim Rutinleri İçin Öneriler

Bu bildirim rutinleri her ne kadar kullanması çok basit olsa da dikkat edilmesi gereken bazı hususlar var. Bunları aşağıdaki listede görebilirsiniz. Bu listede yer alanlar tarafımdan da tecrübeyle sabitlendiği için dikkate almanızda kesinlikle yarar var….

Eğer yukarıdaki listedeki bazı şeyleri illa yapmanız gerekiyorsa bunun için “System Worker Threads” kullanmayı tercih edebilirsiniz.

Bonus! Bildirim Rutini Ekleyen Bir Fonksiyonun İncelenmesi

Az önce anlık bir sevinç patlaması yaşadım o nedenle şimdi bu bildirim rutinlerini ekleyen fonksiyonlardan birinin incelemesini de yapmaya karar verdim. Bu sayade bunların sistem seviyeinde nasıl eklendiğini ve nasıl çağırıldığını görme imkanına da sahip olacağız…

Bu inceleme için ben çalıştırılabilir dosya bildirim rutinlerini seçtim. O halde öncelikle incelememize PsSetLoadImageNotifyRoutine fonksiyonu ile başlayalım. Bakalım nerelere gideceğiz… Aşağıdaki her şey benim son sürüm en güncel Windows 10 bilgisayarımdan alınmadır.

public PsSetLoadImageNotifyRoutine
PsSetLoadImageNotifyRoutine proc near
sub     rsp, 28h
xor     edx, edx        ; Flags
call    PsSetLoadImageNotifyRoutineEx
add     rsp, 28h
retn
PsSetLoadImageNotifyRoutine endp

Görüldüğü gibi bu yeni sistemleride Ex‘li olmayan fonksiyon, Ex‘li olana bir zıplama alanı olarak kullanılıyor. Burada dikkate değer tek şey Flags alanının bir xor işlemi ile sıfırlandığı. O halde yola Ex son ekli fonksiyon ile devam edelim.

mov     rsi, rcx
test    rdx, 0FFFFFFFFFFFFFFFEh ; Flaglerde beklemediğimiz bir şey var mı?
jnz     WRONG_FLAG ; Evet var!
call    ExAllocateCallBack ; Yok, o zaman bir tane çağrım nesnesi ver bize
mov     rdi, rax
test    rax, rax ; Verebildi mi?
jz      INSUF_RESOURCES ; Veremediyse dallan :(
xor     ebx, ebx

Burada öncelikle Flags parametresinin geçerliliği kontrol ediliyor. Eğer bu kontrolden geçebilirsek bir adet çağrım nesnesi tahsis edilip bize veriliyor. Bu nesneyi tahsis eden dostumuz oldukça kısa:

mov     edx, 18h        ; NumberOfBytes, 24, bizim durumumuzda 3*8 yani 3 elemanı var
mov     ecx, 200h       ; PoolType  // Çalıştırılamaz havuz
mov     r8d, 'brbC'     ; Tag
call    ExAllocatePoolWithTag
mov     rbx, rax
test    rax, rax
jz      short loc_140597FBE ; Hafıza kalmadı mı yahu?!
mov     rcx, rax        ; SpinLock
mov     [rax+_EX_CALLBACK_ROUTINE_BLOCK.Callback], rsi ; rcx'den geldi yukarda, bizim fonksiyon
mov     [rax+_EX_CALLBACK_ROUTINE_BLOCK.Context], rdi  ; rdx'den geldi yukarda, context = 0
call    ExInitializePushLock  ; rcx'e alınan kilit nesnesini ilkliyor

Buradaki disassembly çıktısından fonksiyonu tekrar oluşturursak şöyle bir şey olduğunu söyleyebiliriz:

PEX_CALLBACK_ROUTINE_BLOCK ExAllocateCallBack(PVOID Function, PVOID Context)
{
  EX_CALLBACK_ROUTINE_BLOCK *Callback;

  Callback = ExAllocatePoolWithTag(NonPagedPoolNx, sizeof(EX_CALLBACK_ROUTINE_BLOCK), 'brbC');
  if ( Callback )
  {
    Callback->Callback = Function;
    Callback->Context  = Context;
    ExInitializePushLock(&Callback->Lock);
  }

  return Callback;
}

Devam edersek bu nesnenin ilklenmesinden sonra sıra bildirim rutinimizin sistem yapılarına eklenmesine, yani bizim için kritik olan kısma geliyor.

lea     rcx, PspLoadImageNotifyRoutine  ; Rutinleri tutan yapı bu!
xor     r8d, r8d
lea     rcx, [rcx+rbx*8] ; rcx = PspLoadImageNotifyRoutine[rbx*8]
mov     rdx, rdi ;rdi = rdx = Callback nesnemiz
call    ExCompareExchangeCallBack ; Eklemeyi dene bakalım
test    al, al ; Boş muydu? Ekleyebildik mi?
jz      NEXT_SLOT ; Hayır :( Sonraki slota geçelim
lock inc cs:PspLoadImageNotifyRoutineCount ; Evet! Ekledik, o zaman toplam eklenen sayısını arttır
mov     eax, cs:PspNotifyEnableMask
test    al, 1 ; En az bir bildirim rutini var mıymış önceden?
jnz     short loc_140597CF9 ; Varmış sorun yok.
lock bts cs:PspNotifyEnableMask, 0 ; Yokmuş! Ama artık var!

Evet, bütün kritik işlemin yapıldığı kısım işte bu kadar basit aslında. Burada ne öğrendik? PspLoadImageNotifyRoutine isimli ve her bir girdisi 8 bayt olan bir dizi var. Bu dizi çalıştırılabilir dosya bildirimleri için kayıt edilen rutinleri tutuyor. Burada sırayla tüm dizi sırası geziliyor ve bu rutinin daha önce eklenip eklenmediği kontrol ediliyor, eğer eklenmediyse ve rutin limitini aşmadıysak diziye ekleniyor.

En sonda ise PspNotifyEnableMask isimli bir değişken ile birtakım şeyler yapılıyor. Bu değişkeni incelediğimizde görüyoruz ki minik bir performans iyileştirmesi için eklenmiş. Ne yapıyor derseniz, şöyle ki sisteme bir işlem, işlemcik ya da çalıştırılabilir dosya rutini eklenirse bu değişkenin duruma göre farklı biti 1 yapılıyor. Daha sonra bu rutinlerin çağırıldığı yerlerde önce bu bit kontrol ediliyor eğer 1 ise demek ki sistemde çağrı var deyip çağırma işlemini yapıyor, eğer 0 ise yok eklenmemiş deyip birkaç satır kod çalıştırmaktan kurtulmuş oluyor sistem. Mesela bizim örneğimizde çalıştırılabilir dosya rutinleri için 0. bit kullanılıyor. Normal işlem rutinleri için 1. bit, Ex olanlar için 2. bit, normal işlemcikler için 3. bit, Ex olanlar için 4. bit kullanılıyor bilginiz olsun…

Bu kod parçacığında bir diğer dikkate değer fonksiyon ise ExCompareExchangeCallBack. Bu fonksiyonun yaptığı şey ona verilen dizi sırasındaki değeri belirtilmiş fonksiyon ile karşılaştırmak. Eğer aynısını bulursa 0’dan farklı bir değer dönerek bunu belirtiyor.

Diğer bildirim rutinlerinin eklenmesi, çağırılması ve silinmesi de üç aşağı beş yukarı buradaki gibi yapılıyor. Fakat okuyuculara tavsiyem diğerlerini de kendilerinin incelemesi. Bu sayede hem pratik yapmış da olursunuz.

Son olarak bu fonksiyonu da yukarıdaki disassembly çıktısına dayanarak tekrar inşa etmeye çalışalım. Tam olarak aynısı olmasa da bu fonksiyonun aşağı yukarı şöyle bir temelde olduğunu rahatlıkla söyleyebiliriz:

NTSTATUS PsSetLoadImageNotifyRoutineEx(PVOID NotifyRoutine, ULONG_PTR Flags)
{
  ULONG Count;
  PEX_CALLBACK_ROUTINE_BLOCK Callback; 
  NTSTATUS NtStatus = STATUS_INVALID_PARAMETER_2; 

  if ( Flags & 0xFFFFFFFFFFFFFFFE )
  {
      goto RETURN;
  }
  
  Callback = ExAllocateCallBack(NotifyRoutine, Flags);
  if ( Callback == NULL )
  {
      NtStatus = STATUS_INSUFFICIENT_RESOURCES;
      goto RETURN;
  }
  
  Count = 0;
  while ( !ExCompareExchangeCallBack(&PspLoadImageNotifyRoutine[Count], Callback, 0) )
  {
      Count++;
      if ( Count >= 64 )
      {
          ExFreePoolWithTag(Callback, 0);
          NtStatus = STATUS_INSUFFICIENT_RESOURCES;
          goto RETURN;;
      }
  }
  
  InterlockedIncrement(&PspLoadImageNotifyRoutineCount);
  if ( !(PspNotifyEnableMask & 1) )
      InterlockedBitTestAndSet(&PspNotifyEnableMask, 0);
  
  NtStatus = STATUS_SUCCESS;

RETURN:
  return NtStatus;
}

Peki bu bildirim rutinleri nereden çağırılıyor? Hemen bakalım! Bunun için yapmamız gereken tek şey PspLoadImageNotifyRoutine dizisine olan referanslara bakmak. Ben bunu yaptığımda PsCallImageNotifyRoutines isimli bir fonksiyona ulaştım. Bu fonksiyonun yaptığı şey yukarıda bahsi geçen diziyi gezerek buradaki rutinleri tek tek çağırmak. Bu bildirimleri çağıran fonksiyon ise MiDriverLoadSucceeded ve MiMapViewOfImageSection fonksiyonları içerisinden çağırılıyor, ki bu da oldukça manidar sanıyorum….

Bu arada bu referanslara bakarken PspEnumerateCallback isimli bir fonksiyon dikkatimi çekti. Windows 7’de böyle bir fonksiyonun olmadığına eminim az önce baktım. Bu fonksiyon ilginçmiş. Lakin bunu çağıran fonksiyon bir isme sahip değil. Daha da ilginci devasa büyük bir fonksiyon tarafından çağırılıyor. Aklıma direkt olarak PatchGuard geldi açıkçası. Neden derseniz, PatchGuard‘ın fonksiyonları işletim sistemi çekirdeğindeki en büyük fonksiyonlar. Boyutları gerçekten aşırı büyük, bu sayede hemen bulunabiliyorlar ama incelemesi de inanılmaz zor… Biraz eğlenmek isteyen dostlarımız bir göz atabilir :) Bu fonksiyon da karşıma çıkınca dayanamadım. Evet, isminden anlaşıldığı gibi bu fonksiyon ile sistemdeki bildirim rutinlerine ulaşabiliyorsunuz. Geçen sene yazdığım anti-rootkit’de ben taklalar atarak ulaşıyordum bunu öğrendiğim güzel oldu… Olur da o projeye devam edersem şüphesiz kullanacağım.

Bu arada dayanamayıp fonksiyonu inceledim ve tekrar oluşturdum, o da şöyle bir şey oluyor:

BOOL PspEnumerateCallback(DWORD Type, DWORD *Index, PVOID *Callback)
{
  PVOID CallbackArray; 

  switch(Type)
  {
  	case 0:
  	    CallbackArray = &PspCreateThreadNotifyRoutine;
  	    break;
  	case 1:
  	    CallbackArray = &PspCreateProcessNotifyRoutine;
  	    break;
  	case 2:
  	    CallbackArray = &PspLoadImageNotifyRoutine;
  	    break;
  }

  if ( *Index < 64 )
  {
    *Callback = &CallbackArray[*Index];
    *Index++;
    return TRUE;
  }

  return FALSE;
}

Bu durumda Type parametresine 0 verirseniz PspCreateThreadNotifyRoutine; 1 verirseniz PspCreateProcessNotifyRoutine, 2 verirseniz PspLoadImageNotifyRoutine ile eklenen bildirimleri gezebilen bir kod parçasına sahip olabiliyorsunuz. E tabi bunu yapmak bu fonksiyon olmadan çok mu zor? Yoo. Gayet kolay. Sadece dışarıdan gizlenen bazı değişkenlerinin adreslerini bulmanız gerekiyor. Ama yine de bu yeni yöntem bence çok daha uygun.

Sistem Kapanma Bildirim Rutini

Bu bildirimleri sürücülerde sistemin kapanacağı sırada yapmamız gereken işlemler var ise kayıt ettiriyoruz. Bunun için IoRegisterShutdownNotification isimli bir fonksiyonu kullanıyoruz.

NTSTATUS IoRegisterShutdownNotification(
  _In_ PDEVICE_OBJECT DeviceObject
);

Gördüğünüz gibi bu fonksiyona parametre olarak sürücümüzün aygıt nesnesini veriyoruz. Bunu yaptığımız zaman bu fonksiyon bizim aygıt nesnesini IopNotifyShutdownQueueHead isimli bir listeye ekliyor. Bu listeye eklenen tüm aygıt nesneleri IoShutdownSystem fonksiyonu çağırıldığı zaman IRP_MJ_SHUTDOWN Irp’si alıyor. Bu sayede sistemin kapanmakta olduğu bilgisini almış oluyorlar. Tabi bu olayın çalışması için sürücünüzde bu Irp’yi işleyecek fonksiyonu eklemeniz gerektiğini söylemeye gerek yok sanırım…

Bu bildirimler dışında aslında kayıt defteri için, dosyalar, ve bir çok nesne için de benzer mekanizmalar mevcut. Fakat başlangıç için bunların yeterli olduğunu düşünüyorum. Aslında buradaki tüm rutinlerinin sistem seviyesinde analizini yapmayı düşünüyordum fakat ilk başta sahip olduğum motivasyon şu anda baya azaldı. O nedenle biraz kısa tutuyorum yazıyı. Aklınıza takılan sormak istediğiniz herhangi bir şeyde elbette yorumları kullanabilirsiniz. Ayrıca dediğim gibi, kafam motivasyon odaklı çalıştığı için durduk yere yazı yazmak biraz zor oluyor -ki, aslında durduk yere de sahip olduğum bağzı motivasyonlar var, ama bu başka bir konu hehe-. Bu nedenle anlatılmasını istediğiniz konuları belirtebilirsiniz. Eğer bilgim varsa onunla ilgili yazı yazmaya çabalarım….

Sevgiler