Im folgenden teile ich mit euch meine bisherigen Erfahrungen bezüglich dem Thema Speichermanipulation / Gamehacking in Java. Vorausgesetzt werden Kenntnisse in Java, C und C++.
Der direkte Zugriff auf Funktionen der WinApi ist in Java afaik leider nicht möglich. Deshalb ist es auch nicht möglich Funktionen wie Read-/WriteProcessMemory aus einem Java Programm aufzurufen, um fremden Speicher zu verändern oder auszulesen. Will man diese Funktionen trotzdem verwenden, muss man einen kleinen Umweg gehen.
Die Idee besteht darin eine native Dll zu programmieren, die entsprechende Funktionen bereitstellt, über die aus dem Java Programm auf die WinApi Funktionen zugegriffen werden können. Das Problem hierbei ist wiederum die Frage, wie der native Code der Dll aus einem Java Programm aufgerufen werden kann. Die Lösung dieser Frage ist das Java Native Interface (JNI). Das JNI erlaubt Java Programmen auf einfache Weise nativen Code aufzurufen (Anmerkung: das ganze lässt sich auch umdrehen, sodass es auch möglich ist Bytecode in einem nativen Programm aufzurufen).
Im Folgenden soll anhand eines kleinen Beispiels gezeigt werden, wie man aus einem Java Programm heraus via der WinApi Funktion WriteProcessMemory den Speicher eines fremden Prozesses verändern kann.
1. Das Zielprogramm programmieren:
Als erstes schreibe ich mir ein kleines "Opfer"-Programm, bei dem der Wert einer Variable durch unser Java Programm verändert werden soll (C++-Code):
Code:
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
SetConsoleTitle("Target");
int value = 1337;
cout << "Wert: " << value << "\n";
cout << "Adresse: " << &value;
cin.get();
cout << "Neuer Wert: " << value;
cin.get();
return 0;
}
Via SetConsoleTitle wird der Titel des Konsolenfensters geändert. Im Java Programm kann das Zielprogramm dann über diesen Titel festgelegt werden. Der Wert der Variable value soll dann durch das Java-Programm verändert werden. Es wird zuerst der Startwert ausgegeben, dann die Adresse der Variable im Speicher.
Später kann die Adresse so abgelesen und im Java-Programm eingegeben werden. Es wird dann auf eine Eingabe gewartet, sodass Zeit bleibt den Speicher über unser Programm zu ändern. Nachdem das geschehen ist, wird der neue (hoffentlich) veränderte Wert der Variable ausgegeben.
2. Das Java Programm:
Der Java-Quellcode eines Programms, das eine native Funktion aufruft, unterscheidet sich kaum von dem Code eines Programms, das keine native Funktion aufruft. Drei Dinge müssen jedoch beachtet werden:
- Native Methoden werden lediglich deklariert
- Bei der Deklaration wird dem Compiler durch das Schlüsselwort native mitgeteilt, dass es sich hierbei um eine native Methode handelt
- Die Dll, in der die nativen Methoden ausgelagert werden, muss vom Java Programm geladen werden
Der Quellcode eines Java-Programms, das vom Nutzer Fenstertitel des Zielprozess, Speicheradresse der zu verändernden Variable und den neuen Wert der Variable entgegennimmt und anschließend die native Methode writeMemory aus einer Dll (wapi.dll; muss erst noch programmiert werden) aufruft, könnte daher in etwa so aussehen:
Code:
import java.util.Scanner;
public class Winapi {
// Lade Dll, die die native Funktion enthält (wapi.dll)
static {
try {
System.loadLibrary("wapi");
} catch (UnsatisfiedLinkError ex) {
System.err.println("Konnte Library nicht laden: " + ex.toString());
}
}
// Deklaration der nativen Methode
public static native boolean writeMemory(String windowTitle,
long baseAddress, int value);
public static void main(String[] args) {
String windowTitle = new String();
long baseAddress = 0;
int value = 0;
// Daten von Nutzer eingeben lassen
Scanner inputScanner = new Scanner(System.in);
System.out.print("Fenstertitel des Zielprozess: ");
windowTitle = inputScanner.nextLine();
System.out.print("Addresse: ");
baseAddress = inputScanner.nextLong();
System.out.print("Neuer Wert: ");
value = inputScanner.nextInt();
inputScanner.close();
// Native Funktion aufrufen
if (Winapi.writeMemory(windowTitle, baseAddress, value)) {
System.out.println("writeMemory war erfolgreich");
} else {
System.err.println("writeMemory ist fehlgeschlagen");
}
}
}
3. Die Dll programmieren:
Damit das obige Java Programm auch läuft, muss jetzt noch die wapi.dll programmiert werden. Ich werde dies in der Programmiersprache C tun. Das JDK erleichtert einem ein wenig die Arbeit, indem es ein Tool (javah) zur Verfügung stellt, das aus dem Java-Quellcode einen fertigen Header generiert, in dem neben der Funktionsdeklaration der nativen Funktion auch alle für das JNI notwendigen Includes vorhanden sind. Mit folgendem Konsolenbefehl wird der Header erstellt:
Code:
javah –jni <name der Java-Class>
Als erstes muss der generierte Header genauer betrachtet werden, insbesondere die Deklaration der Funktion:
Code:
JNIEXPORT jboolean JNICALL Java_Winapi_writeMemory (JNIEnv *, jclass, jstring, jlong, jint);
Dieser Zeiger zeigt auf eine Funktionstabelle irgendwo im Speicher. Jeder Eintrag in dieser Tabelle enthält wiederum einen Zeiger auf eine JNI-Funktion. Diese JNI-Funktionen werden u.a. dafür benötigt, um mit den JNI-Datentypen zu arbeiten. Ein Blick auf die Parameter der Funktionsdeklaration und man sieht sofort, was gemeint ist: jstring, jlong, jint.
Was hat es mit diesen seltsamen Datentypen auf sich? Nimmt man Beispielsweise den jstring Parameter her, wird sofort klar, wieso diese extra Datentypen benötigt werden. Wie sollte sonst eine Java-String-Objekt an ein natives Programm übergeben werden? C kennt zum Beispiel keine Klassen und Java-Klassen kennt es dann erst recht nicht. Wird also ein String Objekt an eine native Funktion übergeben, erhält die native Funktion eine Variable vom Typ jstring. Wird eine Variable vom Typ long an eine native Funktion übergeben, erhält diese eine Variable vom Typ jlong, usw.
Abschließend ist es noch wichtig zwischen den Java-Datentypen zu unterscheiden. So gibt es in Java primitive Datentypen (int, long, double,…) und Referenzdatentypen (String, Arrays, …). Der JNI-Datentyp von primitven Datentypen korrespondiert dabei jeweils zu einem nativen Datentyp in C. Folgende "Tabelle" soll das veranschaulichen:
Code:
Java Type Native Type Description
boolean jboolean unsigned 8 bits
(unsigned char)
byte jbyte signed 8 bits
(signed char)
char jchar unsigned 16 bits
(unsigned short)
short jshort signed 16 bits
(short)
int jint signed 32 bits
(long)
long jlong signed 64 bits
(long long)
float jfloat 32 bits
(float)
double jdouble 64 bits
(double)
void void N/A
Das sollte als kleiner Ausflug genügen. Wen das näher interessiert, der sollte sich wirklich mit der JNI Spezifikation auseinander setzen. Zurück zur eigentlichen Programmierarbeit. Während man nun einfach mit den jlong und jint Parametern normal weiterarbeiten kann, muss die Zeichenkette der jstring Variable erst in ein char-Array kopiert werden. Dies geschieht mittels der JNI-Funktion GetStringUTFRegion. Die Funktion nimmt als ersten Parameter den jstring entgegen, der zweite Parameter gibt an, ab welchem Zeichen der jstring kopiert werden soll, der dritte Parameter gibt an, wie viele Zeichen kopiert werden sollen und der letzte Parameter gibt an, wohin die Zeichen kopiert werden sollen.
Damit wäre das schwierigste überwunden. Es liegen nun alle Paramater so vor, dass man mit ihnen wie gewohnt in C programmieren kann. Nun muss lediglich noch WriteProcessMemory aufgerufen werden und die Sache ist gegessen. Der Quellcode könnte in etwa so aussehen:
Code:
#include "Winapi.h" //automatisch generierter Header
#include <windows.h>
JNIEXPORT jboolean JNICALL Java_Winapi_writeMemory(JNIEnv* env, jclass cl, jstring windowTitle, jlong address, jint value)
{
char strWindowTitle[256];
ZeroMemory(strWindowTitle, 256);
long long baseAddress = address;
long buffer = value;
//Prüfe Länge des jstrings
int lenWindowTitle = env->GetStringLength(windowTitle);
if(lenWindowTitle > 255)
return JNI_FALSE;
//Kopiere Zeichenkette in das char-Array
env->GetStringUTFRegion(windowTitle, 0, lenWindowTitle, strWindowTitle);
//Besorge Prozesshandle und Schreibe den neuen Wert in den Speicher des Zielprozess
HWND hwnd = FindWindow(NULL, strWindowTitle);
if(hwnd == NULL)
return JNI_FALSE;
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
if(pid == 0)
return JNI_FALSE;
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if(handle == NULL)
return JNI_FALSE;
if(WriteProcessMemory(handle, (void*)baseAddress, &buffer, sizeof(long), NULL) == TRUE)
return JNI_TRUE;
else
return JNI_FALSE;
}
Wirft das Java-Programm beim Aufruf der nativen Methode eine UnsatisfiedLinkError-Exception, dann konnte höchstwahrscheinlich die native Funktion in der Dll durch decoration des Compilers nicht erkannt werden und es müssen noch entsprechende Compilerflags gesetzt werden, welche aus der Spezifikation des verwendeten Compilers entnommen werden können. Also am besten Googlen. Z.B. ist es beim gcc compiler notwendig mit „-Wl,--kill-at“ zu kompilieren.
4. Fazit:
Es funktioniert also: Auch in Java kann man WinApi Funktionen verwenden. Zeugs wie Injektoren sind damit mehr oder weniger auch in Java umsetzbar. Aber man muss sich ernsthaft fragen, ob sich der Aufwand lohnt. Die Kernarbeit muss so oder so in C oder vergleichbaren Sprachen gemacht werden. Ich denke also, dass die Speichermanipulation in Java mehr Spielerei ist, als dass sie einen praktischen Nutzen hat.
EDIT: Wer syntaktische, semantische oder sonstige Fehler findet, darf sie als Beitrag hier drunter anmerken und mich aufs übelste derb beleidigen. Danke.







