versionsunabhängige Game-Trainer bauen

Hier könnt Ihr gute, von Euch geschriebene Codes posten. Sie müssen auf jeden Fall funktionieren und sollten möglichst effizient, elegant und beispielhaft oder einfach nur cool sein.
Benutzeravatar
Thorium
Beiträge: 1722
Registriert: 12.06.2005 11:15
Wohnort: Germany
Kontaktdaten:

versionsunabhängige Game-Trainer bauen

Beitrag von Thorium »

In letzter Zeit gabs ja mal Fragen zu Trainern und Speicherzugriff, etc.
Hier will ich mal was weiterführendes zum Thema Trainer ansprechen: Versionsunabhängigkeit oder zumindest Versionstolleranz. ^^

Es gibt nicht viele Trainer, die dieses Feature haben. Ich kenn eigentlich sogar garkeinen, aber möglich ist das.

Ok, die Sache ist nicht ganz einfach. Das geht weit über normale simple Trainer, die Instruktionen statisch auf fixen Adressen patchen hinaus. Und ich hab das vor kurzem erst selbst zum ersten mal gemacht. Drumm ist das hier kein Tutorial und ich würde mich über eine Diskussion zu dem Thema freuen, falls hier genügend Leute mit Intresse an Trainern sind. Es gibt sicherlich noch einiges zu verbessern an meiner Methode.

Um das ganze zu verstehen sind folgende Kenntnisse von Nöten:
  • Grundkenntnisse in 32 Bit x86 Assembler
  • Kenntnisse über das Speichermodell unter Win32/64
  • Kenntnisse über die generelle Funktionsweise von Game-Trainern
  • Wissen was man unter DLL-Injection versteht
Solltet ihr von einer oder mehrer Sachen davon keine Ahnung haben, bitte macht euch erst darüber schlau. Gibt für alles ausreichend Infos und Tutorials im Netz.

Damit ihr euch eine Vorstellung machen könnt, beschreibe ich euch erstmal wofür ich das in meinem Projekt brauche und wie ich das im groben umgesetzt habe. Details zu dem Projekt darf ich leider keine nennen. Vieleicht zu einem späteren Zeitpunkt. Was ich sagen darf ist, das es sich um ein Hackingtool handelt, welches dazu designt ist dabei zu unterstützen ein bestimmtes Spiel auf Cheat-Schwachstellen während der Betaphase zu testen.

Ok, los gehts: Mein Tool hookt die ingame-Konsole des Spiels um dort seine eigenen Kommandos zu implementieren. Das ganze läuft natürlich über einen Inline-Hook. Das heisst mit jedem neuen Build des Spiels ändert sich die Adresse und in diesem speziellen Fall auch Pointeradressen, welche mein Tool benötigt um Text in der Konsole auszugeben. Das ist ziemlich nervig da aktuell wöchentlich eine neue Beta-Version des Spiels gebaut wird.
So kam die Idee das ganze etwas dynamischer zu machen.

In diesem Fall können wir davon ausgehen das sich der Zielcode selbst nicht ändert, da es sehr unwarscheinlich ist, das große Änderungen am Konsolencode nötig sind. Allerdings sorgen die massiven Codeänderungen bei jedem Build dafür das sich Codeadressen und Pointeradressen verschieben. Lösung ist es nun sich den Code anzuschauen und nun nach seinem binären Muster zu suchen. Ist dieses gefunden kann man daran die Adresse zum Codepatchen ermitteln und die Pointeradressen aus dem Code auslesen.

Damit das ganze sehr schnell über die Bühne geht sollte man zum einen mit DLL-Injection arbeiten um direkte Operationen auf den virtuellen Speicher ausführen zu können und nicht den Umweg über ReadProcessMemory gehen zu müssen. Zweitens sollte man sich die Speicherregionen mit Ausführengsrechten rauspicken und nur in dehnen suchen. Um es nochmal zu optimieren kann man dann auch nur auf das jeweilige Modul begrenzen. Aber wenn man alleine schon die ganzen Datenregionen ausschließt läuft die binäre Suche sowieso im Sekundenbruchteil ab.

Hier mal ein Beispielcode zum raussuchen der Coderegionen im Prozessspeicher:
PureBasic 4.20 Code

Code: Alles auswählen

Structure Region
  BaseAddr.l
  Size.l
EndStructure

Procedure.l GetExecutableMemRegions(RegionList.Region(1))

  Define.l Addr,RetVal,RegionCnt
  Define.MEMORY_BASIC_INFORMATION MemoryInfo

  RegionCnt = -1

  Repeat
  
    Addr = Addr + MemoryInfo\RegionSize
    RetVal = VirtualQuery_(Addr,MemoryInfo,SizeOf(MEMORY_BASIC_INFORMATION))
    
    If RetVal > 0 
      If MemoryInfo\State = #MEM_COMMIT
      
        If MemoryInfo\Protect = #PAGE_EXECUTE Or MemoryInfo\Protect = #PAGE_EXECUTE_READ Or MemoryInfo\Protect = #PAGE_EXECUTE_READWRITE Or MemoryInfo\Protect = #PAGE_EXECUTE_WRITECOPY
        
          RegionCnt = RegionCnt + 1
          ReDim RegionList.Region(RegionCnt)
          RegionList(RegionCnt)\BaseAddr = MemoryInfo\BaseAddress
          RegionList(RegionCnt)\Size = MemoryInfo\RegionSize
          
        EndIf
      
      EndIf  
    EndIf
  
  Until RetVal < 1

  ProcedureReturn RegionCnt

EndProcedure
Nun die binäre Mustersuche. Ich hab dafür keine schöne Prozedur, wenn da jemand was hat, wäre auf jeden Fall für nen Code dankbar. Ich hab das einfach im Augenblick direkt in Assembler geschrieben.

Nehmen wir ein simples Beispiel.
Das ist eine Codestelle im Spiel, welche Text auf der Konsole ausgibt. Mein Tool soll nun die Adresse der Ausgabeprozedur herausfinden, welche dort aufgerufen wird und ausserdem noch eine Pointeradresse, von der ich keine Ahnung habe wozu die gut ist aber ich muss den Pointer als Parameter für die Textausgabe verwenden. Solange es funktioniert is mir erstmal wurscht wozu der Pointer gut ist. ;)

Code: Alles auswählen

00401960    68 8C205E00     PUSH 005E208C
00401965    FF35 24996600   PUSH DWORD PTR DS:[669924]
0040196B    E8 434C1100     CALL 005165B3
00401970    59              POP ECX
00401971    59              POP ECX
PUSH 005E208C Übergabe des Textes als Parameter (Adresse des ASCII-Strings)
PUSH DWORD PTR DS:[669924] Übergabe eines unbekannten Pointers
CALL 005165B3 aufruf der Prozedur zur Textausgabe
POP ECX Stack aufräumen
POP ECX Stack aufräumen

Daraus ergibt sich folgendes Binärmuster. Die Fragezeichen symbolisieren Bytes, die sich ändern können.

Code: Alles auswählen

68 ?? ?? ?? ?? FF 35 ?? ?? ?? ?? E8 ?? ?? ?? ?? 59 59
Wenn wir nach diesem Muster suchen, sollten wir die Adresse des Codes bekommen. Anhand der Adresse des Codes können wir dann die Parameter von "DWORD PTR DS:[669924]" und "CALL 005165B3" auslesen um damit unsere eigene Konsolentextausgabe dynamisch zur Laufzeit zu bauen, bzw. als Parameter für eine entsprechend dynamisch angelegte Prozedur verwenden.

PureBasic v4.20 Code

Code: Alles auswählen

  Define.l i,RegionCnt,RegionStart,RegionSize,FoundAddr
  Dim RegionList.Region(0)
  
  RegionCnt = GetExecutableMemRegions(RegionList())
  
  For i = 0 To RegionCnt

    RegionStart = RegionList(i)\BaseAddr
    RegionSize = RegionList(i)\Size - 20
    FoundAddr = 0
    
    !mov edi,[p.v_RegionStart]
    !mov ecx,[p.v_RegionSize]
    !mov esi,edi
    !add esi,ecx
    
    !GCP_LoopStart:
    
    !mov al,$68
    !repne scasb
    
    !mov ax,[edi+4]
    !cmp ax,$35FF
    !jne GCP_NotFound
    
    !mov al,[edi+10]
    !cmp al,$E8
    !jne GCP_NotFound
    
    !mov ax,[edi+15]
    !cmp ax,$5959
    !jne GCP_NotFound
        
    !mov [p.v_FoundAddr],edi
    !jmp GCP_Found
    
    !GCP_NotFound:
    !cmp edi,esi
    !jl GCP_LoopStart
    
    !GCP_Found:
    
    If FoundAddr <> 0
      
      ConsolePointer = PeekL(FoundAddr+6)
      ConsoleOutputProcAddr = PeekL(FoundAddr+11) + FoundAddr + 15

      Break
    
    EndIf
  
  Next
Das ganze funktioniert nun schon die letzten 2 Builds und ich hoffe das ich da auch für nächsten Builds nichts updaten brauche.

Was haltet ihr davon? Wo seht ihr Probleme, Verbesserungsmöglichkeiten, Schwachstellen?

Vorallem für ne Prozedur mit der man binär den Speicher durchsuchen kann und die Wildcards unterstützt, wäre ich dankbar.
Zu mir kommen behinderte Delphine um mit mir zu schwimmen.

Wir fordern mehr Aufmerksamkeit für umfallende Reissäcke! Bild
Benutzeravatar
edel
Beiträge: 3667
Registriert: 28.07.2005 12:39
Computerausstattung: GameBoy
Kontaktdaten:

Beitrag von edel »

Sehr schoene Idee. Sollte ich nochmal eine Pluginschnittstelle fuer
den PB Editor basteln, werd ich das einbauen. Fuer mich war es
mehr als laestig, fuer jede Version den Code anzupassen.

:allright:

Edit:

Hier mal eine Funktion mit der man suchen kann

Code: Alles auswählen

ImportC ""
  pcre_exec(*pcre,*extra,subject.l,length,startoffset,options,*ovector,ovecsize)
EndImport

Procedure SearchCode(pattern.s,code,codelen)
  Protected Dim ovec(30)
  Protected regex.l
  Protected result.l
  
  regex = CreateRegularExpression(#PB_Any,pattern)
    
  If regex    
    If pcre_exec(PeekL(regex),0,code,codelen,0,0,@ovec(),30) > 0
      result = code + ovec(0)
    EndIf     
    
    FreeRegularExpression(regex)    
  EndIf 
   
    
  ProcedureReturn result
EndProcedure

pattern.s = "(\x68[\x00-\xff]{4}\xFF\x35[\x00-\xff]{4}\xE8[\x00-\xff]{4}\x59\x59)"

; [\x00-\xff]{4} steht fuer 4 hexwerte im bereich $0 bis $FF

Debug SearchCode(pattern,?label0,?label3-?label0)

DataSection
  label0:
  Data.b $69,$AA,$AA,$AA,$0,$FF,$35,$AA,$AA,$AA,$AA,$E8,$AA,$AA,$AA,$AA,$59,$59
  label1:
  Data.b $00,$68,$AA,$AA,$AA,$0,$FF,$35,$AA,$AA,$AA,$AA,$E8,$AA,$AA,$AA,$AA,$59,$59
  label2:
  Data.b $52,$AA,$AA,$AA,$0,$0,$FF,$35,$AA,$AA,$AA,$AA,$E8,$AA,$AA,$AA,$AA,$59,$59
  label3:
EndDataSection

Benutzeravatar
Thorium
Beiträge: 1722
Registriert: 12.06.2005 11:15
Wohnort: Germany
Kontaktdaten:

Beitrag von Thorium »

Sehr cool, danke für die Prozedur. :)
Zu mir kommen behinderte Delphine um mit mir zu schwimmen.

Wir fordern mehr Aufmerksamkeit für umfallende Reissäcke! Bild
Benutzeravatar
Helle
Beiträge: 566
Registriert: 11.11.2004 16:13
Wohnort: Magdeburg

Beitrag von Helle »

Falls die Suchfunktion doch mal zeitkritisch ist, ist die Verwendung der SSE-Befehle durchaus eine Überlegung wert. Hier ein Beispiel mit Teststellung, deshalb etwas umfangreicher:

Code: Alles auswählen

;- benötigt CPU mit SSE2; hier ohne Test darauf, sollte inzwischen allgemein verfügbar sein
Global RegionStart.l
Global RegionSize.l = 50000  ;Testwert, üblicherweise durch 16 teilbar, hier für Test egal
Global FoundAddr.l
Global BitMuster.l
Global Abstand.l

Global TWert0.w              ;nur für Test-Anzeige 
Global TWert1.l
Global TWert2.l
Global TWert3.l
Global TWert4.l

Structure DQuad
  DQuad.q[4]                 ;4*8=32 Bytes "am Stück" (wichtig 2*16) reservieren
EndStructure 

Global *DQuad.DQuad = DQuad.DQuad

DQuad\DQuad[0] = $6868686868686868     ;16-mal das erste zu suchende Byte
DQuad\DQuad[1] = $6868686868686868
DQuad\DQuad[2] = $00E80000000035FF     ;die danach zu suchenden Bytes; Low-Quad  
DQuad\DQuad[3] = $0000005959000000     ;High-Quad

BitMuster = %1100001000011             ;BitMuster-Belegung der zu suchenden Bytes, Bit0 entspricht hier $FF
Abstand = 4                            ;Abstand vom "General-Byte" ($68) zum nächsten Such-Byte ($FF)

RegionStart = AllocateMemory(RegionSize)    ;für Test

For i = 0 To RegionSize - 1
  PokeB(RegionStart + i, Random(255))  ;für Test den Speicher mit Zufall-Bytes füllen
Next

;68 ?? ?? ?? ?? FF 35 ?? ?? ?? ?? E8 ?? ?? ?? ?? 59 59 von Thoriums Beispiel     
SuchTest = RegionStart + Random(RegionSize - 16) ;zufällige Adresse für Test
PokeB(SuchTest, $68)         ;für Test mit dem Such-Muster füllen 
PokeW(SuchTest + 5, $35FF)   ;Little-Endian !
PokeB(SuchTest + 11, $E8)
PokeW(SuchTest + 16, $5959)

Procedure SuchSequenzSSE()   ;ohne Parameter, da globale Variablen
  !mov esi,[v_RegionStart]    ;Start-Adresse für Speicher durchsuchen 
  !movdqu xmm1,dqword[v_DQuad]    ;in xmm1 (16 Bytes) jeweils $68 als Byte-Wert einlesen  movdqa 
  !movdqu xmm2,dqword[v_DQuad+16] ;in xmm2 die Such-Bytes einlesen 
  !mov ecx,[v_RegionSize]
  !shr ecx,4                 ;RegionSize / 16
!Schleife1:
  !movdqu xmm0,dqword[esi]   ;16 Bytes aus dem Speicher in xmm0 einlesen. movdqa, wenn 16-er Alignment von RegionStart (normalerweise ja)
  !pcmpeqb xmm0,xmm1         ;in einem Rutsch jedes dieser 16 Bytes mit $68 vergleichen
  !pmovmskb eax,xmm0         ;in ax (Bitmuster) ist das entsprechende Bit gesetzt, wenn identisch 
  !or eax,eax                ;Test auf Null, eigentlich nur ax interessant
  !je NoFind                 ;in diesen 16 Bytes (=16 Bits in ax) ist kein Byte mit Wert $68 vorhanden
  !mov edi,esi
!Schleife2:                  ;Adresse von gesetzten Bits ermitteln
  !inc edi
  !shr eax,1                 ;gesetzte(s) Bit(s) suchen
  !jnc Schleife2             ;nicht gesetzt
  !mov ebx,[v_Abstand]
  !movdqu xmm0,dqword[edi+ebx]
  !pcmpeqb xmm0,xmm2
  !pmovmskb edx,xmm0
  !and edx,[v_BitMuster]
  !cmp edx,[v_BitMuster]
  !je Ende
  !or eax,eax                ;eax schon Null?
  !jne Schleife2             ;nein, weitersuchen; mehr als ein Byte mit Wert $68 in diesem 16-er Block vorhanden  
!NoFind:
  !add esi,16
  !dec ecx
  !jnz Schleife1
  !mov edi,1                 ;wird dann FoundAddr = 0, also nichts gefunden
!Ende:
  !dec edi                   ;Korrektur von Schleife2
  !mov [v_FoundAddr],edi     ;Adresse des 1.Bytes    
EndProcedure 

SuchSequenzSSE()

If FoundAddr                 ;hier zum Test Anzeige
  ;!mov edi,[v_FoundAddr]     ;allgemein
  !mov ax,[edi]              ;Aufteilung der anzuzeigenden 18 Bytes ist willkürlich gewählt
  !xchg al,ah                ;Little-Endian !
  !mov [v_TWert0],ax
  !mov eax,[edi+2]
  !bswap eax                 ;Little-Endian !
  !mov [v_TWert1],eax
  !mov eax,[edi+6]
  !bswap eax
  !mov [v_TWert2],eax
  !mov eax,[edi+10]
  !bswap eax
  !mov [v_TWert3],eax
  !mov eax,[edi+14]
  !bswap eax
  !mov [v_TWert4],eax

  Result$="Für Test Byte-Folge an Adresse "+Str(SuchTest)+" gesetzt"+#LFCR$  
  Result$+"Byte-Folge an Adresse "+Str(FoundAddr)+" gefunden :"+#LFCR$
  Result$+RSet(Hex(TWert0),4,"0")+RSet(Hex(TWert1),8,"0")+RSet(Hex(TWert2),8,"0")+RSet(Hex(TWert3),8,"0")+RSet(Hex(TWert4),8,"0")
  MessageRequester("Ergebnis", Result$)
 Else 
  MessageRequester("Fehlschlag !", "Keine Übereinstimmung gefunden !")
EndIf
War bei Tests je nach CPU 3- bis 8-mal schneller als mit SCASB.

Gruß
Helle
Antworten