本帖最后由 SnopCat 于 2025-3-5 11:10 编辑
本人初学者,对很多东西理解的不多,这里分享一下我对这款游戏修改的思路。
工具:Cheat Engine
拿能量修改来说,这游戏基本上只要能量无限,通关有手就行。单纯的用CE修改能量很简单,精确值搜索直接就能改,但是每次重新打开游戏或者SL(退出到主页面重新加载游戏)后,能量地址会发生变化,所以我开始寻找便捷的方法,不用每次都要搜索。
首先想到的就是找指针,但是找不到能用的,才发现这个游戏是java写的,内存地址会变,所以换个思路:将修改能量的代码nop掉,这样也可以实现能量无限。
首先精确值扫描,找到能量的地址
然后找出是什么改写了这个地址,点击操作码-显示反汇编程序
将其nop掉发现能量不会再消耗(同时也不会增加了,所以需要在能量满的时候修改),证明思路可行。
此时我们需要找到一个指针指向这个操作码,在每次打开游戏时将其nop,即可实现能量无限。但是直接精确搜索这个地址,没有任何结果。
通过多次重启游戏发现,这个操作码地址的后三位不会变化,就上图来说(04302DB8),每次重启游戏,其地址均为*****DB8,那么在本次游戏中,可以尝试搜索04302000~04302DB8这个区间内的地址,如图
找到了很多基址,随便找一个基址jvm.dll+7FF818,其值为0430266F,那么我们可以计算一下偏移量04302DB8-0430266F=749,通过这个指针,就可以确定修改能量操作码的地址
使用3个长度字节数组表示为[89 04 19],将其修改为[90 90 90],就是将其nop掉。
以上是第一次的思路,但是经过我多次测试,在我自己电脑可行,在其他电脑用该方法获得的基址不一样(差了8),如图(有大佬可以解释一下吗)
所以我又重新改善了方法,使用CE的指针扫描,去扫描操作码的地址,最终找到了18个指针,经测试,这次在两台电脑均可正常使用
此外,需要注意的是游戏打开方式的不同也会导致指针不能通用,使用steam打开的话,游戏进程是javaw.exe,直接在文件夹打开,游戏进程是Slay the Spire.exe,在javaw.exe获取的指针不能直接用在Slay the Spire.exe,经过测试,两种方式的基址是一样的,但是偏移量后者比前者要多10,例如在指针扫描结果中,第一个148的偏移量,在后者为158。
我也写了个C++程序来修改,获取模块基址是用deepseek写的。
void energy(bool on_off)
{
DWORD base_address = GetModuleBaseAddress(pid,L"jvm.dll")+0x804028;
DWORD energy_address = 0;
ReadProcessMemory(handle, (LPVOID)base_address, &energy_address, sizeof(energy_address), NULL);
energy_address += 0x148;
BYTE nop[3] = { 0x90,0x90,0x90 };
BYTE original[3] = { 0x89,0x04,0x19 };
BYTE temp[3];
ReadProcessMemory(handle, (LPVOID)energy_address, (LPVOID)temp, sizeof(temp), NULL);
if (memcmp(original, temp, sizeof(original)) == 0)
{
if (!on_off)
{
std::cout << "修改成功" << std::endl;
return;
}
else
{
WriteProcessMemory(handle, (LPVOID)energy_address, (LPVOID)nop, sizeof(nop), NULL);
std::cout << "修改成功" << std::endl;
return;
}
}
else
{
if (memcmp(nop, temp, sizeof(nop)) != 0)
{
std::cout << energy_address<< std::endl;
MessageBox(
NULL,
TEXT("内存读取错误"),
TEXT("错误"),
MB_ICONERROR | MB_OK
);
}
else
{
if (!on_off)
{
WriteProcessMemory(handle, (LPVOID)energy_address, (LPVOID)original, sizeof(original), NULL);
std::cout << "修改成功" << std::endl;
return;
}
else
{
std::cout << "修改成功" << std::endl;
return;
}
}
}
}
DWORD_PTR GetModuleBaseAddress(DWORD pid, const wchar_t* moduleName)
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32,
pid
);
if (hSnapshot == INVALID_HANDLE_VALUE) {
std::cerr << "创建快照失败!错误代码: " << GetLastError() << std::endl;
return 0;
}
MODULEENTRY32 moduleEntry;
moduleEntry.dwSize = sizeof(MODULEENTRY32);
if (!Module32First(hSnapshot, &moduleEntry)) {
std::cerr << "获取第一个模块失败!" << std::endl;
CloseHandle(hSnapshot);
return 0;
}
DWORD_PTR baseAddress = 0;
do {
if (_wcsicmp(moduleEntry.szModule, moduleName) == 0) {
baseAddress = reinterpret_cast<DWORD_PTR>(moduleEntry.modBaseAddr);
break;
}
} while (Module32Next(hSnapshot, &moduleEntry));
CloseHandle(hSnapshot);
return baseAddress;
}
关于血量的修改,使用此方法会导致游戏崩溃。
目前已发现的问题:
1.打一段时间会失效,能量会减少但不会增加。
2.开始新游戏前如果修改的话会没有初始卡牌。
3.问号事件文字无法正确显示
还有哪些地方可以改善的或者有其他更好的思路希望大佬可以解释一下
另一种思路:使用人造指针获取能量地址
因为那行代码牵扯太多操作了,如果直接nop会导致其他问题,所以可以采用人造指针的方式获取能量的地址,然后修改或者锁定。
在修改能量的操作码那里代码注入
根据rbx(这里是偏移量)的不同,这行代码的操作也不同,通过分析可以得到对能量操作时的偏移量为84,那么可以通过cmp rbx,84来判断是否为能量的操作,从而筛选出能量的地址,当rbx为84时,此时的rcx+rbx为能量的地址,将其写入一个可读可写的地方,因为是java写的游戏,所以这个写入的地址我选用了基址+偏移来定位
alloc(newmem,2048,03E12DB8)
label(returnhere)
label(originalcode)
label(exit)
newmem: //this is allocated memory, you have read,write,execute access
//place your code here
cmp rbx,84
jne originalcode
push rsi
push rdi
lea rsi,[rcx+rbx]
mov rdi,jvm.dll+7A1100
mov [rdi],rsi
pop rdi
pop rsi
originalcode:
mov [rcx+rbx],eax
jmp 03E12E36
exit:
jmp returnhere
03E12DB8:
jmp newmem
nop 3
returnhere:
同理血量等也可以这样改
关于人造指针的修改详见下个帖子https://www.52pojie.cn/thread-2011479-1-1.html |