Как да манипулирате памет от Java с JNA на Windows

Как да манипулирам памет от Java? Знам, че Java работи в собствената си JVM, така че не може да има директен достъп до паметта на процеса.

Чух за JNA, която може да се използва за получаване на интерфейси между операционната система и моя Java код.

Да кажем, че искам да манипулирам резултата от Solitaire. Опитът ще бъде нещо подобно:

  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
Въпросът трябва да бъде отворен отново, защото мотивацията на OP беше да предостави свой (много подробен) отговор. Текстът на самия въпрос обаче е доста лош.   -  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.

    • Базовите адреси са статични адреси. Тези адреси сочат към други адреси най-вече по следния начин: [[base-addres + offset] + offset] -> value. Така че това, от което се нуждаете, е да знаете основния адрес и отместванията, които трябва да добавите към адресите, за да получите динамичния адрес.

Така че сега, след като знаете какво трябва да знаете, направете проучване с CheatEngine на Solitaire.


Намерихте своя динамичен адрес и потърсихте основния адрес? Добре, нека споделим нашите резултати:

Базов адрес за резултата: 0x10002AFA8
Отмествания за достигане до динамичния адрес: 0x50 (първи) и 0x14 (втори)

Всичко ли е наред? Добре! Нека продължим с действителното писане на код.


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

Във вашия нов проект трябва да импортирате тези библиотеки. Използвам Eclipse, но трябва да работи на всяка друга IDE.

User32 интерфейс

Благодаря на Тод Фаст за настройката на User32 интерфейс. Не е пълно, но достатъчно, от което се нуждаем тук.

С този интерфейс получаваме достъп до някои функции на user32.dll в Windows. Имаме нужда от следните функции: FindWindowA и GetWindowThreadProcessID

Странична бележка: Ако Eclipse ви каже, че трябва да добави неприложени методи, просто го игнорирайте и изпълнете кода така или иначе.

Интерфейс Kernel32

Благодаря на 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)
    {

    }
}

За да получим процес, при който можем да манипулираме паметта, трябва да получим прозореца. Този прозорец може да се използва за получаване на ID на процеса. С този идентификатор можем да отворим процеса.

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. Знаем, че трябва да прочетем 4Byte, така че bytesToRead ще бъде 4. В метода ще бъде създаден и върнат обект Memory, който ще има размера на byteToRead и ще съхранява данните, съдържащи се в нашия адрес. С метода .getInt(0) можем да прочетем стойността Integer на нашата памет при отместване 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, просто го проверете в диспечера на задачите и запишете pid ръчно. Немският Solitär няма да работи, мисля, че заради ä в името.

Надявам се, че сте харесали този урок. Повечето части са от някои други уроци, но събрани всички тук, така че в случай, че някой се нуждае от отправна точка, това трябва да помогне.

Благодаря отново на Deject3d и Todd Fast за тяхната помощ. Ако имате проблеми, просто ми кажете и аз ще се опитам да ви помогна. Ако нещо липсва, чувствайте се свободни да ме уведомите или да го добавите сами.

Благодаря и хубав ден.


Нека да разгледаме пълния код на SolitaireHack Class:

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. Той използва необезопасени вътрешни елементи, доколкото е възможно (които се превръщат в една инструкция за машинен код вместо JNI) - person Peter Lawrey; 17.09.2013
comment
@Loki Каква възможна мотивация може да има истински потребител, за да настоява да използва JNA? Този отговор подобрява това, което се иска, ако има такова. - person Marko Topolnik; 17.09.2013