tr4ceflow Crackme/Keygenme 2
2 Tage nach der Veröffentlichung des Keygenme 1 von tr4ceflow gab es Nummer 2 hinterher. Dies war wohl schwieriger und verlangte mehr Geduld. Geduld hat es in der Tat gebraucht, denn bis ich meine Lösung vollständig implementiert hatte vergingen ein paar Stunden.
Aber alles der Reihe nach. Es gibt in diesem Fall 3 Eingabefelder, einmal für den Namen und zwei für die Serial. Nur der Name und die erste Serial hängen zusammen, die zweite Serial wird (bis auf die Länge) nicht vom Usernamen bestimmt.
Fangen wir mit der Analyse an. Ich werde, im Gegensatz zum letzen Artikel, direkt den verschönerten C-Code aus IDA entnehmen und nicht den direkten ASM Code gegenüberstellen. An Anfang wird wieder der Name abgerufen und die Länge des Namens vergleichen. Wenn dieser zwischen 6 und 10 Zeichen lang ist, ist er gültig. Im nächsten Schritt wird eine XORChecksum aus dem Buchstabenindex und dem jeweiligen Buchstaben - 0×30 erzeugt.
usernameLength = GetDlgItemTextA(a1, 40002, usernameString, 200); if ( usernameLength - 6 <= 4 ) { currentStringIndex = 0; XorChecksum = 0; while ( currentStringIndex != strlen(usernameString) - 1 ) { XorChecksum += currentStringIndex ^ (usernameString[currentStringIndex] - 0x30); ++currentStringIndex; } |
Das ist bis hierhin kein Hexenwerk. Weiter geht es mit dem Einlesen der ersten kurzen Serial und dem Vergleich der Länge mit 3 (Code ist zu trivial, daher nicht abgedruckt). Danach folgt der folgende Code der die erste kurze Serial in eine richtige Zahl umwandelt.
memcpy(someStaticInitArray1, &unk_4030C0, sizeof(someStaticInitArray1)); FirstSerial2CharMinus48 = Serial2String[0] - 0x30; currentSerial2Index = 1; // Wir fangen bei dem zweiten Zeichen an! do { v19 = 0; // internal loop counter v18 = 1; // Anfangswert setzen do { v18 *= 10; // beim ersten mal 10^1, beim zweiten mal 10^2 = 100 etc ++v19; } while ( v19 != currentSerial2Index ); FirstSerial2CharMinus48 += (Serial2String[currentSerial2Index++] - 0x30) * v18; } while ( currentSerial2Index != 3 ); |
Aus dem alten Keygenme wissen wir, das eine Zahl als String - 0×30 die jeweilige Zahl als “Zahl” ergibt. Dies wird eingesetzt um den String in eine Zahl zu wandeln, wobei jeweils mit den 10er Potenzen gearbeitet wird, um die Zahl auch in der richtigen Reihnfolge zu haben.
if ( FirstSerial2CharMinus48 == someStaticInitArray1[XorChecksum & 7] ) |
Ok, hier wird es interessant. Die eben konvertierte Zahl wird vergleichen, und zwar mit einem Array das weiter oben per memcpy dorthin kopiert wurde. Dabei wird als Index des Arrays die XorChecksum AND 7 verwendet. Das Array sieht dabei so aus:
146,725,745,803,810,816,166,297
Mit diesen Infos können wir nun zu unserem eingebenen Usernamen den richtigen Index berechnen und diese Zahl jeweils (Rückwärts!) ausgeben. In C# Code sieht das folgendermaßen aus:
String currentUsername = tbxName.Text; char[] usernameArray = currentUsername.ToCharArray(); int xorChecksum = 0; int[] firstSerialArrays = new[] { 146, 725, 745, 803, 810, 816, 166, 297 }; for (int j = 0; j < usernameArray.Length; j++) { xorChecksum += j ^ (usernameArray[j] - 0x30); } for (int i = 0; i < 8; i++) if ((xorChecksum & 7) == i) this.tbxShortSerial.Text = new string(firstSerialArrays[i].ToString().Reverse().ToArray()); |
Im nächsten Schritt wird die zweite Serial genau so über 10er Potenzen eingelesen. In diesem Fall wird allerdings “Rückwärts” gelesen dass die Serial in richtiger Reihnfolge im Register landet. Der folgende Code zeigt, dass auch IDA Pro nicht perfekt ist. Eigentlich wird nur die Quardratzahl aus der eigebenen Serial gebildet und dann durch die Funktionen gejagt. IDA Pro macht daraus, wohl aus falschem Umgang mit int64-Werten folgenden Code draus:
do // Stack leeren und dabei immer mit 0 überschreiben { *(_DWORD *)ArrayFilledWithResultsQ= 0; ArrayFilledWithResultsQ+= 4; } while ( ArrayFilledWithResultsQ!= endOfArrayPointer); do { LODWORD(PotenzSimpleDoubleCombined) = SimpleSqarePotenzStuffCopy; HIDWORD(PotenzSimpleDoubleCombined) = DoubleSqarePotenzStuffTimesSimpleSquarePotenzStuff; LODWORD(ResultFunc1) = sub_402050(PotenzSimpleDoubleCombined, 10i64); ++ArrayFilledWithResultsQ[(_DWORD)ResultFunc1]; LODWORD(ResultFunc2) = sub_402170( __PAIR__( DoubleSqarePotenzStuffTimesSimpleSquarePotenzStuff, SimpleSqarePotenzStuffCopy) - ResultFunc1, 0xAu, 0); SimpleSqarePotenzStuffCopy = ResultFunc2; DoubleSqarePotenzStuffTimesSimpleSquarePotenzStuff = HIDWORD(ResultFunc2); } while ( ResultFunc2 ); |
Im Prinzip kann man den Code auch so abkürzen:
SimpleSqarePotenzStuff = i * i; do { ResultFunc1 = sub_402050(SimpleSqarePotenzStuff, 10i64); ++memPtr[ResultFunc1]; ResultFunc2 = sub_402170(SimpleSqarePotenzStuff- ResultFunc1, 0xAu, 0); SimpleSqarePotenzStuff = ResultFunc2; } while ( ResultFunc2 ); |
Wesentlich schöner nicht? Ok, aber was passiert hier? Die kurze Antwort ist: Ich weiß es nicht. Es wird die Potenz gebildet und diese im Anschluss an die Funktion 0×402050, die ein Wert zwischen 0 und 9 zurückliefert. Die Potenz Minus dem Resultat wird wiederrum an eine weitere Funktion übergeben, die eine Art Wurzel zurückliefert. Die Funktionen selbst sind voll mit Bitshifting-Operationen und XOR-Checks, daher wollte ich mir das nicht antun. Aber die Serial ist unanhängig von Usernamen, also für alle Anwender gleich! Und damit können wir sie im Voraus berechnen.
Die Schleife läuft, solange die ResultFunc2 != 0 ist. Dies ist in der Regel nach 4-8 Durchläufen der Fall. Der Folgende check ist in C hässig und lang, in ASM aber ein Augenschmaus:
// EAX ist die Counter-Variable
// EDX das Array-Resultat an Index EAX
004016FC /MOV EDX,DWORD PTR SS:[EAX*4+ESP+4C] // Array Wert Laden
00401700 |TEST EDX,EDX // Ist er != 0
00401702 |JZ SHORT 0040170C
00401704 |CMP EAX,EDX // Ist der Array-Wert == CounterWert
00401706 |JNE 004014A2 // NOPE-> Bad-Boy
0040170C |INC EAX // EAX erhöhen
0040170D |CMP EAX,9 // Wenn EAX 9 ist
00401710 \JNE SHORT 004016FC // => Springe zum Good-Boy
In C:
while ( 1 ) { currentResultFilled = ArrayFilledWithResultsQ[finalCheckCounter]; if ( currentResultFilled ) { if ( finalCheckCounter != currentResultFilled ) break; } ++finalCheckCounter; if ( finalCheckCounter == 9 ) // 9 Chars müssen übereinstimmen return 1; }
Ich habe eine kleine DLL geschrieben, ich die ich per DLL Injection in das KeygenMe injecte. Dort berechne ich alle Zahlen von 1 bis WURZEL(2^64) = 4294967296 und lasse mir die “gültigen” Resultate abspeichern. Da ich keine gescheite Calling-Convention für die 64 Bit Integer gefunden habe, wurde InlineASM nötig und die Funktionen gescheit zu callen:
__int64 sub_402050(unsigned __int64 Value10, __int64 x10) { DWORD retLow; DWORD retHigh; DWORD dwHigh = HIDWORD(Value10); DWORD dwLow = LODWORD(Value10); __asm { push 0x00 push 0x0A push dwHigh push dwLow; mov eax, 0x402050; call eax; add esp,0x10; mov retLow,EAX; // Der Low-Anteil ist immer in EAX mov retHigh,EDX; // der High-Anteil in EDX } __int64 ret = retLow; ret = (static_cast<__int64>(retHigh) << 32) | (ret & 0xffffffff); // High Wert setzen return ret; } |
Alle Werte zu berechnen hat ca. 4 Stunden mit einem Kern Auslastung gebraucht. Diese Werte wurden in den Keygen eingetragen und davon wird Random einer ausgewählt. Zwar nicht die schönste Lösung, aber effektiv
Download des Bundles: tr4ceflow Crackme2 Bundle (26)
Greez
Das ist ja noch die alte Version vom Crackme
Ich habe aus Versehen eine Debug-Version mit auskommentierten Checks hochgeladen. In der aktuellen Version kommt nur eine Fehlermeldung bei einer falschen Eingabe.
Aber bis hierhin ist alles richtig.
Übrigens sagt dein Keygen, dass “ff” zu lang als Name ist.