Selamlar. Uzun zamandır yazı yazmamamın(writeuplar hariç) nedeni aslında bu yazıydı. Bir süredir .NET Framework ile yazılmış bir yazılımın JIT ile iletişime geçişini ve bunun “hooklanması” hakkında çalışıyordum. Sonunda bu yazı ortaya çıktı.
Yazıma geçmeden önce dnSpy’ın, .NET dosyalar üzerinde çalışırken bizim işimize en fazla yarayacak yazılım olduğunun ve işini en iyi yapan .NET Decompiler/Debugger olduğunu kalın harflerle yazmak istiyorum.
Yazı Amacı
Bu yazıda bir JIT hooker yazmayı planlıyoruz (C#, C/C++ kullanarak). Peki bunu neden yapıyoruz?
Bizim amacımız aslında korumaya yönelik (Anti-Tamper)… .NET Framework ile yazılmış bir dosyayı dnSpy gibi decompile yazılımları ile basitçe decompile/ildasm edebiliriz ama bu her zaman kusursuz şekilde olmaz. Hedef yazılımın üzerinde bir koruma varsa (örn. ConfuserEx) ve bu korumanın da method yapısını bozmak gibi bir özelliği varsa (Anti-Tamper vb.) dnSpy dosyayı decompile edemez çünkü method IL’leri bozulmuş olacak.
Anti-Tamper Örneği
dnSpy dosyayı okuyamıyor çünkü methodlar bozuk. Uygulamanın düzgün şekilde çalışabilmesi için modül constructor’ı olan .cctor noktasında fonksiyonları düzeltmesi gerekiyor.
Tabii ki her korumada böyle olmak zorunda değil örneğin her class’ın kendi içeriğini, class’a ait constructor’da da çözdürebilir burada anlatmak istediğim nokta; çalışacak ilk noktada düzeltmesi gerektiği.
Anti Tamper olayının daha iyi kavranması için ConfuserEx üzerinde bir örnek göstereceğim : Bir uygulamamız var. Bu uygulamanın “Main()” fonksiyonunu decompile etmek için dnSpy üzerinden modüle sağ tıklayıp entry point noktasına gidiyorum.
Entry Point noktasında hiçbir şey gözükmüyor? Bir de fonksiyon adına sağ tıklayıp Edit Method Body… seçeneğinden IL kodlarına bakmayı deneyelim.
:D Program normal bir şekilde açılıp çalışıyor. O halde bu programın Main() fonksiyonundan dahi önce çalışan bir fonksiyonda program fonksiyonlarını düzelten bir fonksiyon çalışıyor.
Bu da Constructor olacak o hâlde noktam .cctor hemen modüle sağ tıklayıp .cctor noktasına gidelim.
(Anti-Tamper korumasının detaylı analizini farklı bir yazıda yaparız şimdilik kalsın). Eğer fonksiyon çalıştıktan hemen sonra bütün fonksiyonlar düzeliyorsa normal olarak decompile edilecek anlamına geliyor.
Yapacağımız şey belli dnSpy üzerinde tespit ettiğimiz antitamper fonksiyonuna breakpoint atalım ve o breakpoint çalıştığı zaman “Step Over(F10)” seçeneğinden devam edelim. Debugger tekrar duracak bu aşamada “Modules” sekmesinden programı dumplayalım.
Anti-Tamp fonksiyonu bütün fonksiyonları eski haline çevirmiş. Ancak bu aşamada bir problem var dosya çalışmayacaktır. Bunun nedeni anti-tamper fonksiyonunu silmemiş olmamız. Dosya çalıştığı zaman method body’leri alakalı keylerle tekrar aksiyona sokunca method bodyler tekrardan bozuluyor bu sefer yazılım exception’a düşüp kapanıyor. Bunun için cctor’da çalışan fonksiyonu noplayıp kaydedebiliriz burayı geçiyorum.
API Hooking
API : Application Programming Interface, işletim sisteminin kendisi ile iletişime geçmek isteyen uygulamalara sunduğu bir dizi fonksiyona verilen isimdir.
API Hooking ise iletişim kurmak istediğimiz API ile uygulama arasına girerek API’ye giden tüm istekleri, yeniden programımız içerisinde yazdığımız API’yi birebir taklit eden sahte fonksiyona yönlendirmek oluyor. Bunu örnekler ile daha net anlatacağım.
Örnek
Bunu daha net kavramak için daha kolay bir örnek üzerinden API Hooking’i göstereyim. Bu örnekte “user32.dll” içerisinde bulunan “MessageBoxA” API’sini hooklayacağız.
Bir Hooking Nasıl Gerçekleşir?
API Hooker yazarken izleyeceğimiz yol şu şekilde olacak:
Öncelikle hooklayacağımız API’yı tamamen taklit edebilecek sahte bir fonksiyon yazalım. “MessageBoxA” API’sini taklit edecek fonksiyonu yazabilmek için API’nin MSDN sayfasına giderek fonksiyonu inceleyelim.
Bunu beraber yapalım öncelikle buradan MSDN dökümantasyonuna gidelim.
Bunu Visual Studio üzerinde açtığım projede tekrar yazıyorum.
Fonksiyonu yazmadan önce #include <iostream> #include <Windows.h> satırlarını eklemeyi unutmayalım.
Fonksiyonumuz tamamdır. Gelen argümanları bana bildirmesi için
Satırlarını da ekleyip bütün işi bitiriyorum.
Sahte fonksiyonumuzu yazdık şimdi hooklayacağımız API’nin process’imiz üzerindeki adresini tespit edelim.
Bunu ve gerisini manual hooking yaptıktan hemen sonra yapacağız.
Buraya kadar her şey tamam. Şimdi yapacağımız şey byteları çalışma zamanında düzenlemek. API’nin adresini almıştık dolayısıyla API ile iletişime geçildiği zaman sürecin nerden yönetildiğini biliyoruz. Yapmamız gereken tek şey okunan byteları editleyerek süreci kendi fonksiyonumuza “jumplatmak” (atlatmak) jmp.
Değiştireceğimiz byteları yedeklemek (bir byte array e kopyalamak).
Hemen nedenini de açıklayalım. Yazdığımız taklit fonksiyon (bizim isteğimize göre) gerçek API ile iletişim kurması gerektiğinde sürekli patchlenmiş API fonksiyonuna geleceği için kendi içerisinde sonsuz döngüye girecek. Bunu engellemek için gerçekten iletişime geçmesi gerektiğinde hook işlemini kaldırmamız lazım yani orjinal byteları yerine yazmamız lazım ki işini yerine getirebilsin.
Manual Hooking with x32dbg
İzleyeceğimiz bütün yol bu kadar. Şimdi daha iyi anlaşılması için x32dbg ile örnek bir dosya üzerinden manual hooking işleminin nasıl yapıldığına bakalım.
Öncelikle yazdığımız dosyanın kaynak kodu bu şekilde :
x32dbg üzerinde dosyayı çalıştırdıktan sonra entry noktasına gelince kesilecektir.
Eğer kesilmezse Symbols sekmesinden .exe modülünü seçip “_main” fonksiyonuna gidilebilir.
ilk olarak gidip sahte fonksiyonumuzun adresini kopyalayalım. “Symbols” sekmesinden “sample”.exe modülünü seçip sağ kısma fonksiyonumun ismini yazıp aratıyorum ve çıkan sonucun adresini kopyalayıp bir yere not ediyorum.
“Symbols” sekmesine tekrar gidip solda görünen modüllerden “user32.dll” i seçiyorum sağa bu modülün içerdiği export fonksiyonları gelecektir. Buradan MessageBoxA fonksiyonuna gidelim.
Burda gördümüz ilk satırı jmp 0xADRES şeklinde düzenleyeceğiz ancak bu düzenleme işlemini dump üzerinde yapacağız.
Neden Assemble edip düzenlemiyoruz da dump üzerinden düzenliyoruz? Çünkü hem otomatize hooker yazarken bunu yapacağımız için hem de yapıyı tamamı ile bozacağı için. Bunu kendiniz de deneyebilirsiniz eğer direkt olarak jmp opcode’unu koyup operand kısmına da adresimizi yazarsanız 1. atlatmada problem çıkmayacak ancak düzenlemeye çalıştığınızda iyice şaşacak disasm kısmı…
jmp 0xADRES şeklinde düzenlemek yerine push 0xADRES ret şeklinde düzenleyeceğim neden?
Uygulamam tam olarak MessageBoxA fonksiyonunun adresinde durunca (bp atıyoruz buraya öncesinden) MOV EDI, EDI satırına sağ tıklayıp dökümde takip ediyoruz.
Bu arada sağ kısımda stdcall(winapi çağırılırken kullanılan) MessageBoxA fonksiyonuna gönderdiğimiz argümanları stack üzerinde görebilirsiniz.
Önce buraya gidip istediğimiz asm satırlarının byte karşılığını alalım.
0: 68 10 1c e9 00 push 0xe91c10 5: c3 ret
68 10 1C E9 00 C3
Yalnız buraya dikkat! Düzenleme işlemine geçmeden önce ne kadar byte düzenleyeceksek orjinal yedeğini almamız gerekiyor. Bizim düzenleyeceğimiz byte sayısı 6 byte dolayısıyla ilk 6 byte’ı yedekleyelim.
8B FF 55 8B EC 83
Düzenleyeceğimiz kadar byte seçip CTRL+E kombinasyonu veya resimde gördüğümüz yolu takip ederek edit panelini açalım.
Hex kısmında en sola tıklayıp kopyaladığımız byteları yapıştıralım.
Satırımız bu hale dönecek. Şimdi bir kere devam ettiriyorum start buttonundan breakpoint’e tekrar çarpınca program ne çıktı vermiş diye bakıp hook işlemini kaldıracağım.
vuhuuu çıktımız geldi. Şimdi asıl messageBox işini yapabilmesi için byte’ları yedeklediğimiz bytelara yani eski haline döndürelim.
Evet, Manual hooking bu kadardı şimdi işi otomatizeye dökelim…
Geldik yazımızın ana konusuna. Bir .NET yazılımının yapısını tam olarak burda yazamayacağım onu da farklı bir yazıya bırakalım.
Normal API Hooking işleminde hangi yolu izliyorsak aynı yolu izleyerek compileMethod() fonksiyonunu sahte fonksiyonumuza yönlendirip gelen ILBody’i almak ve yazdırmak.
.NET Dilleri .NET bytecode’ları şeklinde compile edilir. Runtime şekilde CLR sanal makinesinde yorumlanır ve JIT ile çalıştırılır. JIT içerisindeki compileMethod() fonksiyonu, gelen ILBody’i native dile çevirmek için kullanılıyor…
Bu işlem eğer hedef uygulamanın framework sürümü 4.0’dan yüksekse “clrjit.dll”, eğer düşükse “mscorjit.dll” içerisinde runtime şekilde yapılıyor. Yazacağımız örnek dosyanın framework sürümü de 4.0’dan yüksek olduğu için işlemlerimizi clrjit.dll üstünden yürüteceğiz.
CFF Explorer yardımı ile clrjit.dll dosyasının hangi fonksiyonları extern ettiğine bakalım detaylı analizi GitHub reposu üzerinden yapacağız. 2 adet fonksiyon extern ediyor “getJit” ve “jitStartup” Hangisini hooklayacağımız konusuna karar verebilmek için repoyu inceleyelim. dotnet/runtime
satırda extern ettiği “getJit” fonksiyonunu 88. satırda da jitStartup fonksiyonunu görüyoruz. jitStartup fonksiyonu void bir fonksiyon iken getJit ICorJitCompiler* döndürüyor. Yapıyı inceleyelim.
compileMethod fonksiyonunu bulduk. Hedefimiz getJit(). Yorum satırlarında yazılmış kısımları da özetleyerek compileMethod’un amacını iyice oturtalım.
compileMethod, JIT derleyicisinden bir method için native kod oluşturmasını isteyen ana fonksiyondur. Derlenecek method ‘info’ parametresinde “CORINFO_METHOD_INFO” struct yapısı olarak geçirilir.
Bu işlem için de izleyeceğimiz yolu yazalım. Yazımızın başında yaptığımız API hooking işleminden birazcık farklı. Burada byte’ları doğrudan düzenlemeyeceğiz. getJit() fonksiyonunun döndürdüğü pointer, bir VTable döndürüyor bizim yapacağımız şey de bu VTable’ın gösterdiği ilk pointer’ı patchlemek, kendi fonksiyonumuzun adresi ile değiştirmek.
Çünkü VTable’daki ilk pointer compileMethod fonksiyonumuzu işaret ediyor.
CORINFO_METHOD_INFO yapısını da bırakalım şöyle:
Local JIT Hook
Adımlarımız :
Clrjit.dll için DLL Import işlemi ve extern ettiği getJit fonksiyonunu almak.
getJit fonksiyonumuzun döndürüğü IntPtr değerini bir değişkene alalım ve içerisinde tuttuğu ilk pointer değerini okuyalım.
Burdan sonrası için önce sahte fonksiyonumuzu hazırlamamız gerekiyor. Öncelikle tamamen taklit edebilecek bir delegate hazırlamamız gerekiyor. Bu işlemleri farklı bir class üzerinde yapacağım.
(SJITHook Kullanılabilir) Bunu yaptıktan sonra kullandığımız bazı şeyler hata verecek hemen struct yapılarını da alalım corjit.h ve corinfo.h içerisinden.
Şimdi class ile işimiz bitti şimdi main kısmına geçip işlemlerimizi yapalım gerekli bütün açıklamalar yorum satırlarında yazıyor ama olur da bir yerde takılırsanız bana ulaşabilirsiniz
Yazım gerçekten uzun olmuş olabilir. Tüm ayrıntılarıyla detaylı bir yazı ortaya çıkartmaya çalıştım. Takıldığınız bir nokta olursa bana her zaman yorumlar kısmından veya Twitter’dan ulaşabilirsiniz. Remote JIT Hooker’ı da çok yakında yayınlayacağım aralarında fazla açıklık olmayacak :D İngilizce versiyonu da yakında…