logo  
Manual unpack NFSU2 (Safedisc v3.xx)
Autor: mr_magic
Datum: 3.12.2004
Target: Speed2.exe
Size: 5.987.981 Bytes
Tools: SoftIce, IceExt 0.65, PEditor 1.7, Procdump 1.6, Imprec 1.6f, Hex Workshop, Code Fusion
Kategorie: Unpacking
 
Inhalt:

1. Einleitung
2. Vorbereitungen
3. Imports fixen
4. Redirectete interne Calls fixen
5. Stolen Bytes recovern
6. Rebuilden
7. Schlusswort

1. Einleitung:

Um eines vorweg zu sagen: Zum besseren Verständnis, warum wir die Imports so fixen, wie wir sie fixen, empfehle Ich euch dringend, mein erstes CIP Tutorial über das manuelle Unpacking von Safedisc v2.30.33 zu lesen (hey, habt ihr aber bestimmt ;) denn ich werde darauf nur noch flüchtig eingehen, da es EXAKT genauso funktioniert. Den Rest werde ich dann wieder ausführlicher erklären.

Falls ihr gerade keine Original CD von Need for Speed Underground 2 zur Hand habt, könnt ihr euch eines der Mini Images von GameCopyWorld runterladen und es mit Daemon Tools oder Alcohol 120% mounten.

2. Vorbereitungen:

Bevor wir uns nun ans Eingemachte begeben, sorgen wir dafür, dass wir in den Sections, die wir fixen müssen (.text, .rdata) Schreibrechte haben um ungestört eigenen Code ausführen zu können. Die Macrovision Programmierer haben es auch in der neuen major Safedisc Version nicht zu Stande gebracht, einen CRC Check einzubauen, der uns daran hindert am PE Header rumzuspielen :)

Wir öffnen die "Speed2.exe" also im PEditor und klicken auf "Sections" Wir sollten nun folgendes vor uns sehen:



Wie wir wissen, gehören die untersten beiden Sections zu Safedisc. Die oberere ist für bestimmte Schutzmechanismen zuständig und wird beim spielen des Games noch benötigt. Die unterere ist die Loader Section, sie wird nach dem Decrypten des Games nicht mehr benötigt. Wir werden beide in unserer ungepackten Exe entfernen. Was uns nun Interessiert, sind die Eigenschaften der Sections, die den Code beinhalten (.text) und die die Imports beinhalten (.rdata)
Wir rechtsklicken nun also auf die .text Section und wählen "edit section" aus. Im folgenden Fenster klicken wir rechts unten auf "char. wizard". Jetzt können wir ganz bequem bei "writable" ein Häkchen machen un auf "take it" klicken. Jetzt nur noch auf "apply changes" und das einfachste wäre überstanden ;)
Natürlich machen wir genau das gleiche mit der .rdata Section.



Damit wir das Game ungestört debuggen können, aktivieren wir die IceExt Funktion

:!protect on

3. Imports fixen:

Bei den Erklärungen des Fixens der Imports werde ich mich auf das wesentliche beschränken, weil sich dabei seit meinen letzten Tutorial nichts geändert hat.

Naja, schauen wir uns doch mal den Entrypoint an. Hierbei werden wir wieder den PEditor dazu benutzen, dass SoftIce (SI) nach dem Laden des Programmes breakt. Dazu klicken wir auf "break'n'enter" und setzen in SI einen Breakpoint auf Int03

:bpint3

Kicken wir nun auf "RUN", finden wir uns hier wieder:

0093309E   55               PUSH EBP
0093309F   8BEC             MOV EBP,ESP
009330A1   60               PUSHAD
009330A2   BB 9E309300      MOV EBX,OFFSET speed2.{ModuleEntryPoint}
009330A7   33C9             XOR ECX,ECX
009330A9   8A0D 3D309300    MOV CL,BYTE PTR DS:[93303D]
009330AF   85C9             TEST ECX,ECX
009330B1   74 0C            JE speed2.009330BF
009330B3   B8 13319300      MOV EAX,speed2.00933113
009330B8   2BC3             SUB EAX,EBX
009330BA   83E8 05          SUB EAX,5
009330BD   EB 0E            JMP speed2.009330CD
009330BF   51               PUSH ECX
009330C0   B9 59319300      MOV ECX,speed2.00933159
009330C5   8BC1             MOV EAX,ECX
009330C7   2BC3             SUB EAX,EBX
009330C9   0341 01          ADD EAX,DWORD PTR DS:[ECX+1]
009330CC   59               POP ECX
009330CD   C603 E9          MOV BYTE PTR DS:[EBX],0E9
009330D0   8943 01          MOV DWORD PTR DS:[EBX+1],EAX
009330D3   51               PUSH ECX
009330D4   68 09309300      PUSH 00933009
009330D9   33C0             XOR EAX,EAX
009330DB   85C9             TEST ECX,ECX
009330DD   74 05            JE 009330E4
009330DF   8B45 08          MOV EAX,DWORD PTR SS:[EBP+8]
009330E2   EB 00            JMP 009330E4
009330E4   50               PUSH EAX
009330E5   E8 76000000      CALL 00933160                 ;Unpack Routine
009330EA   83C4 08          ADD ESP,8
009330ED   59               POP ECX
009330EE   83F8 00          CMP EAX,0                     ;Gab es einen Fehler beim Unpacken?
009330F1   74 1C            JE 0093310F                   ;Wenn nicht, dann mache weiter
009330F3   C603 C2          MOV BYTE PTR DS:[EBX],0C2
009330F6   C643 01 0C       MOV BYTE PTR DS:[EBX+1],0C
009330FA   85C9             TEST ECX,ECX
009330FC   74 09            JE 00933107
009330FE   61               POPAD
009330FF   5D               POP EBP
00933100   B8 00000000      MOV EAX,0
00933105   EB 97            JMP {ModuleEntryPoint}
00933107   50               PUSH EAX
00933108   A1 29309300      MOV EAX,DWORD PTR DS:[933029]
0093310D   FFD0             CALL EAX
0093310F   61               POPAD                         ;räume den Stack auf
00933110   5D               POP EBP                       ; ""
00933111   EB 46            JMP 00933159                  ;und springe
00933113   807C24 08 00     CMP BYTE PTR SS:[ESP+8],0
00933118   75 3F            JNZ 00933159
0093311A   51               PUSH ECX
0093311B   8B4C24 04        MOV ECX,DWORD PTR SS:[ESP+4]
0093311F   890D 53319300    MOV DWORD PTR DS:[933153],ECX
00933125   B9 31319300      MOV ECX,00933131
0093312A   894C24 04        MOV DWORD PTR SS:[ESP+4],ECX
0093312E   59               POP ECX
0093312F   EB 28            JMP 00933159
00933131   50               PUSH EAX
00933132   B8 2D309300      MOV EAX,0093302D
00933137   FF70 08          PUSH DWORD PTR DS:[EAX+8]
0093313A   8B40 0C          MOV EAX,DWORD PTR DS:[EAX+C]
0093313D   FFD0             CALL EAX
0093313F   B8 2D309300      MOV EAX,0093302D
00933144   FF30             PUSH DWORD PTR DS:[EAX]
00933146   8B40 04          MOV EAX,DWORD PTR DS:[EAX+4]
00933149   FFD0             CALL EAX
0093314B   58               POP EAX
0093314C   FF35 53319300    PUSH DWORD PTR DS:[933153]
00933152   C3               RETN
00933153   72 16            JB 0093316B
00933155   61               POPAD
00933156   1360 0D          ADC ESP,DWORD PTR DS:[EAX+D]
00933159   E9 9388E2FF      JMP 0075B9F1                ;zum OEP!!!


Also nichts neues seit Safedisc v2. Doch, es gibt eine kleine Veränderung: Wir können direkt einen bpx auf den Jump zum OEP setzen. Safedisc bemerkt diese nicht mehr (hey Jungs, ihr habt eure Protection VERSCHLECHTERT :D). Also

:bpx #00933159

Vorsicht, bevor wir jetzt durchstarten, müssen wir das int3, dass wir an den EP gesetzt haben wieder in das ursprüngliche Byte zurückändern.

:eb eip 55

Dann drücken wir F5, breaken, drücken einmal F8 und befinden uns nun am OEP. Eigentlich wäre alles gut, alle Sections sind entpackt und wir könnten dumpen wenn Safedisc uns nicht ein paar Steine in den Weg legen würde... Phear??? Hehe, let's fix dat... zuerst müssen wir uns um die gemangleten Imports kümmern

Dass die Imports gemanglet sind, erkennt man ganz leicht, indem man sich die IAT (die sich am Anfang der .rdata Section befindet) anguckt.



Die RVA's zu den API Funktionen müssen rückwärts gelesen werden, d.h. die Adresse des ersten Imports auf dem Bild ist 77E53837. Diese gehört bei meinem OS zu Kernel32.RaiseException. Uns fällt sofort auf, dass die Kernel32 Imports alle nicht redirected sind, wir brauchen sie also nicht zu fixen. Leider erspart das uns absolut keine Arbeit...

Alle Imports, die mit 0174/0175 anfangen, sind gemanglete Imports. Wir merken uns für unseren Imports-fix Code diese Range.

Auch merken wir uns, in welchem Bereich der IAT diese gemangleten imports vorkommen. Bei mir sind das ca. die ersten 500h Bytes.

Wie auch schon in meinem letzten Tut beschrieben, werden wir unseren Code im PE Header+500h assemblen (oder injecten, falls ihr lieber Code Snippet Creator oder Hiew verwenden wollt). Um die folgenden Schritte nachvollziehen zu können, rate ich jedem, in einige der redirecteten Calls hineinzutracen. Der entsprechende Call zum ersten redirecteten Import in der IAT (00783004) sieht so aus:



Wenn wir in ihn hineintracen sehen wir folgendes:



Der Safedisc Routine, die die gemangleten RVA's auflöst und den entsprechenden Import dann callt, werden 3 Parameter übergeben. 2 davon können wir auf dem Bild erkennen. Es sind die beiden Pushs (0174D582, 0174D58A). Der dritte wird durch den Stack "unsichtbar" mitgeliefert. Es ist dass Offset, an dem unser gemangleter Import gecallt wurde + 6, also das Offset nach dem "Call [00783004]".
Das liegt daran, dass ein Call nichts anderes ist, als ein JMP und ein Push $Offset_von_JMP+6 in einem. Diese Tatsache macht es für uns notwendig, nicht nur die IAT (wie bei vielen Protections), sondern auch die Call [.ref]'s aus der .text Section zu fixen.
Es ist nämlich so, dass nach dem Redirecten einige Imports für uns wie die selben aussehen, weil sie auf das gleiche Offset in der IAT pointen. Da die Safedisc Routine zum Imports auflösen jedoch dass Offset des Call [.ref]'s mit in ihre Berechnungen ein bezieht, werden unterschiedliche API Funktionen gecallt.

Schauen wir uns die eigentliche Routine, die bei 6678D3BA beginnt, doch einmal an. Wenn wir oft genug durchtracen, bemerken wir, dass folgende 3 Stellen von Relevanz sind:



Hier wird überprüft, ob die aktuell zu bearbeitende RVA schon einmal aufgelöst wurde. Ist das nicht der Fall, passieren wir den Jump und sie wird aufgelöst. Ansonsten wird die schon Vorhandene verwendet. Wir werden die Routine so patchen, dass unsere RVA's immer neu aufgelöst werden. Wir machen dazu also aus dem conditional einen unconditional Jump. Das gibt uns ein besseres Gefühl ;)



Wie wir sehen, findet noch eine zweite Überprüfung statt, bevor die aktuelle RVA aufgelöst wird. (Auch hier werden wir patchen indem wir den Jump nopen). Anschließend werden DLLnr. und Funktionsnr. gepusht, die zuvor aus unseren 3 Parametern berechnet wurden und die RVA wird in dem abgebildeten Call aufgelöst.



Diese Stelle ist für uns am interessantesten, weil es das Ende der Routine ist. Der Stack wird aufgeräumt und wenn wir über das RET tracen, befinden wir uns am anfang der korrekten API Funktion. Das heist, bevor wir über das RET tracen, ist der oberste dword auf dem Stack die korrekte RVA für unseren Import. Das werden wir uns in unserem Code zu nutze machen.
Dazu assembeln wir am Offset 6678D6AB einen "Jump _ReturnNachSafedisc"

Wir unterteilen unseren Code zum Imports fixen in 2 Teile. Der erste wird nötig, weil den Macrovision Programmierern der Trick mit dem 3. Parameter eingefallen ist. Wir werden also zuerst an einem nicht gebrauchten Ort eine temporäre gefixte IAT erstellen, in der keine Imports mehr gemanglet sind. Diese werden wir später über unsere aktuelle kopieren, die wir jetzt aber noch benötigen, um die .text Section zu fixen. Wir werden wir die gesamte Text Section nach Call [.ref]s durchsuchen, die auf gemanglete RVAs pointen und ihre Zieladdressen gemessen an den neuen relativen Adressen der temporären IAT korrigieren, sodass sie nach dem überschreiben der IAT den korrekten Import callen.
Als ein geeigneter Ort zum erstellen der temp. IAT erscheint mir die Safedisc Loader Section. Wir schauen uns also nochmal die Sections an, um alle Werte für unseren Code zu haben

_ReturnNachSafedisc:
mov esp, dword ptr [ebp+0C]    ;Räumt den Stack auf, wie die Funktionen
popad                          ;am Ende der Safedisc Routine, die durch
popfd                          ;den JMP gecrusht wurden.
pop edx                        ;Speichert die korrekte RVA in edx
cmp dword ptr [esp], 00400500  ;würden wir im Falle eines RETurns in dem
jl _KorrigiereStack            ;Bereich unseres Codes herauskommen?
cmp dword ptr [esp], 00401000  ;Wenn nicht, dann korrigiere den Stack.
ja _KorrigiereStack            ;(das wird dadurch nötig, dass wir im 2.
ret                            ;Teil unseres Codes einen zusätzlichen


_KorrigiereStack:              ;PUSH verwenden, um den Returnpoint zu
add esp, 4                     ;setzen)
ret


_CODEPART1:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;; Erstellen einer temporären IAT ;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

mov ecx, 00782FFC              ;IAT Anfang-4
mov ebx, 00932FFC              ;Safedisc Loader Section-4

_Start:

add ecx, 4                     ;mache weiter mit der nächsten RVA
add ebx, 4                     ;erhöhe auch das Offset unserer temp. IAT
cmp ecx, 00783500              ;haben wir schon 500h Bytes bearbeitet?
ja _EndeBuildIAT

mov edx, dword ptr [ecx]       ;setze edx auf die RVA des aktuellen Imports                
cmp edx, 01740000              ;liegt die RVA in
jl _BuildIAT
cmp edx, 01760000              ;der Safedisc Range?
ja _BuildIAT
call edx                       ;wenn ja, calle den Import um die korrekte RVA zu erhalten


_BuildIAT:

mov dword ptr [ebx], edx       ;Trage die korrekte RVA in die temp. IAT ein
jmp _Start

_EndeBuildIAT:

jmp _CODEPART2


_CODEPART2:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;     Fixen der Call [.ref]s     ;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

mov eax, 00401000              ;Anfang der .text Section


_SucheNachCallRefs:

cmp word ptr [eax], 15FF       ;Sind die ersten 2 Bytes der aktuellen Funktion FF15?
je _CallRefGefunden            ;Wenn ja, überprüfe den Call [.ref]


_SucheNachCallRefsLoop:

inc eax                        ;ansonsten mache weiter mit der nächsten Funktion
cmp eax, 00782FFF              ;Haben wir das Ender der .text Section erreicht?
jne _SucheNachCallRefs
jmp _AlleCallRefsGefixt        ;Alle Call [.ref]s gefixt


_CallRefGefunden:

cmp dword ptr [eax+02], 00783000   ;Liegt das gelesene Offset in den
jl _SucheNachCallRefsLoop
cmp dword ptr [eax+02], 00783500   ;ersten 500h Bytes unserer IAT?
ja _SucheNachCallRefsLoop


_CheckobGemangleterCallRef:

mov ecx, dword ptr [eax+02]    ;Setze ecx auf das Offset des Imports in der IAT
mov ecx, dword ptr [ecx]       ;setze ecx auf die RVA des aktuellen Imports
cmp ecx, 01740000              ;liegt die RVA in
jl _SucheNachCallRefsLoop
cmp ecx, 01760000              ;der Safedisc Range?
ja _SucheNachCallRefsLoop


Push _FixCallRefReturnPoint    ;wenn ja, setze unseren Return Point
add eax, 6                     ;Berechne den 3. Parameter (Call [.ref] Offset+6)
push eax                       ;Und pushe ihn
sub eax, 6                     ;setzte eax wieder auf den Anfang des Call [.ref]s
jmp ecx                        ;und führe die SD Routine aus, um die korrekte RVA zu erhalten


_FixCallRefReturnPoint:

mov ebx, 00933000              ;setze ebx auf den Anfang der temp. IAT


_FixCallRefLoop:

cmp dword ptr [ebx], edx       ;Suche die aktuelle RVA in der temp. IAT
je _FixCallRef
add ebx, 4                     ;überprüfe nachsten Import in der temp. IAT
cmp ebx, 00933500              ;haben wir das Ende der temp. IAT erreicht?
jl _FixCallRefLoop             ;wenn nicht, mache weiter
jmp eip                        ;Sonst übergebe uns die Kontrolle zur Fehlersuche


_FixCallRef:

sub ebx, 00933000              ;Setze ebx auf das relative Offset, gemessen am IAT Anfang
add ebx, 00783000              ;und auf das zukünftige Offset wenn die temp. IAT an die richtige Stelle kopiert wurde
mov dword ptr [eax+02], ebx    ;und update den Call [.ref] mit dem neuen Offset des Imports
jmp _SucheNachCallRefsLoop


_AlleCallRefsGefixt:
jmp _CODEPART3


Leider wurden die Imports nicht nur auf diese Art und Weise redirected. Es gibt noch eine Zweite. Es existieren Longjumps, die einmal Call [.ref]s gewesen sind, die nun auf die stx774 Section pointen. Theoretisch hätten es auch einmal Calls zu Jumptables gewesen sein können, das ist bei NFSU2 aber nicht der Fall. Das sehen wir ganz leicht daran, dass das Byte nach dem Jump immer ein Junk Byte ist. Das liegt daran, dass der ursprünglich an dieser Stelle stehende Call [.ref] ein Byte länger ist als der Jump. Die durch den Jump aufgerufene Safedisc Routine überspringt dieses Byte immer. Ein Beispiel für so einen Long Jump:



Wir sehen, dass der Befehl nach den Jump keinen Sinn macht. Eigentlich müsste er so aussehen:



Wenn wir in einen redirecteten Long Jump reintracen, befinden wir uns nur für eine kurze zeit in der stx774 Section, nach einem RET kommen wir hier heraus:




Diese Routine macht also fast das gleiche wie die der Call [.ref]s, mit dem kleinen Unterschied, dass der 3. Parameter manuell gepusht wird, da kein Call stattfindet. Danach wird unsere RVA Auflös Routine gecallt, die wir bereits gepatched haben. Wir assembeln also weiter:


_CODEPART3:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;  Fixen der redirecteten Jumps  ;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

mov eax, 00401000              ;Anfang der .text Section


_SucheNachLongJumps:

cmp byte ptr [eax], E9         ;haben wir es möglicherweise mit einem Long Jump zu tun?
je _JumpGefunden


_SucheNachLongJumpsLoop:

inc eax                        ;Überprüfe das nächste Byte
cmp eax, 00782FFF              ;Haben wir das Ende erreicht?
jne _SucheNachLongJumps
jmp _CODEPART4                 ;Fixen der redirecteten internen Calls


_JumpGefunden:

mov ecx, dword ptr [eax+01]    ;Setze ecx auf den dword Wert des Abstandes
add ecx, eax                   ;Addiere ihn mit dem aktuellen Offset
add ecx, 00000005              ;und mit der länge des Jumps == Ziel Offset
cmp ecx, 00930000              ;Ist das Ziel Offset in
jl _SucheNachLongJumpsLoop
cmp ecx, 00933000              ;der stx774 Section?
jnb _SucheNachLongJumpsLoop

push _LongJumpFixReturnPoint   ;Wenn ja, setze unseren Return Point
jmp ecx                        ;und berechne die korrekte RVA für unseren Import


_LongJumpFixReturnPoint:

mov ebx, 00933000              ;setze ebx auf den Anfang unserer temp. IAT


_LongJumpFixLoop:

cmp dword ptr [ebx], edx       ;und durchsuche sie nach unserer aktuellen RVA
je _LongJumpFix                ;gefunden, dann springe
add ebx, 00000004              ;sonst überprüfe den nächsten dword
cmp ebx, 00933500              ;haben wir das Ende erreicht?
jl _LongJumpFixLoop            ;wenn nicht, mache weiter
jmp eip                        ;Endlos-Loop, manuelle Fehlerbehebung


_LongJumpFix:

sub ebx, 00933000              ;Setzt ebx auf das relative Offset, gemessen am IAT Anfang
add ebx, 00783000              ;und auf das zukünftige Offset wenn die temp. IAT an die richtige Stelle kopiert wurde


_LongJumpIstCallRef:

mov word ptr [eax], 15FF       ;verwandle den Long Jump in einen Call [.ref]
mov dword ptr [eax+02], ebx    ;mit dem passenden Offset unserer temp. IAT
jmp _SucheNachLongJumpsLoop    ;und suche nach dem nächsten Long Jump

Puh, so viel also zu dem altbekannten. Wir können jetzt die alte mit der neuen IAT überschreiben.

:m 00933000 L 500 00783000

Dennoch, vom Safediscfreien Zocken sind wir noch weit entfernt, denn Safedisc hat noch 2 neue Techniken, unseren Dump vom Laufen abzuhalten.
Falls ihr in SI assemblet habt, solltet ihr euren Code sichern:

:!dump \??\C:\nfsu2.patch.dat 00400500 500

um ihn beim erneuten starten des Games nicht wieder von neuem assembeln müsst, sondern ihn mit

:!loadfile \??\C:\nfsu2.patch.dat 00400500

wieder laden könnt. Wurde der Code mit einem Hex Editor eingefügt, entfällt das natürlich ;)

4. Redirectete interne Calls fixen.

Der nächste offensichtliche Versuch von Safedisc, uns das Leben schwer zu machen sind die Stolen Bytes, die alle durch CC, also int3 Opcodes ersetzt wurden. Wenn wir in SI einen

:bpint3

setzen und das Game ganz normal starten, werden wir nach 2 Breaks an einer solchen Stelle stehen:



Wenn wir nun mit F8 in den Int3 Befehl hineintracen, finden wir uns im ntoskrnl Modul wieder. Tracen wir nun in den nächsten Call rein und über alle restlichen drüber, so kommen wir an das Ende der Routine:



Tracen wir über das IRETD, befinden wir uns wieder in unserer "Speed2.exe", allerding 2 Bytes weiter, also vor den nächsten 2 int03 Befehlen. Tracen wir auch über diese, beobachten wir das gleiche Spiel.
Wenn wir unseren bpx auf int3 immernoch aktiv lassen, werden wir beobachten können, dass wenn an einer Stelle 3 int3s hintereinander stehen, nach der ntoskrnl Routine die korrekten Bytes statt der CCs in der .text Section stehen. Auch wenn wir an eine Stelle mit 4 int3s kommen, die wir schon einmal durchlaufen haben, werden nachher die korrekten Bytes ins Code Segment geschrieben.

Für unser Beispiel währen das folgende Bytes:



Wir brauchen natürlich nicht so lange warten, bis wir eine Stelle zum 2. Mal passieren, sondern können die Eip auch einfach -4 nehmen. Weil wir zu faul sind, die gesamte ntoskrnl Routine zu durchtracen, überlegen wir mit welcher API Funktion das kopieren bewerkstelligt werden könnte... Genau! mit Kernel32.WriteProcessMemory.

Setzen wir also, nachdem wir am OEP angelangt sind einen

:bpx writeprocessmemory

Aha, wir breaken. Wenn Wir uns nun die Register anschauen, sehen wir, dass ebx das einzige ist, dass eine Adresse unserer Speed2.exe beinhaltet. Also geben wir ein:

:u ebx



Mhhh... leider steht an dem Offset nicht wie wir erwartet hatten ein int3, sonder ein "Call 00401089". Aber es sieht dennoch interessant aus... geben wir also

:p ret

ein, um aus der API Funktion zu gelangen. Wir sehen jetzt, dass sie von Safedisc gecallt wurde. Tracen wir also in der Routine über alle Calls, bis wir folgende Stelle erreichen:



Das callen der API Funktion "RtlLeaveCriticalSection" deutet auf das Ende der Routine hin. Wir werden also in den nächsten Call hineintracen.



Da eax im Moment eine Adresse des Stacks beinhaltet, springt der Jmp an der EIP dorthin. Wir sehen:



Der Stack wird also korrigiert und nach dem RET enden wir an der Stelle, an die uns der Call eigentlich einmal geführt hätte:



Wir wissen also wieder, dass nach dem korrigieren des Stacks und vor dem RET das Offset des richtigen Ziels des Calls der oberste dword auf dem Stack ist. Weil die Safedisc Routine aber so nett ist und den falschen Call mithilfe von Kernel32.WriteProcessMemory mit dem richtigen überschreibt, brauchen wir uns nicht weiter darum zu kümmern. Wie finden wir nun die anderen redirecteten Calls? Die Lösung kommt, wenn wir ein weiters Mal auf Kernel32.WriteProcessMemory breaken und uns das ebx Register anschauen. Was sehen wir? Hehe, wieder einen "Call 00401089"...

Was wir also tun müssen ist:
1. Die .text Section nach "Call 00401089"s durchsuchen
2. Dafür sorgen, dass wir nach der Safedisc Routine wieder zu unserem Code kommen

Bei Punkt 1 ist zu beachten, dass Calls genau wie Jmps nicht statisch sind. Das heist, wir können anhand des Hex Wertes alleine nicht das Ziel Offset ablesen, sondern müssen es berechnen (Offset+5+dword ptr von [Offset+1])

Bei Punkt 2 ist zu beachten, dass das Ende der Routine im Stack liegt. Da dieser wieder überschrieben wird, wird der Code des Endes der Routine bei jedem Call 00401089 wieder neu dort hin geschrieben. Damit wollte man wohl ein patchen verhinden (nun Junx das ist WIRKLICH Lame...)

Das zwingt uns lediglich dazu, noch zu patchen, wenn wir uns noch in der Safedisc Routine und nicht im Stack befinden. Ich habe also folgendes gemacht, eigentlich haben wir aber alle Möglichkeiten :)



Wir assembeln also am Offset 6673E11C einen "mov dword ptr [ebp-4], _ReturnPointInternCall" und nopen den Rest bis zum "Jmp [ebp-4]"

An unserem "_ReturnPointInternCall:" müssen wir auch wie in der Safedisc Routine den Stack korrigieren. Wir wollen jedoch nicht am Offset des eigentlichen Ziel des Calls herauskommen, sonden in unserem weiteren Code. Also müssen wir den Stack anders korrigieren. Doch dazu gleich mehr. Assembeln wir erstmal, was wir schon wissen:

_CODEPART4:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;  Fixen der redirecteten Calls  ;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


mov eax, 00401000


_SucheNachCalls:

cmp byte ptr [eax], E8         ;Haben wir es vielleicht mit einem Call zu tun?
je _CallGefunden


_SucheNachCallsLoop:

inc eax                        ;Überprüfe das nächste Byte
cmp eax, 00782FFF              ;Haben wir das Ende erreicht?
jl _SucheNachCalls

jmp _CODEPART5                 ;Springe zum letzten Code von uns :)


_CallGefunden:

mov ebx, dword ptr [eax+01]    ;Setzte ebx auf die Länge des Calls
add ebx, eax                   ;Addiere sie mit dem Offset
add ebx, 00000005              ;Und mit der Anzahl der Bytes, die der Call hat
cmp ebx, 00401089              ;haben wir es mit einem "Call 00401089" zu tun?
jne _SucheNachCallsLoop

call eax                       ;wenn ja, dann calle ihn und lasse ihn ersetzen

jmp _SucheNachCallsLoop


Um jetzt den korrekten Wert für unsere Stack korrektur zu haben, sollten wir den ganzen Code einmal tracen, bis wir zu der Stelle kommen, an der er gesichert wird:



Wir durchlaufen also das "push esp, pushfd, pushad" und schauen, welchen Wert esp nun hat. Aha, 0012FF90. Jetzt haben wir alles was wir brauchen.

_ReturnPointInternCall:

mov esp, 0012FF90              ;wir wollen, dass alle Register den Selben
popad                          ;Inhalt haben, wie bei unserer Sicherung
popfd                          ;als der Stack auf 0012FF90 stand
pop esp
mov esp, 0012FFC0              ;Jetzt müssen wir ihn nur noch so einstellen, dass
ret                            ;er unsere korrekte RETurn Adresse beinhaltet


Das "mov esp, 0012FFC0" könnte eigentlich auch ein "add esp, 4" oder "add esp, 8" sein, das ist von Call zu Call unterschiedlich. Deswegen habe ich mich für die sicherere Variante entschieden.

Alles gefixt? Gut, dann können wir jetzt erstmal dumpen. Einmal machen wir mit Procdump einen Full Dump mit allen PE Rebuilding Optionen und OHNE Import rebuilding. Das kommt noch. Dazu machen wir auch noch einen Dump nur von der .text Section:

:!dump \??\C:\nfsu2.text.section.fixed.dat 00401000 00382000

Jetzt werden wir etwas DESTRUCTIVE...

5. Stolen Bytes recovern:

Diesmal werden wir wirklich die stolen Bytes recovern. Theoretisch könnten wir dazu einfach 24 Stunden dauerzocken. Das würde zwar ne Menge Spass machen, allerdings wäre das nicht 100% sicher und es würde vielleicht erst zu spät auffallen, wenn einige Bytes noch nicht da wären.

Wie wir schon weiter oben herausgefunden haben, werden alle Stolen Bytes in der ntoskrnl.exe spätestens beim 2. Mal ausführen durch einen speziellen Exception Handler überschrieben. Wie auch sonst wäre es doch am besten, wir würden wie immer die .text Section nach int3s durchsuchen, diese dann ausführen um sie mit den korrekten Bytes überschreiben zu lassen und schließlich zu unserem Code zurückkehren... Und hier ist auch schon der Haken. Wollen wir wirklich in der WindowsNT Kernel patchen??? Definitiv nicht (hätte fast mein OS damit zerschossen... :X) wie siehts denn davor aus? Mhhhh... davor ist die Text Section. Wir werden wohl nicht darum herum kommen, sie zu zerstören. Deswegen haben wir ja schon einen Dump angefertigt.

Meine erste Idee war es also, nach jedem int3 oder nach jedem Block int3s ein C3, also ein RET Opcode assembeln zu lassen und diese dann zu callen. Dann haben wir aber wieder ein Problem. Es können ja auch Jmps in unseren stolen Bytes auftauchen (wie wir direkt bei unserem 1. Beispiel gemerkt haben). Die 2. Idee war also die logische Schlussfolgerung, alles ausser die Int3s mit C3 Bytes überschreiben zu lassen. Nach einem Jump würden wir so trotzdem auf einem RET landen... Aber auch diese Idee hat einen Haken:

Wir müssen ja irgendwie überprüfen lassen, ob es sich um stolen Bytes oder einfach nur um Bytes eines anderen Operanden handlet. Um besser zu verstehen, was ich meine solltet ihr mit dem SI Command

:s 00401000 l ffffffff cc cc

die .text Section durchsuchen. Ihr werdet feststellen, dass es realtiv viele CC Bytes in anderen Funktionen gibt. Ein Beispiel:



wenn wir ein Stück nach unten und dann wieder nach oben scrollen, sehen wir die wirkliche Funktion:



Wenn wir versuchen, trotzdem diese Int3s auszuführen, übernimmt nicht der spezielle Exception Handler das Überschreiben der betroffenen Bytes, sondern der ganz normale NT Exception Dispatcher. Wir bekommen eine schöne Messagebox, dass eine Exception aufgetreten ist und das Game wird terminiert.

Auch werden wir beim Durchsuchen der .text Section sehr oft folgendes finden:



Hierbei handelt es sich um einen Sonderfall, bei dem immer 5 int3s gefolgt von einem NOP nach einem RET aufteten. Wenn wir diese Int3s ausführen, werden sie von unserem speziellen Exception handler übernommen und er überschreibt sie mit 9090909090. Also wieso Sonderfall? Tja, das sehen wir spätesten wenn wir versuchen, die nächsten Int3s, von denen wir wissen dass es Stolen Bytes sind ausführen. Sie werden nicht mehr überschrieben.

Das scheint mir ein vordergründig guter Schachzug von Macrovision zu sein. Unter normalen Umständen werden diese Bytes NIE ausgeführt, da sie immer nur nach Subroutinen auftreten, und vor ihnen immer ein RET steht. Werden sie also von uns ausgeführt weiss die Protection, dass wir versuchen, sie zu cracken und ist uns nicht mehr behilflich...

Wiso schreibe ich dann vordergründig? Hehe, nun da wir bescheid wissen, werden wir einfach die gesamte .text Section nach der Bytefolge "C3 CC CC CC CC CC 90" durchsuchen und diese NOPen. Bleibt nur noch das Problem zu lösen, wie wir herausfinden ob es sich bei den gefundenen Int3s wirklich um stolen Bytes handelt. Dazu bedarf es einiger Sucharbeit, da wir möglichst viele richtige stolen Bytes ausfindig machen müssen um Gemeinsamkeiten zu erkennen.

Was uns zuerst auffällt, ist dass alle richtigen stolen Bytes mindestens 2 Byte lang sind, ausserdem steht sehr oft vor richtigen stolen Bytes ein Call [.ref], allerdings nicht immer. Wenn wir aber einmal unser Augenmerk darauf lenken, werden wir sehen dass immer kurz über den stolen Bytes ein solcher Call [.ref] steht. Der größte Abstand, den ich von dem ersten Int3 des Blocks bis zum FF des Anfangs des Call [.ref]s gesehen habe, betrug 2C Bytes. Das wird von Game zu Game und Version zu Version allerdings wahrscheinlich variieren.

Unser Plan lautet also folgendermaßen:
Wir werden die .text Section nach CC Bytes und FF15 words durchsuchen. Die potentiellen Call [.ref]s müssen allerdings noch überprüft werden, ob sie unsere IAT lesen. Alles bis auf unsere Int3s und verifizierten Call [.ref]s wird mit dem C3 Opcode überschrieben. Jetzt werden wir die .text Section dumpen. Danach führen wir alle Int3s aus, die einen maximalen Absstand von -2C von einem Call [.ref] haben und dumpen nochmal wenn alles recovered ist. Nun können wir die beiden Files mit einer Patchengine wie Code Fusion vergleichen und die Veränderungen in unsere weiter oben gedumpte und gefixte .text Section schreiben lassen.

Wenn wir das ganze jetzt coden würden, würden wir 2 neue Probleme entdecken, also werde ich diese jetzt vorweg erklären um uns den Ärger zu sparen:

1. Das Problem mit den 3 Int3s
2. Das Problem mit den 7 Int3s

Das Fixen der ersten paar stolen Bytes funktionier wunderbar. Das 1. Problem tritt allerdings dann doch recht schnell auf, und zwar nur wenn 3 Int3s ausgeführt werden. Das liegt daran, dass einige dieser 3er Blocks Int3s "add esp, XX"s sind (die eine länge von 3 Bytes haben). Da sie nach dem schreiben in das Segment ausgeführt werden, stimmt die RETurn Adresse des Stacks nicht mehr und bei dem nächsten RET enden wir im Nirvana. Wir müssen also bevor wir irgendein Int3 ausführen, nach allen 3er Blocks Int3s einen Jump zu einer Stack-Correct Routine assembeln

Das 2. Problem tritt gegen Schluss der .text Section auf. Einige Blocks Int3s sind "mov dword ptr [esi], XX" Wo liegt das Problem? werden sich manche vielleicht fragen. Nun ja, dann gucken wir uns doch einmal esi an. Es hat den Wert 00000000, welche eine Invalide Speicheradresse ist, für die unser Game keine Schreibrechte hat. Das Resultat ist wieder eine Terminierung... Hier können wir uns helfen, indem wir zu Beginn unserer Int3Fix Routine esi auf 00933000, das Offset der Safedisc Loader Section setzen.

Alles verstanden? Dann coden wir:

_CODEPART5:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;   Recovern der stolen Bytes   ;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

mov esi, 00933000              ;Setze esi auf eine Adresse mit Schreibrechten
mov eax, 00401000              ;Anfang der .text Section


_SucheNachInt3sUndCallRefs:

cmp byte ptr [eax], CC         ;Ist die aktuelle Funktion ein Int3?
je _Int3Gefunden
cmp word ptr [eax], 15FF       ;oder ein Call [.ref]?
je _CODEPART5CallRefGefunden


_UeberschreibeByte:

mov byte ptr [eax], C3         ;ansonsten überschreibe sie
inc eax                        ;und mache mit dem nächsten Byte weiter
cmp eax, 00782FFF              ;sind wir fertig?
jl _SucheNachInt3sUndCallRefs

jmp _SucheNach3erBlockInt3s:


_Int3Gefunden:

cmp word ptr [eax+04], 90CC    ;Haben wir es mit einem Macrovision Trick zu tun?
je _MacrovisionTrickGefunden
inc eax                        ;Sonst überschreibe das aktuelle Byte nicht und mache weiter
jmp _SucheNachInt3sUndCallRefs


_MacrovisionTrickGefunden:

mov dword ptr [eax], 90909090  ;NOPe den Macrovision Trick
mov byte ptr [eax+04], 90      ;"
add eax, 00000006              ;überprüfe die nächste Funktion
jmp _SucheNachInt3sUndCallRefs


_CODEPART5CallRefGefunden:
cmp dword ptr [eax+02], 00783000   ;ließt unser potentieller Call [.ref]
jl _UeberschreibeByte
cmp dword ptr [eax+02], 00784000   ;eine Adresse aus unserer IAT?
ja _UeberschreibeByte              ;wenn nicht, überschreibe ihn
add eax, 00000006                  ;sonst spare ihn aus
jmp _SucheNachInt3sUndCallRefs


_SucheNach3erBlockInt3s:

mov eax, 00401000              ;Anfang der .text Section


_SucheNach3erBlockInt3sLoop:

cmp word ptr [eax], CCCC       ;befinden sich am aktuellen Offset 2 Int3s?
je _2Int3sGefunden


_NaechstesInt3:

inc eax                        ;Sonst erhöhe das Offset
cmp eax, 00782FFF              ;Ende?
jl _SucheNach3erBlockInt3sLoop

jmp eip                        ;Loop, jetzt sollten wir die .text Section dumpen (nfsu2.stolen.bytes.not.recovered.dat)
jmp _SucheNachInt3s            ;und mit dem letzten Teil fortfahren :)


_2Int3sGefunden:

cmp byte ptr [eax+02], CC      ;Ist das 3. Byte auch ein Int3?
jne _NaechstesInt3             ;Wenn nicht, suche weiter
cmp byte ptr [eax+03], C3      ;Ist es das letzte oder folgen noch welche?
jne _NaechstesInt3             ;Wenn nicht das letzt, suche weiter
cmp byte ptr [eax-01], CC      ;Sind noch Int3s vor dem Überprüften?
je _NaechstesInt3              ;Wenn ja, suche weiter

add eax, 00000003              ;setze eax auf das Offset nach den Int3s.
mov ebx, eax                   ;setze abx auf den Wert von eax
add ebx, 00000005              ;addiere die Long Jump Länge dazu
sub ebx, _CorrectStack         ;und ziehe das Ziel Offset ab
imul ebx, FFFFFFFF             ;Wir wollen zurück springen, also müssen wir das ganze mal (-1) nehmen
mov byte ptr [eax], E9         ;Setze den Opcode für einen Long Jump
mov dword ptr [eax+01], ebx    ;Und die passende Länge des Jumps 
jmp _NaechstesInt3


_CorrectStack:

cmp word ptr [eax], C483       ;Waren die 3 stolen Bytes ein "add esp, XX"
jne _MussNichtKorrigiertWerden
movzx ebx, byte ptr [eax+02]   ;setze ebx auf das "XX"
sub esp, ebx                   ;und ziehe diesen Wert wieder vom Stack ab


_MussNichtKorrigiertWerden:

ret                            ;Kehre zurück


_SucheNachInt3s:

mov eax, 00401000              ;Anfang der .text Section


_SucheNachInt3sLoop:

cmp word ptr [eax], CCCC       ;sind die aktuellen 2 Bytes potentiell stolen?
je _TesteInt3                  ;wenn ja, teste sie
inc eax                        ;sonst überprüfe die nächsten
cmp eax, 00782FFF              ;Haben wir das Ende der .text Section erreicht?
jl _SucheNachInt3sLoop:

jmp eip                        ;Endlos-Loop, Wir haben es GESCHAFFT!!!


_TesteInt3:

mov ebx, eax                   ;setze ebx auf den Wert von eax
sub ebx, 0000002C              ;und ziehe die maximal zulässige  Entfernung zu einem Call [.ref] davon ab


_TesteInt3Loop:

cmp word ptr [ebx], 15FF       ;Liegt ein Call [.ref] im aktuellen Abstand zum Int3
je _CalleInt3
inc ebx                        ;wenn nicht, verringere den Abstand
cmp ebx, eax                   ;haben wir den gesamten maximal zulässigen Abstand untersucht?
jne _TesteInt3Loop             ;Wenn nicht, mache weiter
add eax, 00000002              ;Sonst calle das aktuelle Int3 nicht
jmp _SucheNachInt3sLoop        ;und suche nach weiteren


_CalleInt3:

pushad                         ;Sichere alle Register
pushfd                         ;und den Stack
call eax                       ;Calle das Int3 offset, um die stolen Bytes zu recovern
popfd                          ;Stelle den Stack
popad                          ;und alle Register wieder her
cmp word ptr [eax], CCCC       ;Sind die stolen Bytes recovered worde?
je _CalleInt3                  ;wenn nicht, calle sie noch einmal
add eax, 00000002              ;Sonst suche die nächsten
jmp _SucheNachInt3sLoop


Phew... das war's fast. Beim Durchlaufen der Routine sollten wir trotzdem vorsichtshalber einen Breakpoint auf den NT Exception Dispatcher setzen, der aufgerufen wird, wenn wir versuchen ein stolen Byte zu recovern, das gar kein stolen Byte ist.

:bpx KiUserExceptionDispatcher

Zack, siehe da, wenn wir unseren Code laufen lassen, breaken wir irgendwann bei KiUserExceptionDispatcher. Wenn wir jetzt

:u eax

drücken sehen wir, dass es sich eigentlich laut unseren Kriterien um stolen Bytes handleln müsste. Kurz über den int3s ist ein Call zu Kernel32.CreateMutexA. Des Rätsels Lösung ist, dass es ab einem bestimmten Offset keine stolen Bytes mehr gibt. Bei NFSU2 ist das ca. 00730000. Es ist nun also schon alles gefixed. Alles was wir in SI noch machen müssen, ist die Section zu dumpen.

:!dump \??\C:\nfsu2.stolen.bytes.recovered.dat 00401000 00382000

Wir können SI jetzt beenden. Stattdessen öffnen wir Code Fusion und erstellen ein neues Projekt. Als File, welches gepatcht werden soll, geben wir "Any File" an, indem wir auf den entsprechenden Button klicken.
Bei "Data to patch" wählen wir "File Compare aus" und stellen die Optionen wie folgt ein:



Als Originales File geben wir unsere .text Section an, die nach dem überschreiben mit C3 Bytes gedumpt wurde. Als gepatchtes File geben wir unsere .text Section an, bei der wir die stolen Bytes recovered haben. Bei "Save Original Data in project" entfernen wir den Haken. So erreichen wir, dass wir unsere gefixte .rdata Section (Imports/Interne Calls) mit dem erstellten Patch updaten können.

Klicken wir also auf Compare und wir erhalten eine nette Liste von Unterschieden, mit denen wir jetzt den Patch "Int3fix.exe" erstellen können.

Diese exe rufen wir auch sofort auf und geben als Target File unsere gefixte .text Section an (nfsu2.text.section.fixed.dat). Wenn das File upgedatet ist dürfen wir es guten Gewissens "nfsu2.text.section.fully.fixed.dat" nennen :)

6. Rebuilden:

Wir öfnnen nun also unseren weiter oben erstellten Full Dump im Hex Workshop, begeben uns ans Offset 1000, wählen Edit=>Select Block und geben als Wert "00381821" (Raw Size der .text Section) an. Wir können jetzt getrost entf drücken um die gesamte Section zu löschen.

Dann öfnen wir unsere "nfsu2.text.section.fully.fixed.dat" im selben Hex Workshop, markieren in ihr ebenfalls 00381821 Bytes, kopieren sie und fügen sie bei Offset 1000 des Full Dumps ein. Zusätzlich können wir die letzten 7000h Bytes (gehörten mal zu den beiden Safedisc Sections) einfach löschen und das ganze dann speichern.

Mit dem PEditor können wir auch noch die letzten 2 Sections, die wir schon aus dem File entfernt haben aus dem Header köschen.

Moment, da war doch noch was... Genau, wir haben die Imports noch gar nicht gerebuilded. Um das zu tun, müssen wir nochmal die Originale Speed2.exe starten, am OEP breaken und unseren Patch für die Imports (bloß nicht den für die stolen Bytes ;D...) laufen lassen und anschließend die alte mit der neuen IAT überschreiben.

Jetzt müssen wir den Imprec starten, Speed2.exe in der Liste der aktiven Prozesse auswählen, den OEP eingeben und auf "IAT autosearch" klicken. Danach "Fix Dump" und unseren Dump auswählen... FERTIG!!!

Zum Abschluss sollten wir die exe noch mit LordPE, PEditor, oder einen PE Rebuilder unserer Wahl rebuilden.

7. Schlusswort

So, ein langes Tutorial geht zu Ende. Ich hoffe euch hat das Lesen Spaß gemacht und ihr habt etwas gelernt.

Grüße an (no particular Order): das gesamte CIP Team (ZeroJump,BigDragon,*RemedY*), old genius r!sc, Peex, quake_qer und alle anderen aus dem Chan, mein Soldier Girl & meine Vatos, und last but not least an DICH


kEEP iT uP...

mAGiC...

Hast Du noch Fragen oder Probleme mit dem Tutorial? Zögere nicht mir eine EMail zu schicken, ich werde versuchen zu helfen: Contact

Besuch uns wieder unter http://cip.myz.info/

oder unser Board: http://board.cip.myz.info/