一、序言
本篇文章是针对之前的文章 “记录第一次逆向过程” 中,所提出的有关于如何获取基地址和偏移地址问题的解答。以及向指定内存地址写入数据的过程描述。(已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
中找询对你有帮助的文章。
对于写入内存部分,你也可以转到