Java-jna 向内存中写入数据

一、序言

    本篇文章是针对之前的文章 “记录第一次逆向过程” 中,所提出的有关于如何获取基地址和偏移地址问题的解答。以及向指定内存地址写入数据的过程描述。(已PVZ中的阳光为例。因为简单,且有相关的文章。)

    并且逆向工具由之前的IDA Freeware 8.4更改为了Cheat Engine 7.4

二、获取地址

        (1)当前阳光的地址

                    运行PVZ、运行Cheat Engine 7.4,点击左上角的放大镜,选择PVZ,然后点击打开。

                之后进入 PVZ 随便挑选一个关卡开始游戏,待游戏开始时按esc暂停游戏,此时太阳应该是500,然后在 Cheat Engine 中 “首次扫描” 500。

                之后返回游戏,种植一个植物,然后再次暂停。将改变后的太阳数重新填入,最后点击再次扫描。这个过程可能要重复多次,直到只留下一个时再停止扫描。

                这样当前太阳的地址就找到了,可以验证一下是否正确。(双击地址,在下面的工作区就会出现,然后双击对应太阳数的值,修改它。看效果)

        (2)偏移地址->基地址

                        在一的基础上,鼠标右击地址,找到F6-“找出是什么改写了这个地址 ” 或者 F5-“找出是什么访问了这个地址”,点击它。

                然后回到游戏中,想办法改变太阳值,改变后暂停,回到Cheat Engine 中。

                下面的偏移地址(5560)一定要记在小本本上

此时你会得到一堆地址,你接下来要做的就是找出地址之最特殊的,然后双击它,放进工作区。

怎样的地址是特殊?我一般看地址前3位,当这三位数只出现一次时(没有就逐渐放大范围,前提是之前步骤没错的情况下)。例如上图展示地址中的倒数第4个地址-014832A0(只出现一次的地址也不一定只有一个,把他们全加入工作区,再在之后的操作中逐一排除)

上面是我找出的地址,其中蓝色框框中的地址对我来说更为特殊,所以会优先操作。

对014832A0进行右击选择F5-“找出是什么访问了这个地址”

之后选择任意一个(因为偏移地址一样),点击详情。记录地址和偏移地址。

继续搜索01482B38。步骤与上面相同。

翻找发现绿光一片,那是娇躯一颤。把这四个全都加入工作区。

按图顺序点击后(当点击指针选项后,地址值会变,不要担心,那是正常的),然后输入之前找的偏移地址。

然后点击确定。会多出一个指针(p->30E39380)

修改值看一下是否影响太阳值,影响就找到了。经过我的测试,其他三个也是正确的。

这里顺便提一嘴,拿到的基地址是可以通过ida 的G快捷找到的,这也为我解答了IDA view视图展示的地址是内存地址,不过意义不大,通过IDA找基地址不如CE方便,但两者结合会让逆向更轻松。

三、向内存写入数据(kernel32)

主要使用的方法如下:

    //定义将会用到的方法。
    public interface Memory extends Library {
        //调用kernel32库
        Pvz.Memory INSTANCE = Native.loadLibrary("kernel32", Pvz.Memory.class);

        //关闭打开的对象句柄。
        //参数 id 为打开对象的句柄。
        void CloseHandle(WinNT.HANDLE id);

        //获取指定进程以及这些进程使用的堆、模块和线程的快照。
        //参数 dwFlags 枚举值TH32CS_SNAPPROCESS = 0x00000002(包括系统中快照中的所有进程。)*https://learn.microsoft.com/zh-cn/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
        //参数 th32ProcessID 要包含在快照中的进程的进程标识符。 此参数可以为零以指示当前进程。
        //返回值 返回指定快照的打开句柄
        WinNT.HANDLE CreateToolhelp32Snapshot(WinDef.DWORD dwFlags, WinDef.DWORD th32ProcessID);

        //检索有关系统快照中记录的下一个进程的信息。
        //参数 hSnapshot 从对 CreateToolhelp32Snapshot 函数的上一次调用返回的快照句柄。
        //参数 lppe 指向 PROCESSENTRY32 结构的指针。
        boolean Process32Next(WinNT.HANDLE hSnapshot, Tlhelp32.PROCESSENTRY32 lppe);

        //打开现有本地进程。
        //参数 dwDesiredAccess 进程访问权限 *https://learn.microsoft.com/zh-cn/windows/win32/procthread/process-security-and-access-rights
        //参数 bInheritHandle 此进程(父进程)创建的进程(子进程)是否继承句柄<true:继承; false:不继承> *继承则是使子进程向父进程通信
        //(即子进程使用父进程中的资源)。只修改内存中的数据使,不用父进程内部资源则可以选择false。
        //参数 dwProcessId 要打开的本地进程的标识符(要操作进程的id,类似指针或者索引)
        //返回值 指定进程的打开句柄。
        WinNT.HANDLE OpenProcess(int dwDesiredAccess, boolean bInheritHandle, int dwProcessId);

        //将数据写入内存。
        //参数 hProcess 要修改的进程内存的句柄。句柄必须具有对进程的PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问权限。
        //参数 lpBaseAddress 将要操作进程中的基址的指针。
        //参数 lpBuffer 指向缓冲区的指针,该缓冲区包含要写入指定进程的地址空间中的数据。
        //参数 nSize 要写入指定进程的字节数(写入数据的大小)。
        //参数 lpNumberOfBytesWritten 要写入文件的字节数。值为零则是空写操作。空写操作不写入任何字节,但会导致时间戳更改。
        //返回值 成功为 true (返回值也可以是int 成功为 非零值)
        //下面三种(不限于三种)列举出来的写法都可以(第一种比较贴近官方api的写法,第二种比较简单粗暴。具体底层是如何实现的,
        //哪些方法能使用类似的写法。目前还不是很了解。但可以尝试参考微软官方文档)
        //由于第二种是直接将值(要写入的值,地址值)作为参数,代码复杂度要比第一种低一点,所以本次会使用第一种,以便后续对其他方法的学习使用。
        boolean WriteProcessMemory(WinNT.HANDLE hProcess, Pointer lpBaseAddress, Pointer lpBuffer, int nSize, IntByReference lpNumberOfBytesWritten);
        boolean WriteProcessMemory(int hProcess, int lpBaseAddress, long[] value, int nSize, int lpNumberOfBytesWritten);
        boolean WriteProcessMemory(WinNT.HANDLE hProcess, Pointer lpBaseAddress, Pointer lpBuffer, int nSize, int lpNumberOfBytesWritten);

        //从内存中读取数据
        //参数 hProcess 要修改的进程内存的句柄。
        //参数 lpBaseAddress 要读取进程中的基地址的指针。
        //参数 lpBuffer 指向从指定进程的地址空间接收内容的缓冲区的指针。
        //参数 nSize 读取的字节数。
        //参数 指向变量的指针,该变量接收传输到指定缓冲区的字节数。
        //返回值 成功为 true。
        boolean ReadProcessMemory(WinNT.HANDLE hProcess, Pointer lpBaseAddress, Pointer lpBuffer, int nSize, IntByReference bytesread);
        boolean ReadProcessMemory(WinNT.HANDLE hProcess, int baseAddress, Pointer buffer, int size, int bytesread);
    }

再声明完要使用的api之后,我们还要解决一下地址问题,因为偏移地址是多级结构,不能简单的当作参数传递,或者通过求和传参解决。所以我们自己写一个方法。

   /**
     * 获取当前地址+偏移地址所对应的地址
     * @param processId 要操作的句柄
     * @param address sum(当前地址,偏移地址)
     * @return 经过偏移后的地址
     */
    public int ReadIntProcessMemory(WinNT.HANDLE processId, int address) {
        Pointer addressPointer = new Pointer(address);

        Pointer buffer = new com.sun.jna.Memory(4);

        IntByReference intByReference = new IntByReference();
        intByReference.setValue(0);

        Pvz.Memory.INSTANCE.ReadProcessMemory(
            processId,
            addressPointer,
            buffer,
            4,
            intByReference
          );
        return buffer.getInt(0);
    }

    /**
     * 处理多级偏移地址
     * @param processId 要操作的句柄
     * @param addresss 第一个值为基地址,之后的为偏移地址(一级,二级...)
     * @return 要写入数据的地址
     */
    public int ReadIntProcessMemory(WinNT.HANDLE processId, int... addresss) {
        if (addresss.length < 1) throw new RuntimeException("地址不能为空");
        int currAddress = 0;
        for (int i = 0; i < addresss.length - 1; i++) {
            currAddress = ReadIntProcessMemory(processId, addresss[i] + currAddress);
        }
        return currAddress + addresss[addresss.length - 1];
    }

这里解释一下 一级、二级,一级是再找偏移地址时最后找到的偏移地址,也是直接作用在基地址的地址,其他以此类推。

之后就是主要逻辑:

 @Test
    void myTest(){
        Scanner scanner = new Scanner(System.in);
        //Tlhelp32.PROCESSENTRY32.ByReference processEntry = new Tlhelp32.PROCESSENTRY32.ByReference();
        Tlhelp32.PROCESSENTRY32 processEntry = new Tlhelp32.PROCESSENTRY32();
        //获取指定进程以及这些进程使用的堆、模块和线程的快照。
        WinNT.HANDLE snapshot = Memory.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinDef.DWORD(0));
        try  {
            //遍历条目。
            while (Kernel32.INSTANCE.Process32Next(snapshot, processEntry)) {
                if (Native.toString(processEntry.szExeFile).equals("PlantsVsZombies.exe")){
                    //参数一:
                    //打开现有本地进程,返回值是指定进程的打开句柄。
                    WinNT.HANDLE handle = Memory.INSTANCE.OpenProcess(PROCESS_ALL_ACCESS, false, Integer.valueOf(processEntry.th32ProcessID.toString()));

                    //参数二:
                    //获取要写入数据的地址。
                    //int path = ReadIntProcessMemory(handle, 0x006A9EC0, 0x768 , 0x5560);
                    int path = ReadIntProcessMemory(handle, 0x006A9EC0, 0x768, 0x5560);
                    //存入缓存。
                    Pointer pathPointer = new Pointer(path);

                    //参数三:
                    //分配要写入的数据的内存空间。
                    //参数为分配内存的大小。
                    com.sun.jna.Memory valuePointer = new com.sun.jna.Memory(4);
                    //将要写入的数据。
                    int number = 0;
                    //存储要写入的数据。
                    int[] numberArray = new int[1];

                    //参数四:
                    //要写入文件的字节数。
                    IntByReference intByReference = new IntByReference();
                    //设置为不写。
                    intByReference.setValue(0);

                    //这里按自己的需求来。
                    while (true) {
                        System.out.println("阳光数量:");
                        //键入数值。
                        number = scanner.nextInt();
                        if (number == 0) {
                            //键入值为零时退出。
                            break;
                        }
                        numberArray[0] = number;
                        //将数据放入缓存中
                        valuePointer.write(0,numberArray,0,numberArray.length);
                        boolean b = Memory.INSTANCE.WriteProcessMemory(
                                                                handle,
                                                                pathPointer, 
                                                                valuePointer,
                                                                4, 
                                                                intByReference);
                    }
                }
            }
        } finally {
            scanner.close();
            Memory.INSTANCE.CloseHandle(snapshot);
        }

在遍历条目时我直接使用 JNA 封装的方法(Kernel32.INSTANCE.Process32Next(snapshot, processEntry))是因为我在使用自己声明的接口(Process32Next)中,出现了乱码,经过不断的尝试解码,最终只发现使用Unicode解码才勉强能分辨出对应的进程名称,(使用其他解码工具发现将编码定义为Big5,使用Unicode重新编码效果最好。可惜使用Java代码解码依旧无效。)所以只能用JNA封装好的方法。

同时也建议和我一样的,作为对微软底层API了解度不高的友友们,不要太依赖JNA提供的方法,而应多尝试使用JNI的写法,这样有助于快速掌握API使用,同时也可以做到更替贴合微软api文档中的代码格式。

四、结束语

文章中有许多描述不准、有误等不足的地方。有误的,请指正。不准,不详细的请多多包涵。

对于使用CE找地址的部分,如果没能帮助到你,可以转到

植物大战僵尸进阶 寻找任意阳光基址(三) - the苍穹 - 博客园 (cnblogs.com)

我找地址的方式就是仿照这篇文章找的。

你也可以在网站

吾爱破解 - LCG - LSG|安卓破解|病毒分析|www.52pojie.cn 

中找询对你有帮助的文章。

对于写入内存部分,你也可以转到

使用java来写一个游戏外挂-内存修改程序(辅助-开篇)_java写外挂-CSDN博客

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java JNAJava Native Access)是一种Java编程语言的工具,它允许Java程序通过本地库来调用C或者C++等编程语言编写的函数。通过使用Java JNA,我们可以很方便地获取游戏数据。 在游戏开发,许多游戏都需要获取游戏的一些数据,比如玩家的属性、技能、任务等等。我们可以通过编写一个本地库,并在Java程序使用Java JNA来访问这个本地库,来获取这些游戏数据。 具体实现方法如下: 1.编写C或者C++的本地库,该库包含可以获取游戏数据的函数。 2.使用Java JNA定义这个本地库的函数。 3.在Java加载这个本地库,并使用Java JNA调用这些函数。 例如,如果我们需要获取玩家的经验值和等级,可以将这些数据存储在游戏内存。我们可以通过编写一个C或者C++的本地库,该库包含一个函数,可以获取游戏内存的经验值和等级。然后使用Java JNAJava程序调用这个函数,就可以获取游戏数据了。 总之,通过使用Java JNA,我们可以很方便地获取游戏数据。这可以使我们更加方便地进行游戏开发。 ### 回答2: 在编写游戏程序时,我们通常需要获取游戏数据来进行操作,比如获取角色属性、装备信息、任务状态等等。在Java,我们可以使用JNAJava Native Access)来实现调用C或者C++编写的游戏程序,并获取游戏数据JNA是一个Java的开源框架,它能够让Java程序直接调用C或者C++编写的动态链接库,从而实现Java应用程序与底层系统进行交互。使用JNA需要我们先创建一个接口类,定义接口需要调用的函数,并在接口声明对应的函数。比如下面是一个简单的JNA接口类: public interface GameData extends Library { GameData INSTANCE = (GameData) Native.loadLibrary("GameData", GameData.class); public int getCharacterLevel(); public String getCharacterName(); public boolean isTaskFinished(int taskId); } 在上面的接口,我们声明了三个函数:getCharacterLevel、getCharacterName和isTaskFinished,它们分别对应获取角色等级、角色名称和任务状态的功能。INSTANCE变量是一个单例对象,用于获取接口的实例。 在调用这些函数之前,我们需要先编写对应的动态链接库,并将其命名为GameData.dll或者GameData.so。然后将库文件放置在Java应用程序能够访问到的路径下,比如放置在项目的根目录下。 下面是一个示例程序,演示了如何调用JNA接口的函数来获取游戏数据: public class GameDataTest { public static void main(String[] args) { int level = GameData.INSTANCE.getCharacterLevel(); String name = GameData.INSTANCE.getCharacterName(); boolean finished = GameData.INSTANCE.isTaskFinished(1); System.out.println("角色等级:" + level); System.out.println("角色名称:" + name); System.out.println("任务1是否完成:" + finished); } } 在上面的示例程序,我们通过调用GameData接口的函数来获取角色等级、角色名称和任务状态,并将它们打印出来。 总之,使用JNA可以方便地实现Java应用程序与底层C/C++程序的交互,从而获取游戏数据。但在使用JNA时需要注意接口函数的声明和动态链接库的编写,确保程序可以正确调用函数并获取有效的数据。 ### 回答3: java jna是一种使用java语言调用C/C++语言动态链接库函数的技术,通过jna的接口来实现java程序与C/C++程序之间的交互,这样就可以直接使用C/C++编写的DLL文件等二进制文件,从而实现Java与其他语言之间的互操作。 对于获取游戏数据,可以使用java jna来调用游戏程序的动态链接库函数,通过该函数获取游戏数据。 1. 首先需要找到游戏动态链接库函数所在的地址,通常可以在游戏程序所在目录下的dll文件查找。 2. 实现调用DLL函数的接口,可以使用jna提供的工具生成DLL所对应的接口文件,也可以手动编写接口函数。 3. 调用DLL函数,通过接口函数调用DLL函数,获取游戏数据。 4. 处理游戏数据,解析从游戏程序获取的数据,可以根据具体情况进行处理,如转化为可读的格式、存储到文件、传输到远程服务器等。 需要注意的是,获取游戏数据属于游戏嵌入式技术,可能会侵犯游戏公司的知识产权,因此在进行相关操作时需要遵守法律法规,否则可能引起一系列法律问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值