Code Injection und dynamische String Decryption

Bei der Analyse eines Programm sties ich heute Mittag auf eine String-Encryption der wildesten Art. In dem heutigen Post wird es um die Entschlüsselung dieser String-Encryption mit Mitteln der Code-Injection gehen.

Bei der String-Decryption wurde ein Offset der verschlüsselten Daten in EBX übergeben und im Gegenzug spucke die Funktion den entschlüsselten String in EAX wieder aus. Eine solche Entschlüsselung wurde in dieser Form schon oft gesehn und ist weiter nichts besonders. Folglich vermutete ich ein XOR decrypt in irgend einer Art. Daher staunte ich nicht schlecht, als ich mit einer unglaublich langen und mit mehreren Subfunktionen durchzogenen Methode stand. Eigentlich baut man die String-Decryption nach, aber in diesem Fall gab ich es nach 20 Minuten auf. Aus der Adresse (!) des Offsets wurden nach Bitshifting-Operationen Offsets in andere Speicherbereiche errechnet, die wiederrum mit in die Decryption einflossen. Diese Speicherbereiche wurden wiederrum erst zur Laufzeit berechnet etc etc. Das dumpen dieser ganzen Daten stellte sich als schwierig herraus, daher verfolgte ich einen anderen Ansatz: Code Injection!

StringDecryptDie Idee dahinter ist schnell erklärt: Anstatt dass man die String-Decrypt-Funktion rekonstruiert um sie im Programm zu verwenden, ruft man die Decrypt-Funktion direkt im zu crackenden Programm auf. Man schreibt also Shellcode der nichts anderes macht als einen String zu entschlüsseln. In diesem Fall ist der Umfang des Shellcodes beschränkt, denn wir müssen nur den gewünschten Offset in EBX laden, die Funktion aufrufen und das Resultat aus EAX auswerten. Uns werden dabei noch einige Steine in den Weg gelegt, aber über die steigen wir nach und nach drüber. Fangen wir erstmal damit an, wie der Shellcode überhaupt ins Programm gelangt und ausgeführt wird.

Die meinst so verhasste Windows API bietet in diesem Fall drei wichtige Funktionen an, die wir uns heute zu nutze machen: WriteProcessMemory. CreateRemoteThread und VirtualAllocEx. Öffnet die verlinkten Funktionen am besten im Browser, denn die Parameter haben es in sich. Um den Shellcode überhaupt erst ins Programm zu bekommen müssen wir Speicherplatz reservieren. Dies geschieht über die VirtualAllocEx-Funktion, die in der Extented-Version (Daher das Ex am Ende) ein Handle zu einem Prozess übergeben bekommt. In Klartext bedeutet das einfach, dass wir in einem fremden Prozess über dessen Prozesshandle Speicherplatz reservieren können. Wo genau der Speicherplatz landet interessiert uns nicht, denn wir bekommen ja einen Zeiger auf ihn zurück. Im nächsten Schritt schreiben wir den Shellcode über WriteProcessMemory an den eben reservierten Speicher. Die WriteProcessMemory ist aus vielen Spielehacks und Trainern bekannt, denn so kann man Speicher eines fremden Prozesses auslesen und manipulieren. Zu guter letzt müssen wir den Shellcode noch aufrufen, und das passiert über die CreateRemoteThread Funktion. Diese erstellt einen neuen Thread im Zielprogramm, welcher den Shellcode ausführt. Die drei oben genannten Funktion finden im übrigen auch Anwendung bei RunPE Modulen, welche im Prinzip nichts anderers machen als wir.

Den Shellcode müssen wir in Byteform vorliegen haben, daher schreiben wir einfach mal mit OllyDBG drauf los. Der entsprechende Assemblercode sieht so aus:

1. Offset laden
MOV EBX, 0xEE21B8 (Entspricht verschlüsseltem String)
2. String-Decrypt-Methode aufrufen
CALL 00EBD710
3. EAX in einer Messagebox ausgeben (4 Argumente, die mitteren beiden sind Strings)
PUSH 0
PUSH EAX
PUSH EAX
PUSH 0
call MessageBoxA

Wenn wir mit OllyDBG den EIP (Instruction Pointer) auf unseren geschriebenen Code setzen klappt alles, wir bekommen eine Messagebox mit dem entschlüsselten String ausgegeben. Und links von den ganzen Opcodes steht direkt dieser Assembler-Code in Shellcode-Form. Also ab damit in unser Programm! Und wieder erwarten crasht das Zielprogramm sofort :/ Hier muss man sich in Erinnerung rufen, dass der Shellcode immer an einer anderen Stelle ins Programm geladen wird. Und im Assembler-Code auf Byte-Ebene werden Funktionsaufrufe relativ zur aktuellen Position angegeben. Dies erspart oftmals Code indem man keine kompletten 4 Byte Funktionsadressen angeben muss, sondern mit einer 2 Byte-Addition zurechtkommt. In diesem Fall ist es für uns doof, denn so landen wie überall, nur nicht in der richtigen String-Decrypt-Funktion. Die Lösung ist zum Glück relativ einfach: Wir callen die Funktionen nicht direkt, sondern laden zunächst die absoluten Adressen in ein nicht genutztes Register. Im Anschluss springen wir die Adresse in diesem Register an. Unser modifizierter Shellcode ließt sich etwas länger, funktioniert aber!

1. Offset laden
MOV EBX, 0xEE21B8 (Entspricht verschlüsseltem String)
2. Funktionsoffset in EAX laden und String-Decrypt-Methode aufrufen
MOV EAX, 00EBD710
CALL EAX
3. EAX in einer Messagebox ausgeben (4 Argumente, die mitteren beiden sind Strings)
PUSH 0
PUSH EAX
PUSH EAX
PUSH 0
mov ECX, 76EA279E (MessageBoxA auf Win 8)
call ECX

Stellt sich schon das nächste Problem in den Weg: Das zu crackende Programm schließt sich immer sofort, so dass wir garnicht in Ruhe unseren Code injecten können. Hier greifen wir zu einem kleinen Trick, der sich “EBFE” nennt. 0xEBFE ist der Bytecode , der einen Sprung auf sich selbst repräsentiert. Eine Endlosschleife, die immer an die vorherige Anweisung springt! Dieses fügen wir über den Debugger ein und das Programm läuft in einer Endlosschleife nebenher. Da wir in der Code-Injection in einem anderen Thread arbeiten stört uns auch nicht, dass der Hauptthread beschäftigt ist. Nächstes Problem aus der Welt!

Zuletzt noch eine Sache, die uns das Entschlüsseln automatisiert. Aktuell bekommen wir eine Messagebox ausgegeben, aber wie liefern wir uns den entschlüsselten String per Sourceode zurück? CreateRemoteThread erwartet einen Parameter, der sich wiederrum “lpParameter” nennt. Dadurch können wir einen Zeiger übergeben, der bei unserem Shellcode auf dem Stack landet. Wenn wir also den Wert von EAX und damit den verschlüsselten String sichern wollen, müssen wir die Adresse an diesen Parameter schreiben. Wir reservieren also nochmals 4 Byte Speicher im Programm und übergeben diese Adresse per lpParameter. Der Shellcode sollte nun den Wert von EAX (4 Byte, Zeiger auf String) in den von uns reservieren Speicher schreiben. Kein Problem!

1. Offset laden
MOV EBX, 0xEE21B8 (Entspricht verschlüsseltem String)
2. Funktionsoffset in EAX laden und String-Decrypt-Methode aufrufen
MOV EAX, 00EBD710
CALL EAX
3. EAX an den Speicher in lpParameter schreiben
MOV ECX,DWORD PTR SS:[ESP+4] => lpParameter aus Stack in ECX schreiben
MOV DWORD PTR DS:[ECX],EAX => EAX nach ECX schreiben
RETN

Nun müssen wir im Decrypter-Programm nur noch die 4 Byte an der lpParameter-Adresse auslesen, um an den String zu kommen. Hier lesen wir solange Daten, bis wir auf ein Nullbyte stoßen. Fertig ist der dynamische Decrypter!

Zum Abschlus gibt es noch eine C# Implementation von der ganzen Theorie. Inklusive Shellcode von knackigen 19 Bytes:

public static String GetDecryptedStringForOffset(Int32 offset)
{
    // Convert the integer to a byte array
    var bytesOffset = BitConverter.GetBytes(offset);
 
    var asm = new byte[]
    {
    0xBB, 0x20, 0x8E, 0xEE, 0x00, 0xB8, 0x10, 0xD7, 0xEB, 0x00, 0xFF, 0xD0, 0x8B, 0x4C, 0xE4, 0x04, 0x89, 0x01, 0xC3
 
    };
 
    // override the EBX byte offset in the shellcode
    asm[1] = bytesOffset[0];
    asm[2] = bytesOffset[1];
    asm[3] = bytesOffset[2];
    asm[4] = bytesOffset[3];
 
    int iProcessId = Process.GetProcessesByName("crackme_1")[0].Id;
    IntPtr hHandle = OpenProcess(ProcessAccessFlags.All, false, iProcessId);
 
    if (hHandle == IntPtr.Zero)
        throw new ApplicationException("Cannot get process handle.");
 
    IntPtr hAlloc = VirtualAllocEx(hHandle, IntPtr.Zero, (uint)asm.Length, AllocationType.Commit, MemoryProtection.ExecuteReadWrite);
 
    if (hAlloc == IntPtr.Zero)
        throw new ApplicationException("Cannot allocate memory.");
 
    IntPtr hRetAlloc = VirtualAllocEx(hHandle, IntPtr.Zero, 4, AllocationType.Commit, MemoryProtection.ExecuteReadWrite);
 
    if (hRetAlloc == IntPtr.Zero)
        throw new ApplicationException("Cannot allocate memory.");
 
    UIntPtr bytesWritten = UIntPtr.Zero;
 
    if (!WriteProcessMemory(hHandle, hAlloc, asm, (uint)asm.Length, out bytesWritten))
        throw new ApplicationException("Cannot write process memory.");
 
    if (asm.Length != (int)bytesWritten)
        throw new ApplicationException("Invalid written size.");
 
    uint iThreadId = 0;
    IntPtr hThread = CreateRemoteThread(hHandle, IntPtr.Zero, 0, hAlloc, (IntPtr)hRetAlloc, 0, out iThreadId);
 
    if (hThread == IntPtr.Zero)
        throw new ApplicationException("Cannot create and execute remote thread.");
 
    WaitForThreadToExit(hThread);
 
    int bytesRead = 0;
 
    byte[] buffer = new byte[1024];
    var bufferDeref = new byte[4];
 
    // Read the 4 Byte lpParameter Adresss
    ReadProcessMemory((int)hHandle, (int)hRetAlloc, bufferDeref, bufferDeref.Length, ref bytesRead);
 
    // Convert it to a Int32
    var actualAddr = BitConverter.ToInt32(bufferDeref, 0);
 
    // Read the actual decrypted String
    ReadProcessMemory((int)hHandle, (int)actualAddr, buffer, buffer.Length, ref bytesRead);
    var StringD = Encoding.ASCII.GetString(buffer);
 
    CloseHandle(hThread);
    CloseHandle(hHandle);
 
    return StringD.Remove(StringD.IndexOf('\0'));
}

7 people like this post.
  1. Noch keine Kommentare vorhanden.

  1. Noch keine TrackBacks.