Veröffentlicht man als Autor seine Programme als Shareware, steht man vor dem Problem,
dass man seine Demo- oder sonst wie eingeschränkte oder mit einer "Nickscreen"
versehene Version davor schützen möchte, dass dieser Schutz entfernt wird, sprich
"gecrackt" wird. Die sicherste Methode ist immer noch eine Version zu veröffentlichen,
die einen eingeschränkten Funktionsumfang aufweist. (Dies darf natürlich nicht durch
eine einfache Abfrage im Code passieren, sondern die deaktivierten Funktionen dürfen
erst gar nicht im Code auftauchen. Dies kann man als Programmier bequem durch eine
bedingte Kompilierung erreichen.) Dies ist aber nicht immer praktikabel, zum Beispiel
wenn genau diese Funktion das Programm interessant macht, aber es nötig ist, dass
der Kunde es auch testen kann. Weitere Möglichkeiten wären eine Version die zeitlich begrenzt ist und
freigeschaltet werden muss oder durch eine Vollversion "ersetzt" wird oder
dass man das Programm beim Start mit einer störenden Nickscreen versieht. Der Fantasie
des Programmieres sind hier keine Grenzen gesetzt.
Aber all diese Möglichkeiten haben eins gemeinsam: Irgend wo im Code erfolgt eine Abfrage,
mehr oder weniger versteckt, die entweder eine Sereiennumer abfragt, ein Datum prüft
oder was auch immer sich der Programmierer hat einfallen lassen. Durch debuggen läßt
sich nun diese Stelle im Code finden und entprechend ändern, "patchen".
Ziel diese Artikels ist es nun Strategien zu entwickeln, die es Crackern erschweren ein Programm zu "cracken". Dabei sollte man aber immer im Auge behalten: Unmöglich machen wird man es nie können. Hat ein Cracker genug Zeit und Energie, wird er jeden Schutz überwinden. Enin anderer Punkt, den man sich vor Augen halten sollte, ist, ob der Aufwand dem Nutzen entspricht. Ein simples Programm, was man in zwei Stunden programmiert hat, mit einem Schutz zu versehen, für den man die doppelte Zeit investiert hat, erscheint wenig sinnvoll. Zu beachten ist auch, dass ein Schutz dem Endanwender nicht zu sehr stört und er dadurch die Lust am Programm verliert und erst gar nicht die Vollversion erwirbt.
Um ein Programm zu cracken gibt es mehrere Möglichkeiten. Ist es zum Beispeil durch eine Seriennumer geschütz, kann man einen Seriennumern Generator schreiben, was genau die Schwachstelle von Seriennumern ist, der immer eine gültige Seriennumer generiert. Oder man patcht die Stelle im Code, die selbige Abfragt. Und hier wären wir an der zweiten Schwachstelle. Um diese Schwachstelle zu finden, muss man den Code debuggen, das heißt man verfolgt den Programmablauf mit einem Debugger. Unsere erste Hürde für den Cracker wäre es nun, ihm dies schon mal zu erschweren, in dem wir versuchen festzustellen ob unser Programm in einem Debugger läuft oder nicht und wenn es das tut den Programmablauf unterbrechen oder etwas anderes, gemeines tun.
Die Windows API stellt eine Funktion zur Verfügung mit, der man überprüfen kann, ob ein Programm im Debugger ausgeführt und somit debuggt wird oder nicht. Das PSDK sagt dazu:
IsDebuggerPresent
The IsDebuggerPresent function determines whether the calling process is being debugged.
BOOL IsDebuggerPresent(void);Parameters
Mit Hilfe dieser API Funktion können wir also feststellen, ob unser Programm gerade mit einem Debugger analysiert wird oder nicht. Was wäre also leichter als diese Funktion zu nutzen und in Abhängikeit des Rückgabewertes zu reagieren?
if IsDebuggerPresent then // Programmablauf abbrechen else // Programmablauf fortsetzen
Ja, warum nicht einfach so? Nun, dieser Funktionsaufruf ist wie ein [Zitat Olliver] Leuchtturm [Ziatatende] im Code, da er mit Aufrufen von LoadLibrary und GetProcAddress mit verräterischen Strings verbunden ist. Unser Angreifer kann nun den Code bis zu dieser Stelle debuggen, dort anhalten, den Code patchen und schön kann er einfach den Code weiter analysieren. Der Aufruf dieser API Funktion stellt zwar eine Hürde da, aber für erfahrene Cracker ist auch dies kein Problem.
Etwas schwerer können wir es dem Angreifer machen, wenn wir unsere eigene IsDebuggerPresent Funktion, welche nicht mit solchen verräterischen Aufrufen verbunden ist, schreiben und nutzen. Dazu müssen wir wissen was diese API Funktion macht. Dazu sehen wir uns einmal den Assembler-Code der Funktion an:
.text:77E5AC39 _IsDebuggerPresent@0 proc near .text:77E5AC39 mov eax, large fs:18h ; Adresse des TEB (TEB.Self) .text:77E5AC3F mov eax, [eax+TEB.Peb] ; Adresse des PEB (TEB.Peb) in EAX .text:77E5AC42 movzx eax, [eax+PEB.BeingDebugged] .text:77E5AC46 retn .text:77E5AC46 _IsDebuggerPresent@0 endpWie man sieht ist die Funktion nicht sehr umfangreich, aber sehen wir uns einmal an, was genau in diesen drei Zeilen passiert.
.text:77E5AC39 mov eax, large fs:18h ; Adresse des TEB (TEB.Self)holen wir uns die Adresse unseres TEB. Dieser TEB enthält wiederum einen Pointer auf den "Process Environment Block", dessen Pointer in der zweiten Zeile berechnet wird:
.text:77E5AC3F mov eax, [eax+TEB.Peb] ; Adresse des PEB (TEB.Peb) in EAXDer PEB wiederum enthält ein Boolean-Feld namens "BeingDebugged", welches (wie der Name schon sagt) angibt, ob der jeweiligen Prozess von einem Debugger kontrolliert wird. Dieses Feld wird nun in der dritten Zeile ausgewertet und als Funktionsergebnis zurückgegeben.
.text:77E5AC42 movzx eax, [eax+PEB.BeingDebugged]Letzt endlich liest "IsDebuggerPresent" also eigentlich nur ein Byte aus einer Struktur aus, und dieses Verhalten können wir leicht nachbauen.
Mit obigem Wissen können wir dann folgende Funktion schreiben:
function MyIsDebuggerPresent: Boolean; assembler; asm mov eax, fs:[$18]; mov eax, [eax+$30]; movzx eax, byte ptr [eax+2]; end;
Schon besser, aber es geht noch besser. Auch dieser Funktionsaufruf ist mit einem CALL verbunden, den man rauspatchen kann. Deswegen ist es besser die Funktion im Code "hardzucoden":
var BeingDebugged: Boolean; asm mov eax, fs:[$18]; mov eax, [eax+$30]; mov eax, [eax+2]; mov [BeingDebugged], al end;
Zusätzlich kann man die Register variieren:
var BeingDebugged: Boolean; begin asm push ebx; mov ebx, fs:[$18]; mov ebx, [ebx+$30]; mov ebx, [ebx+2]; mov [BeingDebugged], bl; pop ebx; end; end;
... oder komplexer:
var BeingDebugged: Boolean; begin asm push eax; push ebx; mov eax, fs:[$18]; mov ebx, [eax+$30]; mov eax, [ebx+2]; mov [BeingDebugged], al; pop ebx; pop eax; // Wichtig! POP immer in umgekehrter Reihenfolge von PUSH end; end;