Keygening / Kryptographie
7.4.2003 BiSHoP's Lockless Keygenme #4 MD5 + RSA


Programmname BiSHoP's Lockless Keygenme #4
Programmtyp Crackme
Programmgrösse 50751 Bytes
Autor Crackme: BiSHoP; Tutorial: figugegl
Tools Softice (Olly) - IDA - RSA Tool - Miracl - IDA Flirt-Signatur für Miracl 4.5
Schwierigkeitsgrad Mittel

Einleitung
Grüezi mitenand

    Fondue Ist Gut Und Gibt Eine Gute Laune!

Im zweiten Teil meiner Miniserie geht es um ein bekanntes, weit verbreitetes Duo: MD5 und RSA. Richtig angewandt, kann daraus ein unbezwingbarer Softwareschutz erstellt werden. Wie gesagt gehe ich in dieser Serie weder auf Theorie noch dahintersteckende Mathematik von RSA ein, sondern beschränke mich auf die Praxis: Wie reverse ich das? Wer genauer Bescheid wissen will, kann oVeRFLoW's RSA-Artikel im Kräcker #15 lesen, oder eines der meiner Meinung nach besten Dokumente in Englisch: How the RSA Cipher Works.

Eigentlich wollte ich ein richtiges Programm nehmen, aber da das erstens illegal ist (*rofl*) und zweitens beim nächsten Update garantiert die Protektion geändert wird, macht ein Crackme mehr Sinn. Es geht ja hauptsächlich ums Verständnis. Meine Wahl fiel auf BiSHoP's Lockless Keygenme #4, welches ich vor ungefähr einem halben Jahr gelöst, und dessen MD5/RSA Algorithmus ich in etlichen Sharewareprogrammen fast unverändert angetroffen habe.

Erste Schritte
Das Crackme ist mit UPX gepackt, lässt sich jedoch mit Bratalarm's UPX-Unpacker oder manuell schnell entpacken. Sogleich fällt uns ein Debuggerschutz auf, wenn das Crackme gestartet wird. In den Strings finden wir folgendes:
    0000B840  BCHK
    0000B848  TRW2000
    0000B850  \\.\TRW2000
    0000B85C  NTICE
    0000B864  \\.\NTICE
    0000B870  Please unload your debugger before running this program.
    0000B8AC  SICE
    0000B8B4  \\.\SICE
Es handelt sich um die altbekannten Meltice- und Boundscheck-Tricks. Am einfachsten neutralisiert man diese Checks mit Icedump, zur Sicherheit können auch alle Strings per Hexeditor umbenannt werden. Wir finden auch noch einen Int68 an Adresse 401731 welcher ausgenopt werden soll, um ungestört mit dem Ollydebugger arbeiten zu können.

Ausserdem finden wir den String "Miracl" und viele verdächtige Funktionsnamen, die auf Bignums und/oder Kryptographie schliessen lassen. Wir disassemblieren die Datei in IDA und applizieren auf gut Glück die Miracl Flirt-Signatur (Version 4.5) vom letzten Tutorial. BINGO - es werden alle wichtigen Funktionen erkannt! Das vereinfacht unsere Aufgabe um einiges. Aber fangen wir doch von vorne an, dort wo unsere Angaben mit "GetDlgItemTextA" eingelesen werden:

    00401551     push 32h
    00401553     push edx
    00401554     push 3EBh
    00401559     push esi
    0040155A     call edi ; GetDlgItemTextA          ; Name einlesen
    0040155C     test eax, eax                       ; Länge ln == 0 ?
    0040155E     jnz short loc_401581                ; nein: springe weiter
        .
    00401588     push 32h
    0040158A     push eax
    0040158B     push 3ECh
    00401590     push esi
    00401591     call edi ; GetDlgItemTextA          ; Company einlesen
    00401593     test eax, eax                       ; Länge lc == 0 ?
    00401595     jnz short loc_4015B8                ; nein: springe weiter
Nach der Eingabe werden Name und Company aneinandergehängt:
    004015CC     push ecx
    004015CD     push edx
    004015CE     call ebx ; lstrcpy                  ; Name kopieren
    004015D0     lea eax, [esp+0E8h]
    004015D7     lea ecx, [esp+0B0h]
    004015DE     push eax
    004015DF     push ecx
    004015E0     call ds:lstrlen
    004015E6     lea edx, [esp+eax+124h]
    004015ED     push edx
    004015EE     call ebx ; lstrcpy                  ; Company anhängen  --> (NameComp)
                
MD5 Hash
Jetzt wird es langsam interessant. Dem Call an Adresse 4015FD werden zwei Argumente übergeben: einen Zeiger auf Name/Company in Register Ecx und Register Eax, welches auf einen ausgenullten Speicherbereich zeigt. Mit F10 führen wir den Call aus, wobei wir uns aber vorher den Speicher mit d eax anzeigen lassen.
    004015F0     lea eax, [esp+2Ch]
    004015F4     lea ecx, [esp+120h]
    004015FB     push eax                            ; ???
    004015FC     push ecx                            ; NameComp
    004015FD     call sub_401170                     ; Berechnung 1
Das Resultat ist ein 32-stelliger String, der eine Hexzahl darstellt. Da wird also irgendwie unser Name in einen String umgewandelt. Wir gucken deshalb genauer in diese Berechnung rein, wobei ich den Code bereits vorgreifend kommentiert habe:
    004011B1     push ecx                            ; Zeiger auf MD5_Context
    004011B2     stosw
    004011B4     call sub_401800                     ; MD5_Init
    004011B9     mov esi, [esp+10h+arg_1A00]
    004011C0     add esp, 4
    004011C3     push esi                            ; NameComp
    004011C4     call ds:lstrlenA                    ; Länge NameComp  --> (lnc)
    004011CA     push eax                            ; lnc
    004011CB     lea edx, [esp+10h+arg_34]
    004011CF     push esi                            ; NameComp
    004011D0     push edx                            ; Zeiger auf MD5_Context
    004011D1     call sub_401830                     ; MD5_Update
    004011D6     lea eax, [esp+18h]
    004011DA     push eax                            ; Zeiger auf MD5-Hash
    004011DB     call sub_4018E0                     ; MD5_Finalize
Im Call an Adresse 4011B4 (MD5_Init) stossen wir auf folgendes:
    00401800     mov eax, [esp+arg_0]
    00401804     xor ecx, ecx
    00401806     mov [eax+14h], ecx
    00401809     mov [eax+10h], ecx
    0040180C     mov dword ptr [eax], 67452301h
    00401812     mov dword ptr [eax+4], 0EFCDAB89h
    00401819     mov dword ptr [eax+8], 98BADCFEh
    00401820     mov dword ptr [eax+0Ch], 10325476h
    00401827     retn
Wer es einmal gesehen hat, vergisst es nie wieder: Das ist die typische Initialisierungssequenz von MD5. Aber gucken wir weiter, ob es sich auch wirklich um MD5 handelt. Wir tracen in jeden Call hinein bis zum eigentlichen Hash-Algo:
    0040197B     mov eax, edi
    0040197D     mov edx, [esp+5Ch+arg_0]
    00401981     not eax
    00401983     mov ecx, ebx
    00401985     and eax, ebp
    00401987     and ecx, edi
    00401989     or  eax, ecx
    0040198B     mov ecx, [esp+5Ch+var_40]
    0040198F     add eax, ecx
    00401991     lea ecx, [edx+eax-28955B88h]
    00401998     mov edx, edi
    0040199A     mov eax, ecx
    0040199C     shr eax, 19h
    0040199F     shl ecx, 7
    004019A2     or  eax, ecx
    004019A4     add eax, edi
       ..
    0040224F     add ebp, ebx
    00402251     lea eax, [eax+ecx-14792C6Fh]
    00402258     mov ecx, [esi]
    0040225A     add ecx, edx
    0040225C     mov edx, eax
    0040225E     shl edx, 15h
    00402261     shr eax, 0Bh
    00402264     or  edx, eax
    00402266     mov eax, [esi+4]
    00402269     add edx, ebx
    0040226B     mov ebx, [esi+0Ch]
    0040226E     add eax, edx
    00402270     add ebx, edi
    00402272     mov [esi+4], eax
    00402275     lea eax, [esp+64h+var_40]
    00402279     push eax
    0040227A     mov [esi], ecx
    0040227C     mov [esi+8], ebp
    0040227F     mov [esi+0Ch], ebx
Mehrere hundert Zeilen Asm-Code ohne jegliche Sprünge oder Calls, das ist MD5! Message Digest 5 berechnet aus einem beliebigen String bzw. Text eine 128-Bit lange Zahl, ein sogenannter Hash. Dieser Hash ist eindeutig und nicht umkehrbar (was zwar methematisch nicht bewiesen ist, mit an Sicherheit grenzender Wahrscheinlichkeit jedoch angenommen werden kann). Das heisst: Jeder String liefert einen anderen Hashwert, oder noch anders ausgedrückt: Es gibt keine zwei unterschiedliche Strings, die den gleichen MD5-Hash liefern.

MD5 wird vorwiegend dazu verwendet, um Dokumente zu signieren. Der Verfasser eines Dokuments berechnet dessen MD5-Hash und stellt ihn separat zur Verfügung. So kann jeder Empfänger überprüfen, ob es sich bei seiner Kopie des Dokuments um die Originalfassung handelt, oder ob daran etwas geändert wurde. Ist nur ein einziger Buchstabe anders, ergibt dies einen unterschiedlichen MD5-Hash.

Wer es genauer wissen will, kann diesen Code Schritt für Schritt durchgehen. Mit ein bisschen Erfahrung erkennt man MD5 aber sofort an der Initialisierung, den Konstanten im Algorithmus und am Ergebnis. Da man bekanntlich nicht weit kommt, wenn man in jeden Call reingeht, kürzen wir die Sache ein wenig ab. Dies geht, indem man alle Argumente, die vor einem Call auf den Stack gepusht werden, genau anguckt. Am Ergebnis kann oft ziemlich schnell festgestellt werden, was geschieht. In unserem Beispiel machen wir dies mit Name = "figu", und Company = "gegl", das ergibt "figugegl". Dies darum, weil ich meinen MD5-Hash (fast) auswendig kenne, und somit gleich sagen kann, ob es wirklich MD5 ist oder nicht.

Nach dem Call zu MD5_Init sehen wir folgenden MD5_Context:

    0065DF44 01 23 45 67 89 AB CD EF-FE DC BA 98 76 54 32 10  .#Eg........vT2.
    0065DF54 00 00 00 00 00 00 00 00-10 00 00 00 80 FB 65 00  ..............e.
Dieser Context verändert sich nach MD5_Update so:
    0065DF44 01 23 45 67 89 AB CD EF-FE DC BA 98 76 54 32 10  .#Eg........vT2.
    0065DF54 40 00 00 00 00 00 00 00-66 69 67 75 67 65 67 6C  @.......figugegl
Und schlussendlich der MD5-Hash nach MD5_Finalize:
    0065DF0C 4D 80 80 E1 32 76 14 97-97 68 82 4F 7D 04 7A 70  M...2v...h.O}.zp
In unserem Fall wird dieser Hash in oben erwähnten String umgewandelt. Deshalb wusste ich auch gleich auf Anhieb, dass es MD5 ist. Für uns als Reverser bedeutet MD5, dass wir dies nachcoden müssen, denn wie gesagt: MD5 kann NICHT umgekehrt werden! MD5-Routinen sind in allen Programmiersprachen frei erhältlich, man muss nur die entsprechenden Dateien in seinen Code einbinden. Wer den im Zipfile beigelegten MD5 C-Code genauer anguckt, erkennt auch sogleich die Implementation hier im Crackme und meine Kommentare.
 
Algorithmus
Jetzt wird unsere Serial eingelesen und geprüft, ob alle Zeichen aus '0'..'9' bzw. 'A'..'F' bestehen. Danach kommt die eigentliche Serialberechnugsroutine.
    00401613     push 32h
    00401615     push eax
    00401616     push 3EDh
    0040161B     push esi
    0040161C     call edi                            ; Serial einlesen
    0040161E     test eax, eax
    00401620     jnz short loc_401643
      ...
    00401643     lea ecx, [esp+78h]
    00401647     push ecx
    00401648     call sub_401090                     ; Serialzeichen prüfen
    0040164D     add esp, 4
    00401650     cmp eax, 1
    00401653     jnz short loc_40167B
    00401655     lea edx, [esp+50h]
    00401659     lea eax, [esp+78h]
    0040165D     push edx
    0040165E     push eax
    0040165F     call sub_4010D0                     ; Berechnung
    00401664     add esp, 8
    00401667     lea ecx, [esp+2Ch]
    0040166B     lea edx, [esp+50h]
    0040166F     push ecx
    00401670     push edx
    00401671     call ds:lstrcmpA                    ; Vergleich
    00401677     test eax, eax
    00401679     jz  short loc_40169C
Die Berechnung wird dank Miracl Flirt-Signatur und Funktionsnamen sehr übersichtlich, man könnte schon fast sagen: einfach! Dies vorallem auch, weil der Algorithmus sehr konventionell implementiert wurde und keine Überraschungen enthält. Aber gehen wir doch ins Detail:
    004010D5     push 0
    004010D7     push 64h                            ; 100 Stellen pro Bignum
    004010D9     call _mirsys                        ; Miracl initialisieren
    004010DE     push 0
    004010E0     mov [esp+20h+var_4], eax
    004010E4     call _mirvar                        ; Big1
    004010E9     push 0
    004010EB     mov esi, eax
    004010ED     call _mirvar                        ; Big2
    004010F2     push 0
    004010F4     mov edi, eax
    004010F6     call _mirvar                        ; Big3
    004010FB     push 0
    004010FD     mov ebx, eax
    004010FF     call _mirvar                        ; Big4
    00401104     mov ecx, [esp+2Ch+arg_0]
    00401108     mov ebp, eax
    0040110A     mov eax, [esp+2Ch+var_4]
    0040110E     push ecx                            ; Zeiger auf Serial
    0040110F     push ebp                            ; Big4
    00401110     mov dword ptr [eax+238h], 10h       ; IOBASE = 16 (Hexadezimalsystem)
    0040111A     call _cinstr                        ; Serial in Bignum umwandeln  --> (BigSerial)
    0040111F     push offset a24dfda27fa14d3         ; "24DFDA27FA14D3F27DDF62CEA5D2381F9"
    00401124     push edi                            ; Big2
    00401125     call _cinstr                        ; "24DF..F9" in Bignum        --> (BigModulus)
    0040112A     push offset aE401c1b                ; "E401C1B"
    0040112F     push ebx                            ; Big3
    00401130     call _cinstr                        ; "E401C1B" in Bignum         --> (BigPubExp)
    00401135     push esi                            ; Big1
    00401136     push edi                            ; BigModulus
    00401137     push ebx                            ; BigPubExp
    00401138     push ebp                            ; BigSerial
    00401139     call _powmod                        ; Big1 = (BigSerial ^ bigPubExp) mod BigModulus 
    0040113E     mov edx, [esp+54h+arg_4]
    00401142     add esp, 40h
    00401145     push edx
    00401146     push esi                            ; Big1
    00401147     call _cotstr                        ; Big1 in String umwandeln
Als erstes werden vier Bignums erstellt, danach die Serial und zwei Strings in Bignum umgewandelt. Schlussendlich wird eine PowMod-Berechnung durchgeführt mit diesen drei Zahlen: Big1 = (BigSerial ^ bigPubExp) mod BigModulus. Deren Ergebnis wird in String umgewandelt und mit dem MD5-Hash unseres Namens verglichen.
 
Reversing RSA
Dem geübten Auge fallen sogleich zwei Dinge auf: Die PowMod Funktion und die beiden Strings. Die Funktion PowMod ist bereits ein deutlicher Hinweis auf RSA. Die Serial wird für die Berechnung verwendet und das Ergebnis mit dem Hash useres Namens verglichen. Es lässt sich keine Serial sniffen, was zwar eklig für alle faulen Reverser, jedoch ein Merkmal vieler kryptographischer Verschlüsselungsverfahren ist. Die Serial kann nur berechnet werden, indem der Algorithmus "gebrochen" wird. Die beiden erwähnten Strings sind auch auffällig, denn beim einen handelt es sich um eine Zahl, welche das Produkt zweier Primzahlen ist. Natürlich bin ich kein Hellseher, doch dazu gleich mehr.

Zuerst aber wollen wir in Kurzform unsere RSA-Kenntnisse auffrischen:

Chiffriertext (Verschlüsselung):    
Originaltext (Entschlüsselung):    
Zwei Primzahlen:    
Öffentlicher Modulus:    
Öffentlicher Exponent:    
Privater Exponent:    
C = M^E mod N
M = C^D mod N
P, Q
N = P * Q
E, es gilt: GCD (E, (P-1)*(Q-1)) == 1
D = E^(-1) mod ((P-1)*(Q-1))

In unserem Fall handelt es sich um die Gleichung zur Verschlüsselung C = M^E mod N , wobei M unserer Serial entspricht. Mit der Entschlüsselungsgleichung M = C^D mod N erhalten wir sie zurück, aber leider kennen wir den Privaten Exponenten D nicht. Diesen gilt es nun zu berechnen. Warum und wieso? Bitte lest die beiden ganz oben erwähnten Dokumente! Kurz gesagt: Wir müssen den Modulus faktorisieren, daraus erhalten wir D. Genau dazu dient nun tE's RSA Tool.

Gucken wir uns doch noch einmal die Gleichung an:

      Big1 = (BigSerial ^ bigPubExp) mod BigModulus
      Big1 = (BigSerial ^ E401C1Bh) mod 24DFDA27FA14D3F27DDF62CEA5D2381F9h

Wir wählen das 16er-System aus (Number Base) und kopieren den Modulus N. Danach drücken wir auf Exact size, das gibt uns dessen genaue Länge. Es handelt sich also um RSA-130. Jetzt faktorisieren wir den Modulus mit Factor N. Je nach PC dauert das eine (ganz) kurze Weile, auf einem P4 - 2.53 GHz zum Beispiel ganze 6 Sekunden. Danach kopieren wir den Öffentlichen Exponenten und drücken Calc D.

Somit haben wir alle Parameter beisammen, um die Serial zu berechnen:

          P = 16F54CE422ACC40EB
          Q = 19B2CF048CA1B2FAB
          N = 24DFDA27FA14D3F27DDF62CEA5D2381F9
          E = E401C1Bh
          D = 1E2D9B52ADCBC20DCCDE3C721AA740E83

Keygen
Unser Keygen wird wie gesagt relativ einfach: Wir lesen Name und Company ein, hängen beide aneinander und bilden davon den MD5-Hash. Mit diesem Hash, dem Modulus N und dem privaten Exponenten D berechnen wir mit der Funktion PowMod die Serial. So einfach ist das!
#include <miracl.h>
#include "ms32.lib"
#include "md5.c"

void CalculateSerial (HWND hWnd)
    {
        MD5_CTX md5ctx;
        big     bigSerial, bigHash, bigPrExp, bigModulus;
        int     iNameLen, iCompLen;
        char    szName[64], szCompany[32], szSerial[34] = "", cHash[16] = "";

        // get input
        iNameLen = GetDlgItemText (hWnd, EDF_NAME, szName, 32);
        iCompLen = GetDlgItemText (hWnd, EDF_COMPANY, szCompany, 32);

        // check input lengths
        if ((iNameLen == 0) || (iCompLen == 0))
        {
            SetDlgItemText (hWnd, EDF_SERIAL, NULL);
            return;
        }

        // initialize miracl: 100 digits per big, base 16
        miracl *mip = mirsys (100, 16);

        // initialize big numbers
        bigSerial  = mirvar (0);
        bigModulus = mirvar (0);
        bigPrExp   = mirvar (0);
        bigHash    = mirvar (0);

        // initialize constants
        instr (bigModulus, "24DFDA27FA14D3F27DDF62CEA5D2381F9");
        instr (bigPrExp,   "1E2D9B52ADCBC20DCCDE3C721AA740E83");

        // md5-hash on name+company
        lstrcat (szName, szCompany);
        iNameLen = lstrlen (szName);
        MD5Init (&md5ctx);
        MD5Update (&md5ctx, szName, iNameLen);
        MD5Final (cHash, &md5ctx);

        // convert result to bignum
        bytes_to_big (16, cHash, bigHash);

        // calculate serial (rsa-130): serial = hash ^ d % n
        powmod (bigHash, bigPrExp, bigModulus, bigSerial);

        // show serial
        otstr (bigSerial, szSerial);
        SetDlgItemTextA (hWnd, EDF_SERIAL, szSerial);

        // miracl cleanup
        mirkill(bigSerial);
        mirkill(bigModulus);
        mirkill(bigPrExp);
        mirkill(bigHash);
        mirexit();
        return;
    }

Nachwort
Es ist wie mit vielen anderen Dingen auch: Am Anfang erscheint das Problem unlösbar, wer es aber zwei-, dreimal gemacht hat, findet sich sofort zurecht. Mit RSA ist das nicht anders. Das Problem besteht hauptsächlich darin, dass man überhaupt erkennt, worum es sich handelt. Danach gilt es noch, die Grösse des Modulus zu bestimmen. Ist dieser nämlich zu gross, kann er nicht mehr faktorisiert werden. Genauer gesagt: Er kann schon, aber nicht in nützlicher Zeit! Ist der Modulus grösser als 200 Bit, wird der Prozess schon langwierig, ab 300 Bit kann man es bereits vergessen! Das kann jeder selber ausprobieren mit dem RSA-Tool, indem entsprechende Muduli generiert und danach faktorisiert werden.

Noch ein Wort zum Anfangs erwähnten "unbezwingbaren Softwareschutz". Wer sich einmal Asprotect genauer angeguckt hat, findet ihn dort bereits: RSA-1024 und damit wichtige Teile seines Programms verschlüsseln, z.B. die Funktion "Speichern". Diese würde somit nur bei einer gültigen Serial richtig entschlüsselt. In dem Fall hilft auch Patchen nichts: kein Speichern ohne richtige Serial!

Den kompletten Sourcecode, das Crackme, das RSA-Tool und eine C-Routine für MD5 findet ihr in der Zip-Datei. Sollte irgendwas noch unklar, nicht gut erklärt oder sogar falsch sein, bitte mailt mir. Für Fragen, Kritik, Morddrohungen, Heiratsanträge oder Lob stehe ich gerne zur Verfügung, auch Geldgeschenke oder Naturalien werden gerne angenommen!

Ich freu mich auf jeden Fall schon aufs nächste Fondue... -={ figugegl }=-

Grüsse an die Mitglieder von TNP, alle Idler vom Board, Scarebyte, +Q (welcher mich auf den Geschmack gebracht hat) und besonders an the Egoiste für sein 13370 RSA-Tool!

Abspann
Leute, falls noch Fragen auftauchen, kommt auf unsere Homepage, in unser Forum oder in unseren IRC-Chan #newbiez in Efnet! Dank geht an alle, die bereits an unserem Projekt mitgearbeitet haben, alle Mitglieder und Freunde von TNP und an alle bewußten und unbewußten Lehrmeister "all around the world". Für spezielle Sachen, z.B. wenn Ihr ein Tutorial verfaßt habt und es gern veröffentlichen wollt, schickts an unsere E-Mail-Adresse tN-p@gmx.net.