Как манипулировать памятью из Java с помощью JNA в Windows

Как мне манипулировать памятью из Java? Я знаю, что Java работает в своей собственной JVM, поэтому не может напрямую обращаться к памяти процесса.

Я слышал о JNA, который можно использовать для получения интерфейсов между операционной системой и моим Java-кодом.

Допустим, я хочу манипулировать счетом пасьянса. Попытка будет примерно такой:

  1. получить процесс пасьянса
  2. получить доступ к памяти пасьянса
  3. узнать, где в памяти хранится счет
  4. напишите мое новое значение в адресе

Сама Java не может получить доступ к этой памяти, так как я могу сделать это с помощью JNA?


person Loki    schedule 17.09.2013    source источник
comment
a lot of people want to manipulate memory on Windows OS with Java. Я очень в этом сомневаюсь, но вы можете сделать это с помощью Unsafe или github.com/OpenHFT/Java-Lang версия 6.0   -  person Peter Lawrey    schedule 17.09.2013
comment
Вопрос следует открыть снова, потому что мотивация ОП состояла в том, чтобы дать свой собственный (очень подробный) ответ. Тем не менее, текст самого вопроса плохой.   -  person Marko Topolnik    schedule 17.09.2013
comment
Я не совсем уверен, как улучшить вопрос. Но если кто-то знает, дайте знать.   -  person Loki    schedule 17.09.2013
comment
Цель определенно соответствует часто задаваемым вопросам, и сам вопрос был улучшен. Я думаю, что это должно быть снова открыто   -  person Moritz Roessler    schedule 17.09.2013


Ответы (2)


Вам нужно использовать библиотеку JNA. Загрузите два файла Jar (jna.jar и jna-platform.jar)

Я нашел руководство на pastebin, в котором объясняется, как использовать эту библиотеку. Но не обязательно будет читать его, чтобы понять следующее.

Допустим, вы хотите манипулировать адресами и их значениями в Windows-игре «Пасьянс».


Знай, что ты делаешь

  1. Если вы хотите манипулировать адресами и их значениями, знайте, что вы делаете!
    Вам нужно знать, какой размер имеет значение, хранящееся в адресе. Это 4 байта или 8 байт или что-то еще.

  2. Знать, как использовать инструменты для получения динамических и базовых адресов. Я использую CheatEngine.

  3. Знайте разницу между базовыми и динамическими адресами:

    • Динамические-адреса будут меняться каждый раз, когда вы перезапускаете приложение (Solitaire).
      Они будут содержать нужное значение, но вам нужно будет каждый раз находить адрес заново. Итак, сначала вам нужно научиться тому, как получить базовый адрес.
      Узнайте об этом, поиграв в учебном пособии CheatEngine.

    • Базовые адреса – это статические адреса. Эти адреса указывают на другие адреса в основном следующим образом: [[базовые адреса + смещение] + смещение] -> значение. Итак, вам нужно знать базовый адрес и смещения, которые необходимо добавить к адресам, чтобы получить динамический адрес.

Итак, теперь, когда вы знаете, что вам нужно знать, вы можете провести небольшое исследование с помощью CheatEngine в Solitaire.


Вы нашли свой динамический адрес и искали базовый адрес? Хорошо, давайте поделимся нашими результатами:

Базовый адрес для оценки: 0x10002AFA8
Смещения для получения динамического адреса: 0x50 (первый) и 0x14 (второй)

Все правильно? Хорошо! Давайте продолжим написание кода.


Создать новый проект

В ваш новый проект вам необходимо импортировать эти библиотеки. Я использую Eclipse, но он должен работать и в любой другой IDE.

Интерфейс User32

Спасибо Тодду Фасту за настройку интерфейса User32. Это не полное, но достаточно, что нам нужно здесь.

С помощью этого интерфейса мы получаем доступ к некоторым функциям user32.dll в Windows. Нам понадобятся следующие функции: FindWindowA и GetWindowThreadProcessID

Примечание: если Eclipse сообщает, что необходимо добавить нереализованные методы, просто проигнорируйте его и все равно запустите код.

Интерфейс ядра32

Спасибо Deject3d за интерфейс Kernel32. Я немного модифицировал его.

Этот интерфейс содержит методы, которые мы используем для чтения и записи в память. WriteProcessMemory и ReadProcessMemory. Он также содержит метод для открытия процесса OpenProcess

Фактическая манипуляция

Теперь мы создаем новый класс, который будет содержать несколько вспомогательных методов и основную функцию в качестве точки доступа для JVM.

public class SolitaireHack {

    public static void main(String... args)
    {

    }
}

Давайте заполним то, что мы уже знаем, например наши смещения и базовый адрес.

public class SolitaireHack {

    final static long baseAddress = 0x10002AFA8L;
    final static int[] offsets = new int[]{0x50,0x14};

    public static void main(String... args)
    {

    }
}

Затем мы используем наши интерфейсы, чтобы получить доступ к нашим специальным методам Windows:

импортировать com.sun.jna.Native;

public class SolitaireHack {

    final static long baseAddress = 0x10002AFA8L;
    final static int[] offsets = new int[]{0x50,0x14};

    static Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32",Kernel32.class);
    static User32 user32 = (User32) Native.loadLibrary("user32", User32.class);

    public static void main(String... args)
    {

    }
}

И последнее, но не менее важное: мы создаем некоторые константы разрешений, которые нам нужны, чтобы получить разрешение на чтение и запись в процесс.

import com.sun.jna.Native;

public class SolitaireHack {

    final static long baseAddress = 0x10002AFA8L;
    final static int[] offsets = new int[]{0x50,0x14};

    static Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32",Kernel32.class);
    static User32 user32 = (User32) Native.loadLibrary("user32", User32.class);

    public static int PROCESS_VM_READ= 0x0010;
    public static int PROCESS_VM_WRITE = 0x0020;
    public static int PROCESS_VM_OPERATION = 0x0008;

    public static void main(String... args)
    {

    }
}

Чтобы получить процесс, в котором мы можем манипулировать памятью, нам нужно получить окно. Это окно можно использовать для получения идентификатора процесса. С этим идентификатором мы можем открыть процесс.

public static void main(String... args)
{
    int pid = getProcessId("Solitaire");
    Pointer process = openProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, pid);
}

public static int getProcessId(String window) {
     IntByReference pid = new IntByReference(0);
     user32.GetWindowThreadProcessId(user32.FindWindowA(null, window), pid);

     return pid.getValue();
}

public static Pointer openProcess(int permissions, int pid) {
     Pointer process = kernel32.OpenProcess(permissions, true, pid);
     return process;
}

В методе getProcessId мы используем параметр, являющийся заголовком окна, чтобы найти дескриптор окна. (FindWindowA) Этот дескриптор окна используется для получения идентификатора процесса. IntByReference — это версия указателя JNA, в которой будет храниться идентификатор процесса.

Если вы получите идентификатор процесса, вы можете использовать его, чтобы открыть процесс с помощью openProcess. Этот метод получает разрешения и pid для открытия процесса и возвращает указатель на него. Для чтения из процесса вам необходимо разрешение PROCESS_VM_READ, а для записи из процесса вам нужны разрешения PROCESS_VM_WRITE и PROCESS_VM_OPERATION.

Следующее, что нам нужно получить, это фактический адрес. Динамический адрес. Итак, нам нужен другой метод:

public static void main(String... args)
{
    int pid = getProcessId("Solitaire");
    Pointer process = openProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, pid);

    long dynAddress = findDynAddress(process,offsets,baseAddress);
}

public static long findDynAddress(Pointer process, int[] offsets, long baseAddress)
{

    long pointer = baseAddress;

    int size = 4;
    Memory pTemp = new Memory(size);
    long pointerAddress = 0;

    for(int i = 0; i < offsets.length; i++)
    {
        if(i == 0)
        {
             kernel32.ReadProcessMemory(process, pointer, pTemp, size, null);
        }

        pointerAddress = ((pTemp.getInt(0)+offsets[i]));

        if(i != offsets.length-1)
             kernel32.ReadProcessMemory(process, pointerAddress, pTemp, size, null);


    }

    return pointerAddress;
}

Для этого метода требуется процесс, смещения и базовый адрес. Он хранит некоторые временные данные в объекте Memory, о чем он и говорит. Воспоминание. Он считывает базовый адрес, возвращает новый адрес в памяти и добавляет смещение. Это делается для всех смещений и возвращает в конце последний адрес, который будет динамическим адресом.

Итак, теперь мы хотим прочитать нашу партитуру и распечатать ее. У нас есть динамические адреса, где хранится счет, и нам просто нужно его прочитать. Оценка представляет собой 4-байтовое значение. Integer — это тип данных размером 4 байта. Таким образом, мы можем использовать целое число, чтобы прочитать его.

public static void main(String... args)
{
    int pid = getProcessId("Solitaire");
    Pointer process = openProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, pid);

    long dynAddress = findDynAddress(process,offsets,baseAddress);

    Memory scoreMem = readMemory(process,dynAddress,4);
    int score = scoreMem.getInt(0);
    System.out.println(score);
}

public static Memory readMemory(Pointer process, long address, int bytesToRead) {
    IntByReference read = new IntByReference(0);
    Memory output = new Memory(bytesToRead);

    kernel32.ReadProcessMemory(process, address, output, bytesToRead, read);
    return output;
}

Мы написали оболочку для нашего метода kernel32 readProcessMemory. Мы знаем, что нам нужно прочитать 4 байта, поэтому bytesToRead будет 4. В методе будет создан и возвращен объект Memory, который будет иметь размер byteToRead и хранить данные, содержащиеся в нашем адресе. С помощью метода .getInt(0) мы можем считать целочисленное значение нашей памяти по смещению 0.

Поиграйте немного со своим пасьянсом и получите несколько очков. Затем запустите свой код и прочитайте значение. Проверьте, является ли это вашей оценкой.

Наш последний шаг будет состоять в том, чтобы манипулировать нашей оценкой. Мы хотим быть лучшими. Итак, нам нужно записать 4 байта данных в нашу память.

byte[] newScore = new byte[]{0x22,0x22,0x22,0x22};

Это будет наш новый счет. newScore[0] будет младшим байтом, а newScore[3] — старшим. Итак, если вы хотите изменить свой счет на значение 20, ваше byte[] будет:
byte[] newScore = new byte[]{0x14,0x00,0x00,0x00};

Запишем на память:

public static void main(String... args)
{
    int pid = getProcessId("Solitaire");
    Pointer process = openProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, pid);

    long dynAddress = findDynAddress(process,offsets,baseAddress);

    Memory scoreMem = readMemory(process,dynAddress,4);
    int score = scoreMem.getInt(0);
    System.out.println(score);

    byte[] newScore = new byte[]{0x22,0x22,0x22,0x22};
    writeMemory(process, dynAddress, newScore);
}

public static void writeMemory(Pointer process, long address, byte[] data)
{
    int size = data.length;
    Memory toWrite = new Memory(size);

    for(int i = 0; i < size; i++)
    {
            toWrite.setByte(i, data[i]);
    }

    boolean b = kernel32.WriteProcessMemory(process, address, toWrite, size, null);
}

С помощью нашего метода writeMemory мы записываем byte[] вызываемые данные в наш адрес. Мы создаем новый объект Memory и устанавливаем его размер равным длине массива. Записываем данные в объект Memory с правильными смещениями и записываем объект на наш адрес.

Теперь у вас должен быть фантастический результат 572662306.

Если вы точно не знаете, что делают некоторые методы kernel32 или user32, загляните в MSDN или не стесняйтесь спрашивать.

Известные проблемы:

Если вы не получили идентификатор процесса Solitaire, просто проверьте его в диспетчере задач и введите идентификатор процесса вручную. Немецкий Solitär не подойдет, я думаю, из-за ä в названии.

Надеюсь, вам понравился этот урок. Большинство частей взяты из некоторых других руководств, но собраны все вместе здесь, так что, если кому-то нужна отправная точка, это должно помочь.

Еще раз спасибо Deject3d и Тодду Фасту за помощь. Если у вас есть проблемы, просто скажите мне, и я постараюсь вам помочь. Если чего-то не хватает, не стесняйтесь, дайте мне знать или добавьте это самостоятельно.

Спасибо и хорошего дня.


Давайте посмотрим на полный код класса SolitaireHack:

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;

public class SolitaireHack {

    final static long baseAddress = 0x10002AFA8L;
    final static int[] offsets = new int[]{0x50,0x14};

    static Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32",Kernel32.class);
    static User32 user32 = (User32) Native.loadLibrary("user32", User32.class);

    public static int PROCESS_VM_READ= 0x0010;
    public static int PROCESS_VM_WRITE = 0x0020;
    public static int PROCESS_VM_OPERATION = 0x0008;

    public static void main(String... args)
    {
        int pid = getProcessId("Solitaire");
        Pointer process = openProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, pid);

        long dynAddress = findDynAddress(process,offsets,baseAddress);

        Memory scoreMem = readMemory(process,dynAddress,4);
        int score = scoreMem.getInt(0);
        System.out.println(score);

        byte[] newScore = new byte[]{0x22,0x22,0x22,0x22};
        writeMemory(process, dynAddress, newScore);
    }

    public static int getProcessId(String window) {
         IntByReference pid = new IntByReference(0);
         user32.GetWindowThreadProcessId(user32.FindWindowA(null, window), pid);

         return pid.getValue();
    }

    public static Pointer openProcess(int permissions, int pid) {
         Pointer process = kernel32.OpenProcess(permissions, true, pid);
         return process;
    }

    public static long findDynAddress(Pointer process, int[] offsets, long baseAddress)
    {

        long pointer = baseAddress;

        int size = 4;
        Memory pTemp = new Memory(size);
        long pointerAddress = 0;

        for(int i = 0; i < offsets.length; i++)
        {
            if(i == 0)
            {
                 kernel32.ReadProcessMemory(process, pointer, pTemp, size, null);
            }

            pointerAddress = ((pTemp.getInt(0)+offsets[i]));

            if(i != offsets.length-1)
                 kernel32.ReadProcessMemory(process, pointerAddress, pTemp, size, null);


        }

        return pointerAddress;
    }

    public static Memory readMemory(Pointer process, long address, int bytesToRead) {
        IntByReference read = new IntByReference(0);
        Memory output = new Memory(bytesToRead);

        kernel32.ReadProcessMemory(process, address, output, bytesToRead, read);
        return output;
    }

    public static void writeMemory(Pointer process, long address, byte[] data)
    {
        int size = data.length;
        Memory toWrite = new Memory(size);

        for(int i = 0; i < size; i++)
        {
                toWrite.setByte(i, data[i]);
        }

        boolean b = kernel32.WriteProcessMemory(process, address, toWrite, size, null);
    }
}
person Community    schedule 17.09.2013
comment
Впечатляющий! Действительно отличная инструкция. Охватывает все, что вы должны знать по теме +1 - person Niton; 22.08.2019

Используя https://github.com/OpenHFT/Java-Lang, вы можете сделать

long size = 1L << 40; // 1 TB
DirectStore store = DirectStore.allocate(size);
DirectBytes slice = store.createSlice();
slice.writeLong(0, size);
slice.writeLong(size - 8, size);
store.free();

DirectByte может указывать произвольные адреса в памяти или выделяться с помощью malloc

person Peter Lawrey    schedule 17.09.2013
comment
@Loki Извините, он вообще не использует JNA и никаких дополнительных библиотек JNI, только встроенные JVM. Он максимально использует встроенные функции Unsafe (которые превращаются в одну инструкцию машинного кода вместо JNI). - person Peter Lawrey; 17.09.2013
comment
@Loki Какая возможная мотивация может быть у реального пользователя, чтобы настаивать на использовании JNA? Этот ответ улучшает то, о чем просят, если что. - person Marko Topolnik; 17.09.2013