AntiCracking - IsDebuggerPresent


Ziel dieses Artikels

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.

Das Debuggen erschweren

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.

IsDebuggerPresent

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
This function has no parameters.
Return Values
If the current process is running in the context of a debugger, the return value is nonzero.
If the current process is not running in the context of a debugger, the return value is zero.
Remarks
This function allows an application to determine whether or not it is being debugged, [..]

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 endp
Wie man sieht ist die Funktion nicht sehr umfangreich, aber sehen wir uns einmal an, was genau in diesen drei Zeilen passiert.
Jeder Thread hat einen so genannten "Thread Environment Block" oder kurz TEB. In der ersten Zeile:
.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 EAX
Der 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;

2010-12-29T23:44:36 +0100, mail+homepage[at]michael-puff.de