API Hooking (Yönlendirme, Araya Girme) Nedir ve Nasıl Yapılır?

Merhaba

Uzunca süredir biraz detaylı bir yazı yazmayı planlıyordum. Takip edenler bilir Buffer Overflow konusunun 2. bölümünü yazmayı planlıyordum. Ancak kısa kesmemek ve oldukça detaylandırmak istediğimden ve bunun için de biraz fazlaca boş zaman olması gerektiğinden ikisini birbirine denk getirip konuyu içime sinecek şekilde yazamamıştım. Bu sürede konuyu vakit buldukça hazırlıyordum elbette. Ancak bir talihsizlik sonucu (Biraz benim de hata payım var bu işte) Blog için yazdığım içerik verilerini (yazı, resim vs) talihsiz bir şekilde kaybettim. Tabi o kadar detaylandırıp bitirmek üzere olduğum içeriğin yok olması epeyce moralimi bozdu. O yüzden uzunca bir süre de birşey yazmaya içim gitmedi. Ancak bu süre zarfında hazır boş vakit bulmuşken API Hooking hakkında şöyle kısaca birşeyler karalamak istedim.

Bu yazının konusu API Hooking. API nedir bunları anlatmaya gerek duymuyorum. Bu konuya merağınız varsa, ne olduğunu zaten biliyorsunuzdur. O yüzden bu yazıyı okuyan okuyucuların önceden belli bir seviyede olduğu varsayımını yapıyorum. Çünkü konunun anlaşılması için bu şart. Ancak yine de API ve API Hooking olayının kavramsal olarak ne olduğunu biliyor ama nedir’ini basit anlamda da öğrenmek istiyorsanız bu yazıda yine sizin için birşeyler olacaktır. O yüzden okuyabilirsiniz.

Hooking terimini Türkçe’ye tam olarak nasıl çevirebilirim bilemiyorum. Kancalamak terimi biraz abes olabiliyor. O yüzden ben “yönlendirmek”, “araya girmek” şeklinde tanımlamak isterim.

API Hooking yapmanın birden fazla yolu mevcut. Birçok teknik, yaklaşım mevcut. Örneğin eğer ilgili API’ler bir tabloda tutuluyorsa bu tablo entry’lerini (girdi) replace etmek (değiştirmek). Örneğin spesifik Kernel API’ları için bu yöntem kullanılır. Bu API’lar SSDT (System Service Descriptor Table) tutulur ve bu tablo girdilerini kendimizinkilerle değiştirdiğimizde o API’ları yönlendirmiş oluruz. Yahut IAT Hooking de benzer bir tekniktir. Çalıştırılabilir dosyalar sistem servis API’larını kullanmak için kullanılan API ve Modül bilgilerini IAT (Import Address Table) adı verilen tabloda tutar. Bunlar görece kolay yöntemlerdir. Bir de dinamik kod yükleme, taşıma ve değiştirme tabanlı hooking yöntemleri mevcuttur. Çağrılan fonksiyon komutlarını anlık olarak değiştirme, yahut relocation bu yöntemlerden bir kaçıdır.

Anlatacağım teknik konuyla tam dengede bilgi verebilmesi açısından relocation (Yeniden konumlandırma) tabanlı olacak.

Relocation terimi kasıt, çağrılan fonksiyonu çalışan Process’in adres alanında (Process Space) bir başka noktaya taşıyarak, orjinal lokasyonuna kendi fonksiyonumuzu yerleştirip araya girmeyi ifade eder.

Kod da veri gibi bellek üzerinde ardışık biçimde bulunur. Figür 1.’de örnek bir yerleşim görünüyor.

2

Figür 1

Örnek olarak WriteFile Win32 API fonksiyonunu bellekteki yerleşik düzenini görüyoruz. Aynı şekilde ilgili API fonksiyonuna çağrı yapan kodun yerleşkesi de görülüyor. Kodun nereye yerleştirileceği daha doğrusu nereye yükleneceği İşletim Sistemi’nin inyisatifindedir. Ve tasarımına göre yerleşke adresi değişkenlik gösterebilir.

Bizim bu bölgeyi relocate (Yeniden konumlandırma) etmemiz için orjinal fonksiyonun başlangıç adresini ve bitiş adresini bilmemiz gerek. Bu bilgileri kullanarak fonksiyonun kaç byte uzunluğunda olduğunu da kolaylıkla bilebiliriz.

Kodun uzunluğu ve bitiş noktasını neden bilmemiz gerek? Bilmeliyiz çünkü esas kodu yeniden konumlandırmak için ayrı bir bellek bölgesine ihtiyacımız olacak. Orjinal kodu bizim kontrolümüzdeki bir bellek bölgesine taşıyacağız.

API Fonksiyonunun Adresi ve Uzunluğunu Tespit Etmek

Bu işlem Hooking işinin en temel ve önemli kısmı. Çünkü yanlış yerden ve yanlış uzunlukta kod taşınması demek uygulamanın anında çökmesine sebebiyet verecektir.

API adresini bulmak zor değil. İlgili modülü adres alanımıza yükleyip prosedür adresini bize veren LoadLibrary ve GetProcAddress Win32 API’ları ile kolayca yapabiliriz.

Yukarıdaki kod WriteFile fonksiyonuna ait modülü önce yükleyip ardından, API fonksiyonunun adresini (Function Pointer) verecektir.

Bu kolay kısmıydı. Peki rutin kaç byte uzunluğunda ve nerede bitiyor?
Bu kısım biraz uğraşmamız gereken kısım. Çünkü bunun tespiti için dinamik olarak kodları başlangıç adresinden itibaren analiz etmemiz gerekiyor. Yani disassemble ederek bitiş noktasını bulabilmemiz lazım. x86 ve x64 assembly komutları değişken uzunlukta olduklarından komutları decode etmek (çözmek) için bir önceki komutun (instruction) ne olduğunu anlayabilmek gerekir. Çünkü aynı Opcode birden fazla anlama gelebilmektedir. Bunun ayrımı da ancak her bir komutun tam olarak çözülebilmesiyle olur. Buna yazının ilerleyen kısımlarında değineceğim çünkü bu anlatımda yazdığım kodlar küçük çapta bu yöntemi kullanıyor olacaklar.

Aslında doğru bir şekilde komutları çözümlemek için Disassembler kütüphaneler kullanılması en doğrusudur. Çünkü ne kadar kendimiz disasm rutinleri yazsak da mutlaka eksik kalacaktır. Ben örnek olması için disasm rutinlerini kendim yazdım. Konunun anlaşılması için. Eğer 3. parti disassemler kütüphaneleri isterseniz tavsiyem Distrom library’dir.

Şimdi, bu işi bu yazı için kendimiz yapacağız dediğime göre ilk olarak yapılması gerekenden başlayalım. Bilmemiz gereken ilk şey Win32 API fonksiyonlarının STDCALL calling convention kullanmasıdır. Bu bize fonksiyonunun sonunu tespit etmede bir ipucu verecek. stdcall bilindiği üzere argüman listesini sağdan sola aktarıp, stack’i çağrılan fonksiyonun kendisinin düzenlediği bir yaklaşımdır. Peki bu stack düzenleme nasıl yapılır. Çok çok özel bir API olmadığı müddetçe stack ret x komutu ile düzenlenir. Eğer fonksiyon argüman almıyorsa sadece ret ile geri dönülür.

ret ve ret x tam olarak ne yapar anlatmayacağım çünkü en başta da dediğim gibi bu yazı için bunu zaten biliyor olmanız gerek. İlk iş olarak fonksiyonun başlangıç noktasından itibaren ret veya ret x ‘e denk gelene kadar kod üzerinde dolaşmak.

 

Aşağıda 32 bit WriteFile fonksiyonunun assembly kod dökümünü görüyorsunuz.

ve 64 bit assembly dökümü

Şuan her iki assembly dökümünü çok dikkatli incelemenize gerek yok. İlk aşamada kodun başlangıç ve bitiş noktalarına dikkat etmeniz yeterli. Her iki kodda bazı platform farklılıkları haricinde aynılar.

32 bit dökümde gördüğünüz üzere rutin ret 14h ile sonlanmaktadır. Stack rutin sonunda 20 byte eklenerek düzenlenip çıkılıyor (Hex: 14 , Onluk Düzen: 20 byte).

WriteFile API prototip’ine baktığımızda

32 bitte her bir argüman 4 byte uzunluğunda ve 5 adet argümana sahip. Yani 20 byte.
64 bitte doğrudan bir ret görüyoruz. Bunun sebebi x64 platformunda parametre aktarımının stack yerine registerlar aracılığı ile yapılmasıdır. Yani fastcall calling convention. Bu konuda biraz daha bilgi almak isterseniz https://msdn.microsoft.com/en-us/library/ms235286.aspx adresine göz atabilirsiniz.

Buradan çıkaracağımız sonuç x86 veya x64 için geçerli olmak üzere, fonksiyon bitimini bu komutları tespit ederek sağlayabileceğimiz. Bunun için de her iki komutun opcode değerini bilmemiz gerek.

RET komutunun opcode değeri 0xc3,
RET X komutunun opcode değeri 0xc2’dir

RET tek bytelık bir komut iken,  RET X bir X operandı aldığından ve bu operand 16 bitlik bir relative değer olduğundan 3 byte uzunluğunda bir komuttur.

Öncelikle başlangıç adresini bildiğimiz fonksiyon için bir döngü kurarak bu noktayı bulmaya çalışalım.

Yukarıdaki kod parçası ile adresini aldığımız fonksiyon kodu üzerinde dolaşarak uygun bir ret instruction’ı aramaktadır. Ancak küçük bir ek işlem yapmamız faydalı. Fonksiyon gövdesinde belli bir şarta bağlı olarak rutin sonuna gelmeden de bir RET kodu bulunabilir. Biz sonraki byte’ı da kontrol ederek gerçekten rutin sonuna gelip gelmediğimizi öğreniyoruz. Win32 API fonksiyonlarının son kısımları bellek üzerinde genelde NOP (0x90) ile doldurulmuştur. Yahut bu değer varsayılan bir junk byte  (Genelde 0xcc) ile de doldurulmuş olabir. Yahut zero fill (0x00) halde de bulunabilir. Sonraki byte’ı bu veriler olup olmadığını kontrol etmemiz gerçek anlamda rutin sonunda olup olmadığımız konusunda bize ipucu verecek. Elbette bu çok garanti bir durum değil ancak konu için yaptığım çalışma için şuan yeterli. Geçerli bir ret koduna denk geldiğimizde ret instruction adresinden başlangıç adresini çıkardığımızda bize rutinin boyutunu verecektir. Tabi bu ret adresine artı olarak ret uzunluğunu da ilave etmemiz gerek. Bu değer ret için 1 byte iken ret x için 3 byte’dır. Çünkü ret x komutu ret opcode’u ve 2 byte kadar da stack’i düzenleyen byte miktarı kadar yer demek.

Bu bilgiyi aldıktan sonra bize fonksiyonun kontrolümüzdeki yeni yeri için bellek tahsisatı yapmak gerekecek.

3

Gerekli belleği ise VirtualAlloc API’ı ile yapabiliriz. VirtualAlloc kullanmamızın sebebi bize istediğimiz bellek bölgesinden yahut ona en yakın bellek bölgesinden bellek alanı ayırabilmemizdir.  Çünkü taşınacak bölge orjinal bellek bölgesine olabildiğince yakın tutmamız bizim için avantaj sağlayacak. Ayrıca o bellek bölgesi üzerinde hangi hakların (okuma, yazma, çalıştırma) atanabileceğini belirleyebilmemiz. Bunun için PAGE_EXECUTE_READWRITE sabitini kullanıyoruz. Bu bize hem çalıştırılabilir hem de okuma yazma yapılabilir bir bellek sağlayacaktır.

Bu işlem için yazdığım kod parçası yukarıdaki gibi. Bunu bir döngü içinde yapıyoruz çünkü işletim sistemi ilk seferde istediğimiz bellek bölgesinden bellek tahsisatını uygun görmeyebilir. İlk aşamada orjinal fonksiyondan en az 1024 + rutin kod boyutu kadar geride (Düşük Bellek bölgesi) alan istiyoruz. Eğer başarısız olursak her bir seferinde 1 page kadar (4 kb) eksiltip tekrar deniyoruz taki bize uygun bir bellek verilene kadar. Kodda görüldüğü üzere ek olarak 32 byte daha alan istedik. Sebebi fonksiyonu güvenli şekilde yerleştirebilmek. NOP (No Operation) herhangi bir yan etkiye sahip olmadığından olası bir karışıklıkta herhangi bir çöp veri bulundurmamak.

Bellek bölgesini aldıktan sonra yapmamız gereken orjinal bölgeden ayırdığımız bellek bölgesine rutini taşımak ve orjinal bölgesi NOP ile temizlemek.

Yukarıdaki kod parçası ayırdığımız bellek bölgesini NOP ile dolduruyor ardından orjinal fonksiyon kodlarını bellek bölgemize memcpy ile basitçe kopyalıyoruz. Ardından orjinal bellek bölgesini temizliyoruz. Dikkat edilmesi gereken nokta orjinal bellek bölgesini VirtualProtect ile yazılabilir şekilde ayarlamak. Normalde bu alanlar sadece okunabilir ve çalıştırılabilir durumdadır. Bu ayarlamayı yapmadığımız taktirde işletim sistemi illegal bir işlem yaptığımızı anlayacak ve Access Violation exception’ı fırlatacaktır. Daha sonra bu bölge üzerinde rahatlıkla oynama yapabiliriz.

4

Taşıma işleminden sonra belleğin durumu.

Bu işlem ardından orjinal bellek bölgesini bir zıplama noktası olarak kullanacağız. Normalde diğer uygulamalar halen bu adresi orjinal fonksiyonun adresi olarak göreceklerinden yapılan çağrılar bu adrese düşecektir. Bizim de yapmamız gereken buraya bir zıplama noktası hazırlamak. Bu alan trambolin fonksiyon olarak adlandırılır. Trambolin benzetmesi de bu noktayı zıplama noktası olarak kullanmamızdan ileri gelir. Bu bölgesi trambolin noktası haline getirmek için buradan ayırdığımız yeni fonksiyon adresine zıplama yapan kodu oluşturup yazmamız gereklidir. Aşağıdaki kod parçası ile bu işlemi yapıyoruz. Örnek hem 64 bit hem de 32 bit üzerinde çalışacak şekilde yapmak istediğimden iki durum için de ayrı şekilde bir kod hazırlamamız gerek.

Kod aktif olarak çalışan process’in 64 bit olup olmadığına göre devam ediyor. Bu kod parçasını merak ediyorsanız github sayfamdaki gist kod parçasını inceleyebilirsiniz.

https://gist.github.com/0ffffffffh/24438233cb99e33dde7b

64 bit için doğrudan bir 64 bit mutlak adrese jump yerine yeni fonksiyon adresini 64 bitlik bir register’a yazıp daha sonra o register’a zıplayan kod ile yapacağız. Çünkü bu adres 64 bit uzunluğunda olabilir ve doğrudan 64 bit zıplama yapmamız olası değil. Bunun yerine aynı işi görecek şöyle bir varvasyon kullanacağız.

bu kod da

MOV RAX, 64BIT_ADRES
JMP RAX

şeklinde yapılabilir. RAX, 32 bitlik EAX register’ının 64 bitlik karşılığıdır. Kalan kısımlar ise bildiğimiz x86 komutları ile aynı

MOV RAX, ADRES

komutunun opcode karşılığı 0x48 ve 0xB8 değerleridir. Aslında MOV komutunun opcode değerlerinden bir tanesi 0xB8’dir. ve MOV reg32, imm32 anlamına gelir. Yani 32 bitlik bir register’a 32 bit uzunluğunda bir değer atanacak. Normalde 32 bit içindir. Ancak başındaki 0x48 değeri 64 bit platformda bir prefix değeridir. (0x40 REX, 0x8 W bit) Buna REX prefix adı verilir ve yapılacak işlemin long mode yani 64 bit karşılığı şeklinde yapılmasını söyler.

REX prefix’i ile ilgili biraz daha fazla bilgi almak için

http://wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefix

adresinden faydalanabilirsiniz.  Rex prefix’den sonra MOV opcode’u. Ardından da 64 bit yani 8 byte uzunluğundaki adresi orjinal fonksiyon başlangıç noktasındaki adrese yazdık. Ardından jmp rax komutu için gerekli opcode’u adres+10 byte ötesine yazıyoruz. Bunun sebebi bir önceki komutun toplamda 10 byte uzunluğunda olması 8 byte adres 2 byte da rex prefixli mov komutu.

JMP RAX için opcode 0xff 0xe0

Söz konusu işlem eğer 32 bit ise bunun için de standart bir relative offset’li bir JMP instruction’ı yeterli olacaktır. Bu relative offset değerini bulabilmek için bulunduğumuz bellek adresinden hook fonksiyonumuzun adresini ve artı 5 byte kadar çıkardığımızda bu ofset değerini elde edebiliriz. 5 byte kadar daha ötelememizin sebebi bu değere JMP komutunun uzunluğunun da dahil edilmesi gerekliliği JMP + REL32 komut uzunluğumuz 5 byte kadardır. Offset değerini de bulunduğu noktadan – (negatif) operatörü ile tersine çeviriyoruz çünkü bu değer bulunduğumuz adrese eklenerek hesaplanmaktadır.

Bu offset değerini hesapladıktan sonra JMP (0xE9) opcode ve ardından offset değerini adres üzerine yazarak işin trambolin fonksiyon kısmını halletmiş oluyoruz.

6

Bu işlemden bellek görünümü yukarıdaki figür gibi olacaktır. Orjinal alanı zımplama noktası olarak kullandık. Bu zıpnan nokta da görüldüğü gibi bizim Hook fonksiyonumuzun bellekteki yerleşkesi.

Hook Fonksiyonu

Hook fonksiyonu orjinal API çağrısının yönlendirileceği ve yapılan çağrının kontrolümüze gireceği, onu istediğimiz gibi filtreleyip, değiştirebileceğimiz bir sahte API implementasyonudur. Yukarıdaki figürde trambolin noktasının bu hook fonksiyonumuza işaret ettiği görülebilir. Hook edilmiş bir API çağrıldığında çağrılar şu sırada yapılıyor olacak

7

1. adımda herşeyden habersiz kod WriteFile API fonksiyonunu çağırmak istiyor. O orjinal fonksiyonun bulunduğu adresi doğru nokta varsayarak çağrıyı oraya yapıyor.

2. adımda trambolin noktasına ulaşıyoruz. Herhangi bir şeyi değiştirmeden doğruca Hook fonksiyonumuza zıplıyoruz. Burada farklı şeylerle oynayıp önemli register ve flag değerlerinin değişimine sebep olmamamız gerekiyor. Eğer böyle bir şeye ihtiyaç duyacaksak dahi bu iş için gerekli rutinleri yazıp bellek bölgesine yüklememiz gereklidir.

3. adımda hook fonksiyonumuza giriyoruz. Bu adımda gelen parametreleri isteğimize göre oynayıp elimizde tuttuğumuz bizim kontrolümüzdeki esas işi yapan fonksiyonu çağırıyoruz ve dönüyoruz.

Bu 3 adımdan oluşan işlem sonunda ilgili API başarılı şekilde hook edilmiş olacak.

Peki hook fonksiyonumuz nerede ve nasıl yüklendi?

Hook fonksiyonumuzu hook kodumuzun içine C dili kullanarak yazmış olmamız gerek. Bu sebepten ilgili hook fonksiyonu ne şekilde hedef process’e enjekte edersek o şekilde işletim sistemi tarafından zaten otomatik olarak hafızaya yerleştirilmiş olacaktır. Bu noktadan sonra zaten bu hook fonksiyonun nerede olduğunu bulmak çocuk oyuncağı. Aşağıda WriteFile örneğimiz için bir hook fonksiyon implementasyonu görülüyor.

Yukarıdaki hook fonksiyonu aynen WriteFile prototipine sahip olacak şekilde kendimiz için yeniden yazıldı. Yapılan çağrı trambolin noktasından sonra doğrudan bu noktaya yönlenecektir. Örneğimizde yazılmak istenen verinin başına hook ettiğimizi belirtmek için bir mesaj prepend (Başına ekleme) yaparak orjinal API fonksiyonunu çağırıyoruz. İşin bu kısmı tamamen sizin yapmak istediğiniz şeyle orantılı olarak değişebilir. İstenirse orjinalden farklı bir dosya veya akış üzerine ikincil olarak da yazılabilir.

Problem!

Herşey buraya kadar yolunda ancak kodu yeniden konumlandırırken (relocate) atlamamamız gereken bir nokta mevcut. Eğer hatırlarsanız trambolin fonksiyonu oluştururken bir relative offset kavramından bahsettik. Bu ofset değerinin işlemci tarafından o an çalışan kodun adresi baz alınarak hesaplandığını söylemiştim.

Peki taşıdığımız kod verisi içinde de buna benzer relative offset değerlerine sahip komut serileri varsa ne olacak? Cevabı basit bu ofset değerleri eski fonksiyon yerleşkesini baz alınarak oluşturulduğu için taşınan yeni yerinde geçersiz olacak ve API fonksiyon rutini bambaşka noktalara yönlenecek ya beklediğimizden çok farklı bir sonuç alacağız ya da çok büyük ihtimalle process’in çöktüğünü göreceğiz.

Çözüm?

Bu işin çözümü biraz meşakkatli sayılabilir. Çünkü bu komutları tespit edip bunların da ofsetlerini yeniden hesaplattırmamız şart. Yazımın başında bu işi gerçek anlamda yapmak istiyorsak bir disassemler library kullanmamızın çok yararlı olacağını bu yüzden belirtmiştim. Ancak ben örneğimiz için bunu manuel olarak karşılaşabileceğimiz senaryolara uygun biçimde bulup hesaplattırmak istiyorum. Yaptığım testlerde de birçok API fonksiyonunda gerek 32 bit gerek 64 bit problem çıkmadan başarıyla çalıştığını söyleyebilirim. Problem çıkanlarda ise ufak düzenlemelerle bunların önüne geçmek çok zor değil.

Çözümden önce relative offset kullanabilen ve sıkça rutinlerde kullanılan komutlar neler olabilir bunları bilmek gerekli. Yukarıdaki WriteFile API assembly dökümüne tekrar çıkıp şöyle bir bakarsak birkaç nokta gözümüze çarpacaktır. Ya da siz zahmet etmeyin ben WriteFile’ın 64 bit assembly dökümünü tekrar buraya ekleyeyim.

Yukarıdaki dökümden 6, 9, 13 ve 16. satırlara dikkat vermenizi istiyorum. 3. adet şartlı zıplama (conditional jump) ve bir adet CALL rutin dallanma komutu görmekteyiz. Bu komutlar relative offset ile çalışan komutlardır ve işte biz bu noktaları yeniden hesaplamamız gerekli.

Ancak dikkat! Her instruction farklı biçimlerde farklı adres, operand tiplerine göre çalışabilir ve komut adı aynı olmasına rağmen Opcode değerleri farklı olabilir. Örneğin CALL komutunu ele alırsak,

Bir instruction set referans sitesinden aldığım CALL opcode değerleri.

8

Görüldüğü gibi bir CALL komutunun 8 farklı opcode’u var ve her biri işlemci için farklı bir anlam taşımaktadır. Peki kendi durumumuza gelirsek relative yapılan CALL komutlarını tespit etmemiz gerektiğini biliyoruz. Yukarıdaki WriteFile dökümüne bakarsak 16. satırda bu şekilde bir CALL yapıldığını görebiliriz. Ve solundaki opcode dökümüne bakarsanız E8 ile başladığını görebilirsiniz. Yani bir rel16/32 instruction’ı mevcut. Yine aynı şekilde şartlı dallanma komutları için de bu aynen geçerlidir.

Bu opcode değerlerini başta Intel ve AMD developer reference dökümanları başta olmak üzere birçok siteden küçük bir aramayla bulabilirsiniz. Benim referans aldığım site http://ref.x86asm.net/coder64.html bu adrestedir. Oldukça detaylı ve işe yarar.

Ben bana lazım olan değerleri oluşturup hook kodumun içerisine tanımlamıştım.

9

Şuan için bazı anlamsız gelebilecek tanımlamalar görüyor olabilirsiniz. Onlara ilerleyen kısımlarda değineceğim.

İlk değineceğim şey CALL instruction’ı hakkında. Yukarıdaki tanımlara bakıldığında bir de CALL komutu için MOD RM şeklinde tanımlı bir değer görüyorsunuz. Mod rm adı verilen durum ilgili komutun hangi adres modunda, hangi registerin yahut register yerine bir bellek bölgesi mi kullanılacak onu belirtmek için kullanılan bir bitfield (bit alanı, dizisi)dir. 0xff ile başladığında bunlar dikkate alınarak yorumlanır. Ve her bit aralığının bir anlamı mevcuttur. Böylece gereksiz yere yeni adres modları için opcode ayırmak zorunda kalınmaz. Tek byte’lık bir değer ile hangi adres modunda, hangi registerlar veya bellek bölgesi mi kullanılacak işlemci anlar. Bizim şuan bilmemiz gereken eğer 0xFF mevcutsa sonraki byte’ın kontrol edilerek esasen hangi komutun çalıştırılmak istendiğini bulmak istememizdir.

Mod rm ile ilgili biraz daha bilgi almak isterseniz

http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm adresine uğrayabilirsiniz.

Burada küçük bir hatırlatma, eğer komut tek operand alan bir komut ise MOD RM deki REG bit bloğunun bize çalıştırlacak komut hakkında bilgi sağlaması. Intel developer manual’da bu konuya ilişkin şöyle yazılmıştır.

If the instruction does not require a second operand, then the Reg/Opcode field may be used as an opcode extension.

CALL komutu da tek operandlı olduğundan bu kısmı diğer tek operandlı komutlardan ayırmak için kullanabiliriz. Yukarıda verdiğim linke dikkat ederseniz REG bloğunun 3. ve 5. bitler arası olduğunu görürsünüz. Biz buradaki veriyi alıp binary 11 , (Onluk düzende 7) ile and lediğimizde bize opcode türünü verecektir. Peki neyin ne olduğunu nereden bileceğiz. Bunun için başta referans olarak size verdiğim kaynaktan yararlandım.

10

Görüldüğü üzere hangi değer hangi komuta işaret ediyor görebiliyoruz. Biraz yukarıdaki tanımlama bölümünde de aynı şekilde sabitlerimi tanımladım. Eğer 0xFF prefixi ile karşılaştırsak sonraki byte’ı (x >> 3) & 7 işleminden geçirip bize lazım olan bir instruction olup olmadığını tespit edeceğiz.

Arama yapan döngümüz içine aşağıdaki kod parçasını ekleyebiliriz.

Kodda 0xff değerine denk geldiğimizde sonraki byte’ı anlattığım şekilde kontrol ediyoruz ve eğer bir CALL işlemi ise onu relocation listemize dahil ediyoruz. Dikkatinizi çekmiş olabilir öncesinde bir küçük kontrol mevcut. Her 0xff prefix manasına gelmeyebilir. Örneğin bir 0xff mov komutu için anlamlı bir değerdir. Yani

MOV EDI,EDI komutunda mov operasyonunun edi register’ından edi, register’ına yapılmasını söyler.  Bu komutlarla Win32 API fonksiyonlarının başında karşılaşabiliriz. Bu komutun yaptığı bir iş yoktur sadece iki bytelık bir NOP görevi görür ve Microsoft tarafından geliştirilmiş detours kütüphanesi için kullanılabilecek bir hotpatch noktası olarak işlev görür. Neyse.

CALL kısmını tamamladıktan sonra sırada şartlı dallanmalar mevcut. Bunların da ofsetlerini yeniden hesaplamamız gerektiğini söylemiştik. Şartlı NEAR dallanmalar 2 byte uzunluğunda (two byte opcode) komutlardır. Ve normal JCC dallanma opcode’u haricine başına bir 0x0F değeri alır. Daha sonraki byte ile de hangi tür şartlı dallanma komutu kullanılacaksa ona ait opcode bulunur. Şartlı dallanmaların opcode değer aralığı

0x80 ile 0x8F aralığındadır. Bu aralıkta bir değere rastlarsak bir şartlı dallanma üzerindeyiz diyebiliriz.

döngü içine dahil etmemiz gereken kod parçası yukarıdaki gibi. Demin anlattığım olayın koda yansıtılmış hali. Böyle bir noktaya denk geldiğimizde yine bunu relocation listemize alıyoruz. Daha sonra bunları yeniden hesaplayacağız. addReloc fonksiyonu bu adres ve verileri bir linked list (Bağlı liste)’e ekleyen basit bir fonksiyon.

İşi yapan kod basit bir linked list insert işlemi yapıyor. Orası çok önemli değil. Önemli olan veri yapıları. En başta tanımlı veri yapıları instruction adresi, uzunluğu ve ofset değerini tutmaktadır. Bu bilgi bize lazım olacak.

Yukarıdaki fonksiyon ofseti tekrar hesaplayıp değiştirmektedir. Bu işlemi de önce orjinal fonksiyon ile yeni fonksiyon adresi arasındaki uzaklığı hesaplayıp ofseti ne kadar kaydıracağını bulduktan sonra eski ofset değeri üzerine ekleyerek tamamlıyor.

İkinci nokta geçersiz ofsete sahip komutun fonksiyon içerisinde hangi ofsette bulunduğunu bilmek. Bunun için de komut adresinden orjinal fonksiyon adresini çıkararak değiştireceğimiz komutun gerçekte hangi adreste olduğunu bulacağız. Bu ofseti de hesapladıktan sonra

YENI_FONKSIYON_ADRESI + KOMUT_OFSETI = GERCEK_KOMUT_ADRESI

eşitliği ile değiştireceğimiz adresi bularak yeni ofset değerini üzerine yazıyoruz.

11

Yukarıdaki komut ofsetleri üzerine yeniden konumlandırma yapılmış ve yapılmamış versiyonlarını görüyorsunuz. Orjinal bırakılan fonksiyon gövdesi taşındıktan sonra yanlış bölgeye işaret ederken, diğerinde doğru noktayı gösteriyor. Burada dikkat etmenizi istediğim kırmızı ve mavi dörtgen içine alınmış kısımlar.

Mavi dörtgen içine alınmış komutlar relocation yapılmadığı halde doğru adres alanını gösteriyorlar?

Doğru noktayı gösteriyorlar çünkü orada yapılan şartlı dallanma SHORT (kısa) dallanmadır ve -128 ve +128 aralığındaki kısa dallanmalar için geçerlidir. Bu tür dallanmalarda çok büyük ihtimalle fonksiyon gövdesi içinde yapılacak döngüsel dallanmalarda veya şartlı durumlarda kullanılacağı için onları yeniden konumlandırmaya gerek duymadık. Yazının başlarında bizi ilgilendiren kısmın NEAR JUMP komutları olduğunu belirtmem bu yüzdendi.

Sonuç

Bunları bir araya doğru biçimde getirdiğimizde başarılı bir API Hooking gerçekleştirmiş olacağız. Ben makaleyi hazırlarken örnek olarak toparlayıp örnekte bahsi geçen WriteFile fonksiyonunu hook edip manipüle ettim.

Örnekte bir dosya açalım ve o dosya üzerine bir text verisi girelim. Hook fonksiyonu içinden de bu veriye bizim istediğimiz bir veri ekleyip yazdıralım.

 

Test kodumuz böyle. Önce geliştirdiğimiz hook sistemi ile WriteFile’ı manipüle ediyoruz. Ardından olacaklara bakacağız.

Programı çalıştırdıktan sonra WriteFile fonksiyonun arasına başarıyla girebildik.

13

Ne yazdık, ne bulduk :)

14

Son Söz

API hooking derin ancak eğlenceli bir konu. Bu yazıda bu işin ne olduğu ve nasıl yapıldığı konusunu bir açıdan inceledik. Bir açıdan diyorum çünkü en başta bahsettiğim gibi bu yöntemlerden sadece bir tanesiydi. Ancak bu şekliyle tam işlevsel bir hooking’den bahsedemeyiz. Bunu işlevsel hale getirmek için hedef programa nasıl enjekte etmemiz gerektiği, nasıl farklı bir process’i kontrol altına alabileceğimiz konusu var ki o tamamen başka bir konu. O yüzden o konuyu da bunun devamı sayılacak ayrı bir makalede vakit bulabilirsem değinmek istiyorum.

Çalışmada bahsi geçen kodların tamamına https://github.com/0ffffffffh/Win32ApiHook github repo sayfamdan ulaşabilirsiniz.

Okuduğunuz için teşekkürler. Umuyorum faydalı olmuştur.

6 thoughts on “API Hooking (Yönlendirme, Araya Girme) Nedir ve Nasıl Yapılır?

  1. Çok güzel çok çok güzel! Emeğinize teşekkürler.
    Kod yazmak her zaman makale yayınlamaktan kolay geliyor, keşke hepimiz daha çok yazabilsek (özellikle siz :))

  2. teşekkürler umut. ne kadar bir konuya hakim olsan da makale yazmak, bildiğini anlatmak başlı başına bir işmiş onu anladım. elimden geldiğince açıklayıcı olmaya çalışıyorum. daha çok yazmaya devam ve gayret edeceğim. özellikle okunup yararlanıldığını gördükçe bu konudaki isteğim oldukça artıyor.

  3. Hocam parmaklarınıza sağlık,hooking işlemi benim gibi aciz bir insan için bile kolay anlaşılacak şekilde anlattınız makalelerinizin devamını sabırsızlıkla bekliyorum.Umarım ikinci bölümü bulabilirim.

  4. Hocam emeğinizi elinize sağlık. Hocam bir proje için yardıma ihtiyacım var kısacası şu elimde bir dosya listesi olduğun düşünün bu listedeki dosyalardan herhangi bir tanesi açılma işlemi sırasında, bu dosyayı açan process her ne ise önce durduracak sonra bir karar mekanizması çalışıp çalışmamasına karar verecek. mümkünmüdür? yani yapmak istediğim dosya açılmadan önce dosyayı açan programı askıya almak.

    saygılarımla Erdem

    • Evet mümkün, ancak bunun doğru düzgün yapılabilmesi için kodun kernel modda çalışması gerek. Bu işlemi yapmak içinde bir kernel mode filter driverı yazman gerekir. Daha önce kernel mode kodu yazdıysan https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/file-system-minifilter-drivers buradaki bilgilerden yürüyüp amaçladığın işlemi yapabilirsin. daha önce kernel modda kod yazmadıysan evvela kernel modda kodlama yapmak için o ortamın nasıl işlediğini, kurallarını aşina olman gerekli. filter driverları tam senin amaçladığın işlemler için windows kernel API’ına konulmuş bir sistemdir. bir filter driver yaratırsın. FsRtlRegisterFileSystemFilterCallbacks kernel API ile bir filtre callback register edersin. Windows yapılacak her I/O işlemini register edilen filter driverlardan geçirir, sıra senin driver’ına geldiğinde gerekli kontrolleri yapar, eğer istemediğin bir durum olursa prosesi sonlandırırsın. Eğer maksat belirli bir dosyanın açılmasını engellemekse prosesi sonlandırmak yerine dosya açılmaya çalışıldığı eventi yakalayıp (IRP_MJ_CREATE) eğer karalistedeyse geçersiz durum döndererek prosesin dosya açma işlemini başarısız olarak algılamasını sağlayabilirsin. Yapmak istediğin şey genelde anti-virüs yazılımlarının uyguladığı birkaç teknikten bir tanesi. Amaç proses askıya almadan dosyaya erişim engeliyse bunu User modda da erişim izinleriyle dahi halledebilirsin aslında. Benzer engeller user-modda hooking teknikleri ile de yapılabilir ancak bu durumda proseslere özel olarak çeşitli tekniklerle Remote thread oluşturma, Window Hook API ile hedef prosesin adres alanına girme, hedef prosesi dondurup değiştirilmiş thread context ile devam ettirmek suretiyle adres alanına erişmek gibi birçok yöntemle çalıştığı anda tespit edip erişerek lokal olarak makaledeki teknikleri uygulayarak engellmeye çalışmak olabilir. Dediğim gibi net olarak hedeflenen şeyi ve senin bu konudaki bilgi birikimini bilmediğimden genel hatlarıyla neler yapılabileceğinden bahsettim.

      Kısacası yapılabilir elbette. Ancak bunu başarabilmek için ekstra birşeyler öğrenmek durumunda kalabilirsin. Misal makale bir prosesin adres alanına dahil olduktan sonra yapılacakları anlatıyor örneğin. Prosesin adres alanına girmek için de biraz evvel yukarıda saydığım tekniklerden biri veya birkaçını kullanmak gerekebilir. Hooking daha çok kötü amaçlı yazılımların başvurduğu bir teknik olduğundan bir proje kapsamında bu tür ihtiyaçları en doğal yoldan yapmak her zaman iyidir. Tabi doğal yollarla olmayacak yahut zorluk yaratacak durumda da illegal şekilde zor kullanarak bunları yaptırmaya çalışmak bir seçenek olarak durur elbette.

      • Hocam detaylı anlatımınız için çok teşekkür ederim. Aslında C/C++ hakkında çok fazla bilgim yok ve daha öncede karnel mod iç yazmadım. Genellikle C# kullanıyorum fakat oluşturduğumuz projenin bahsettiğim bölümünün low level ile yazılması kanaatına vardık ve bununla ilgili araştırma yapmaya başladık. ne varki tamamlanma sürecinde ne yeterli döküman ne de yazım işlemini tamamlayacak bir C/C++ yazılımcısına erişebildik. Fakat detaylı anlatımınız en azından bundan sonra ki aşamada bize nasıl bir yol izlememiz gerektiğine dair bir fikir vermiş oldu.

        Tekrar teşekkür ederim İyi Çalışmalar.

Leave a Reply

Your email address will not be published. Required fields are marked *