对现代单机游戏制作简单的修改器界面(进阶)

背景:

基本来说,网上可以找到的修改器教程大部分都是植物大战僵尸以及十几年前的老游戏,如果你对单机游戏的修改感兴趣,并且这游戏刚好没有相关的修改器比如风灵月影,我在这里分享我制作简易修改器的思路和一些理解。(要求一定的ce基础和python基础)

流程1:

首先我们先用ce附加我们想要修改的游戏进程

用ce搜索我们想要查找的数据(ce修改的基本方法,通过改变数据不断查找物品数值所存放的地址)

然而,这个地址一般不是固定地址,比如当游戏重启,或者游戏内置saveload功能时,存放物品数据的地址就会改变,为了稳定修改,我们应该找到一个稳定地址

这个地址一般是游戏的dll地址或者exe地址进行偏移得到的一个稳定地址

这里用到ce修改器的指针扫描功能

很多老修改教程可能会教你通过查找“什么改写了这个地址”来查找地址的偏移集,

但是实际上那是因为老游戏的加密手段有限或者说游戏制作工序简易,基本上两三次就可以找到稳定地址的初始地址,如果你要修改近几年的游戏的话还是得用指针扫描功能。

我推荐偏移值为8000,最大级别为7 来进行搜索

(搜索教程不再提及,很多ce教程有教)

搜索完后,我们得到了游戏中“金钱”以及“灵魂”的一个稳定地址

金钱:

灵魂:

可以看见,我们得到的稳定地址偏移达到了7级偏移,基本上你用改写地址寻找的方法是基本不可能找的到的,因为现代游戏加密规则还是比较严厉的(我推测是这个原因)。

流程2:

得到稳定地址后,我们可以进行python代码的编写了

我们需要引用到的库是pypem库,这个库提供了一些访问游戏内存的方法。

创建我们的py文件 这里我命名为getadd (意为获取地址)

首先,为了让pypem能够顺利读取游戏内存,我们应提供游戏的进程名

如:process_name = "... .exe"(可以查看任务管理器获取进程名字)

定义函数获取游戏进程:

def get_game_process(process_name):
    try:
        pm = pymem.Pymem(process_name)
        return pm
    except Exception as e:
        print(f"Error: {str(e)}")
        sys.exit(1)

接着,我们得先获取游戏中基地址的量

根据CE查找结果我们发现变量的基址是:"mono-2.0-bdwgc.dll"+00494C70

也就是说我们得获取这个mono-2.0-bdwgc.dll  这个dll的地址量

def get_moduladdr(pm, dll_name):
    modules = list(pm.list_modules())
    for module in modules:
        if module.name.lower() == dll_name.lower():
            # print(f"成功获取到 {dll_name} 的基地址: {hex(module.lpBaseOfDll)}")
            return module.lpBaseOfDll
    print(f"未找到 {dll_name} 模块。")
    return None

如果基址是进程.exe 也是一样的,我们只需要改变传入的dll_name,这里我们就传入mono-2.0-bdwgc.dll就可以了。

接着我们再导入偏移地址数组:

soulstone_offsets = [0xE0, 0x0, 0x18E8, 0x60, 0x0, 0x38, 0x26C]  # 灵魂偏移链
money_offsets = [0xE0, 0x0, 0x18E8, 0x60, 0x0, 0x38, 0x268]  # 金钱偏移链

查看CE

我们可以发现由基础地址到达最终物品地址的偏移规则是:

(设中括号为对地址的解引用取值操作)

[上一级地址存储的值+偏移量]=下一级地址

据此,我们可以写下相关函数

伪代码为:

address = pm.read_int(address)
address = new_address + offset  

进行循环操作

然而,此时我们会惊讶的发现,这样子的偏移规则,最后查找会出错,我们找到的物品地址和ce告诉我们的地址完全不一样。

这可能和 内存映射与模块加载 相关

通过仔细查找,我们可以发现原因出在这里

基址存储的值是72502D20,没有错吧?

然而当添加一级偏移之后,你会惊讶地发现这个值发生了变化:

看到没?本来是72502D20的值加上了一个前缀12E

通过反复的重启游戏和尝试,可以发现每次加上的这个前缀都不一样

但有一个共同规律

都是三位,且这三位都是十六进制字符

123456789ABCDEF  15个字符

实际上,如果每次都是三位的前缀码的话,也就意味着我们仅用15*15*15的循环次数就能找到唯一一条正确指向物品地址的完整路径

一般来说,路径不对的话,要么值就中途消失了(也就是CE读取该地址的值是??????)要么就是一个特别大或者特别小的数字,所以死循环的方法错误概念应该是极小的。

ps:作者也不太懂这是不是一个 内存映射与模块加载方面的问题,最后我尝试用死循环的方法去查找,基本上完全没出过错。如果你有更好的解释或者方法,可以告诉我

据此,我们可以得到getadd的完整源码。

getadd的源码:

可以参照着根据自身需求修改

import random
import string
import pymem
import sys

#
process_name = ".exe"  

# 偏移链(灵魂和金钱)
soulstone_offsets = [0xE0, 0x0, 0x18E8, 0x60, 0x0, 0x38, 0x26C]  # 灵魂偏移链
money_offsets = [0xE0, 0x0, 0x18E8, 0x60, 0x0, 0x38, 0x268]  # 金钱偏移链

# 获取游戏进程
def get_game_process(process_name):
    try:
        pm = pymem.Pymem(process_name)
        return pm
    except Exception as e:
        print(f"Error: {str(e)}")
        sys.exit(1)

# 获取DLL模块的基地址
def get_moduladdr(pm, dll_name):
    modules = list(pm.list_modules())
    for module in modules:
        if module.name.lower() == dll_name.lower():
            # print(f"成功获取到 {dll_name} 的基地址: {hex(module.lpBaseOfDll)}")
            return module.lpBaseOfDll
    print(f"未找到 {dll_name} 模块。")
    return None

# 检查地址是否有效
def is_valid_address(address, pm):
    try:
        pm.read_int(address)
        return True
    except:
        return False

def generate_all_prefixes():
    hex_chars = string.digits + 'abcdef'
    return [f"{x}{y}{z}" for x in hex_chars
            for y in hex_chars
            for z in hex_chars]

def generate_random_prefix():
    return ''.join(random.choices('0123456789abcdef', k=3))

# 通过基地址和偏移链获取最终的内存地址
def get_address_with_offsets(base_address, offsets, pm):
    valid_addresses = []
    all_prefixes = generate_all_prefixes()
    for prefix in all_prefixes:
        address = base_address
        valid = True
        for i, offset in enumerate(offsets):
            try:
                address = pm.read_int(address)

                # 如果address为负数,将其转化为正数(无符号整数)
                if address < 0:
                    address = address + 2 ** 32  # 转换为无符号整数

            except Exception as e:
                valid = False
                break

            # 如果 address 仍然为负数
            if address < 0:
                valid = False
                break

            try:
                new_address = int(f"{prefix}{hex(address)[2:]}", 16)
            except ValueError:
                valid = False
                break

            address = new_address + offset

            if not is_valid_address(address, pm):
                valid = False
                break

        if valid:
            value = pm.read_int(address)
            if 0 <= value <= 2 ** 20:
                print(f"Value at address: {value}")
                valid_addresses.append(address)

    if valid_addresses:
        return valid_addresses[-1]
    return None


# 获取灵魂的地址
def get_soulstone_address():
    pm = get_game_process(process_name)
    mono_base_address = get_moduladdr(pm, "mono-2.0-bdwgc.dll")
    if not mono_base_address:
        return None
    actual_base_address = mono_base_address + 0x00494C70
    soulstone_address = get_address_with_offsets(actual_base_address, soulstone_offsets, pm)

    return soulstone_address

# 获取金钱的地址
def get_money_address():
    pm = get_game_process(process_name)
    mono_base_address = get_moduladdr(pm, "mono-2.0-bdwgc.dll")
    if not mono_base_address:
        return None
    actual_base_address = mono_base_address + 0x00494C70
    money_address = get_address_with_offsets(actual_base_address, money_offsets, pm)

    return money_address

# 可以在其他文件中通过导入以下方式调用
if __name__ == "__main__":
    soulstone_address = get_soulstone_address()
    money_address = get_money_address()

    print(f"灵魂地址: {hex(soulstone_address) if soulstone_address else '未找到'}")
    print(f"金钱地址: {hex(money_address) if money_address else '未找到'}")

流程3:

在getadd的源码中,我们用get_soulstone_address函数和get_money_address函数获取了灵魂以及金钱的稳定地址

接下来我们就可以创建图形界面并且对游戏中的内存值进行直接修改了!

在同目录下创建modifyui.py,记得import getadd  这个文件

这里我们使用的是pyqt界面,csdn上也有pyqt环境配置的具体教程,这里不做赘述

pyqt的界面和功能可以根据自己需求设定,这里提供我的源码

modifyui的源码:

注意其中的创建线程操作,不然很大概率修改器会卡死

import sys
import pymem
import psutil
from PyQt5.QtCore import QTimer, QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QMessageBox
import getadd

# 游戏进程名称
process_name = ".exe"


# 设置物品数量
def set_item_quantity(base_address, new_quantity, pm):
    item_address = base_address
    pm.write_int(item_address, new_quantity)


# UI 主窗口类
class GameModifierApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("修改器")
        self.setGeometry(100, 100, 400, 250)

        self.layout = QVBoxLayout()

        # 创建标签和输入框
        self.label_soulstone = QLabel("当前灵魂数量: 0", self)
        self.label_money = QLabel("当前金钱数量: 0", self)

        self.soulstone_input = QLineEdit(self)
        self.soulstone_input.setPlaceholderText("请输入新的灵魂数量")

        self.money_input = QLineEdit(self)
        self.money_input.setPlaceholderText("请输入新的金钱数量")

        # 设置按钮
        self.update_button = QPushButton("更新数量", self)
        self.update_button.clicked.connect(self.update_item_quantities)

        # 布局
        self.layout.addWidget(self.label_soulstone)
        self.layout.addWidget(self.label_money)
        self.layout.addWidget(self.soulstone_input)
        self.layout.addWidget(self.money_input)
        self.layout.addWidget(self.update_button)

        self.setLayout(self.layout)

        # 获取游戏进程和初始化
        self.pm = getadd.get_game_process(process_name)

        # 获取mono-2.0-bdwgc.dll基地址
        self.mono_base_address = getadd.get_moduladdr(self.pm, "mono-2.0-bdwgc.dll")

        # 初始化UI显示
        self.update_ui()

    def update_ui(self):
        try:
            soulstone_address = getadd.get_soulstone_address()
            money_address = getadd.get_money_address()
            # 获取当前灵魂和金钱数量
            soulstone_quantity = self.pm.read_int(soulstone_address)
            money_quantity = self.pm.read_int(money_address)

            self.label_soulstone.setText(f"当前灵魂数量: {soulstone_quantity}")
            self.label_money.setText(f"当前金钱数量: {money_quantity}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"读取进程失败: {str(e)}")
            self.close()

    def update_item_quantities(self):
        try:
            new_soulstone_quantity = int(self.soulstone_input.text())
            new_money_quantity = int(self.money_input.text())

            # 创建后台线程来更新物品数量
            self.thread = UpdateThread(self.pm, new_soulstone_quantity, new_money_quantity)
            self.thread.update_signal.connect(self.on_update_complete)
            self.thread.start()

        except ValueError:
            QMessageBox.warning(self, "无效输入", "请输入有效的整数值。")

    def on_update_complete(self, success, error_message=""):
        if success:
            self.update_ui()
            QMessageBox.information(self, "成功", "灵魂和金钱数量已更新")
        else:
            QMessageBox.critical(self, "错误", f"更新失败: {error_message}")


# 后台线程类,用于执行耗时的更新操作
class UpdateThread(QThread):
    update_signal = pyqtSignal(bool, str)

    def __init__(self, pm, new_soulstone_quantity, new_money_quantity):
        super().__init__()
        self.pm = pm
        self.new_soulstone_quantity = new_soulstone_quantity
        self.new_money_quantity = new_money_quantity

    def run(self):
        try:
            # 获取灵魂和金钱地址
            soulstone_address = getadd.get_soulstone_address()
            money_address = getadd.get_money_address()

            # 设置新的数量
            set_item_quantity(soulstone_address, self.new_soulstone_quantity, self.pm)
            set_item_quantity(money_address, self.new_money_quantity, self.pm)

            # 更新完成,发出信号,成功时传递True和空字符串
            self.update_signal.emit(True, "")
        except Exception as e:
            # 发生错误,发出失败信号,失败时传递False和错误信息
            self.update_signal.emit(False, str(e))


# 主程序
def main():
    app = QApplication(sys.argv)
    window = GameModifierApp()
    window.show()

    # 定时更新UI
    def periodic_update():
        window.update_ui()
        QTimer.singleShot(1000, periodic_update)  # 每1秒更新一次

    periodic_update()

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

测试:

打包目录下的两个py文件成exe可执行文件,

可以发现,金钱和灵魂数据如出一辙

接着进行修改:

可以发现游戏中的数据得到了正确更新,修改成功!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值