吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1614|回复: 35
上一主题 下一主题
收起左侧

[游戏安全] 杀戮尖塔修改思路

[复制链接]
跳转到指定楼层
楼主
SnopCat 发表于 2025-3-2 19:21 回帖奖励
本帖最后由 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;//"jvm.dll"+00804028
        //修改能量值代码地址
        DWORD energy_address = 0;

        //读取基地址处的值
        ReadProcessMemory(handle, (LPVOID)base_address, &energy_address, sizeof(energy_address), NULL);
        //添加偏移
        energy_address += 0x148;

        //修改89 04 19为nop,即90 90 90,实现不消耗能量
        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)//此处代码既不为原始的,也不为nop后的,说明读取错误,报错退出
                {
                        std::cout << energy_address<< std::endl;
                        MessageBox(
                                NULL,                   // 窗口句柄(无窗口时设为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) 
{

        // 1. 创建进程模块快照
        HANDLE hSnapshot = CreateToolhelp32Snapshot(
                TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, // 标志:获取所有模块(包括32位)
                pid                                       // 目标进程ID
        );

        // 检查快照是否创建成功
        if (hSnapshot == INVALID_HANDLE_VALUE) {
                std::cerr << "创建快照失败!错误代码: " << GetLastError() << std::endl;
                return 0;
        }

        // 2. 初始化MODULEENTRY32结构
        MODULEENTRY32 moduleEntry;
        moduleEntry.dwSize = sizeof(MODULEENTRY32); // 必须设置结构大小

        // 3. 遍历模块列表
        if (!Module32First(hSnapshot, &moduleEntry)) {
                std::cerr << "获取第一个模块失败!" << std::endl;
                CloseHandle(hSnapshot); // 关闭快照句柄
                return 0;
        }

        DWORD_PTR baseAddress = 0; // 存储找到的基址
        do {
                // 4. 比较当前模块名与目标名(不区分大小写)
                if (_wcsicmp(moduleEntry.szModule, moduleName) == 0) {
                        baseAddress = reinterpret_cast<DWORD_PTR>(moduleEntry.modBaseAddr);
                        break; // 找到后退出循环
                }
        } while (Module32Next(hSnapshot, &moduleEntry)); // 继续下一个模块

        // 5. 清理资源
        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

免费评分

参与人数 11吾爱币 +16 热心值 +9 收起 理由
wanfon + 1 + 1 热心回复!
Element777 + 1 + 1 谢谢@Thanks!
mengshj + 1 + 1 我很赞同!
Tori97 + 1 热心回复!
ljwstx + 1 感谢分享,学习一下
Hameel + 1 热心回复!
nekoneko2021 + 1 + 1 谢谢@Thanks!
LULU1218 + 1 + 1 谢谢@Thanks!
willJ + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Link_Stark + 2 + 1 谢谢@Thanks!
ZXHS_ + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
zhouxinyi 发表于 2025-3-4 15:58
醉月清风 发表于 2025-3-4 12:08
大佬牛啊,如何用CE查找模拟器运行游戏的基址啊

现在的很多游戏都是64位,而且构造方式复杂很多,如果按照以前的经验去追踪真正的[[[game.exe+XXX]+YYY]+ZZZ]=ABC这种方式的基址会比较困难,因为层级变多了,甚至存在嵌套结构等,所以现在使用注入的方式在某个位置直接找到到ZZZ偏移,然后获取某寄存器的数值MMM,前面的公式直接就变为[MMM+ZZZ]=ABC了,不用去追完完整的基址。
推荐
 楼主| SnopCat 发表于 2025-3-3 17:10 |楼主
zhouxinyi 发表于 2025-3-3 14:55
直接在反汇编段AOB扫描,然后hook获取RCX和RBX的值写入自建地址,再把这两个地址相加提取里面的数值,不就 ...

其实都差不多,但是这个地址用于很多操作,需要获取当rbx=84时的rcx+rbx的值,而且在游戏主页面时也无法获取能量地址,这些都要进行筛选
3#
fradadeng 发表于 2025-3-3 12:35
4#
zhouxinyi 发表于 2025-3-3 14:55
直接在反汇编段AOB扫描,然后hook获取RCX和RBX的值写入自建地址,再把这两个地址相加提取里面的数值,不就是EAX血量了么?
5#
xinxingezi 发表于 2025-3-3 16:32
求多出CE教程
6#
JoseYue 发表于 2025-3-3 17:56
牛逼啊,还可以用deepseek来帮忙写代码,具体怎么把需求转换成文字准确的给到deepseek也是一个学习的过程哇
7#
Yugin 发表于 2025-3-4 08:51
思路非常清晰
8#
zbfdyw 发表于 2025-3-4 11:40
修改了,还玩个啥呢,咋玩的就是自己的水平和能力,都改写了,也就不玩了吧!
9#
醉月清风 发表于 2025-3-4 12:08
zhouxinyi 发表于 2025-3-3 14:55
直接在反汇编段AOB扫描,然后hook获取RCX和RBX的值写入自建地址,再把这两个地址相加提取里面的数值,不就 ...

大佬牛啊,如何用CE查找模拟器运行游戏的基址啊
10#
sun1492zx 发表于 2025-3-4 13:06
好,为楼主点赞
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-3-27 10:06

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表