| 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 | |
| |
|
1. Einleitung 2. Vorbereitungen 3. Imports
fixen 4. Redirectete interne Calls fixen 5. Stolen Bytes
recovern 6. Rebuilden 7. Schlusswort
|
|
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.
|
|
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
|
|
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
:)
|
|
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.
|
|
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/ | |