Overview
如果你认为这根本不能算是游戏指南,可以关掉它,因为我也是这么想的。之所以写这篇文章,是因为今天和朋友聊天聊到了Mirror,他说他 买了个修改器 ,很好用。我当时就迷了,以可能比游戏还贵的价格买个修改器,而且还是最容易修改的Mono框架的游戏。。。。我个人是喜欢改游戏的,不一定是改了玩的更过瘾,修改本身也充满着乐趣。我相信和我抱着同样心情的朋友有很多,但因为各种各样的原因不知如何下手。鉴于官方对于Mod和修改的宽容态度,我准备在这里发一篇修改攻略,你可以把它看做是CE的新手教程,以及类似U3D游戏(Mono框架)的修改指南。本指南分为四个部分。第一部分是修改游戏基础概念。第二部分是如何用CE修改本游戏。需要阅读者有CE的使用经验,以及对汇编的基本了解。第三部分是更进一步的做法,结合dnSpy的分析进行修改。需要阅读者有一定的编程经验。第四部分是一个完善的CT表单代码。 如果你对前面的内容不感兴趣,可以直接阅读第四部分,先把两部分的代码合起来,然后直接Ctrl+V粘贴到CE中就可以了。 本文可以任意转载,但禁止将本文以及本文内容所涉及的代码以任何形式作为商品进行出售。
一、基础概念
无论是很早以前的金山游侠,FPE,还是现在的Cheatengine(以下简称CE),修改游戏的原理都是一致的,就是找到目标数值并进行编辑。
寻找数值的方法就是对比。
游戏有一个数值A,用修改工具在游戏内存中搜索这个值,或许会找到很多相同的数值。
为了判断哪个才是我们需要的,我们要在游戏中让这个值变动起来,然后在上一步搜索的结果中寻找这个新的值,一直重复直到找到唯一一个数值,这就是我们想要修改的目标了。
以上就是游戏修改的最基础的概念,几乎所有的游戏,都是以这种方式开始修改的。
与金山游戏,FPE,Gamemaster等前辈不同的是,CE可以提供更强大的分析与辅助功能,它的动态调试能力甚至可以在一些时候替代专门的调试工具。
例如,当你想要用它修改本游戏或其他U3D游戏时,借助CE的Mono分析功能可以非常简单的完成你希望实现的工作,对于CE的使用者来说,修改这里动态编译类游戏的效率要远比修改其他使用本地代码的游戏要简单。
这也就是我之前说的,用U3D游戏更好修改的原因。
你可以在Cheatengine官网[www.cheatengine.org]下载这个强大的工具。
CE中自带了一个它的使用方法和修改教程(菜单Help——>Cheat Engine Tutorial),网络上也有很多教程,这里不再赘述。
二、用CE修改Mirror
本节假设阅读者了解CE的基本使用方法,并对能够理解基本的汇编代码。
首先假设我们已经找到了游戏的金钱地址:
这个地址当然不是静态的,很多人开始习惯性的尝试寻找基址,发现找不到,然后又尝试使用人造指针,却又发现连代码也不是静态的,于是失败……
其实解决方法特别简单,那就是使用CE的MONO分析功能。
依次点击CE菜单的Mono——>Activate Mono Features 即可激活此功能,关于此功能的详细介绍在官方的维基上就有,这里只介绍最基础的内容。
如上图,我们选择查看是什么访问了这个地址(F5),然后消费,这时得到了很多结果:
注意,在这一过程中,Mono特性可能会被关闭,检查一下,如果是关闭的,那么就再次开启它。
现在在反汇编区域查看他们吧:
必须注意左侧的地址部分,现在它不再是无意义的数字,如果你有编程经验,那么就会发现,这是ShopItem类的ClickBuy方法中的代码。
ShopItem:ClickBuy+c1 – 8B 48 4C – mov ecx,[eax+4C]
也就是说,使用的CE的MONO分析功能之后,即使是游戏动态生成的代码,我们也可以像是直接使用静态地址一样,直接访问。
以此为基础,我们可以写一个脚本,来手动制造一个指向金钱的指针。
首先选中地址(ShopItem:ClickBuy+c1),然后开始创建脚本(Tool——>Auto Assemble,或快捷键Ctrl+A),先自动生成一个作弊表框架(Ctrl+Alt+T),然后使用CE的代码注入功能(Ctrl+I)自动生成注入的代码:
点击确定之后,代码生成完毕,这时我们只需要简单的修改,即可为金钱地址制造一个指针了。
[ENABLE] alloc(newmem,2048) label(returnhere) label(originalcode) label(exit) label(addrMoneyPointer) registersymbol(addrMoneyPointer) newmem: mov [addrMoneyPointer],eax originalcode: mov ecx,[eax+4C] mov edx,[edi+14] exit: jmp returnhere addrMoneyPointer: db 00 00 00 00 ShopItem:ClickBuy+c1: jmp newmem nop returnhere: [DISABLE] dealloc(newmem) ShopItem:ClickBuy+c1: mov ecx,[eax+4C] mov edx,[edi+14] unregistersymbol(addrMoneyPointer)
将脚本保存到表单,启用后,只要买一次东西,addrMoneyPointer指向的地址就是金钱的基址了。当然,你也可以直接写mov [eax+4C],#9999999来让金钱直接变得很多……
注意:要想激活脚本,需要确保CE启用Mono特性,而另一种方式是,在脚本中加入一些lua代码,让脚本在激活时自动启用Mono特性:
[ENABLE] {$lua} LaunchMonoDataCollector() {$asm} //以下代码照常
到此为止,动态编译给我们造成的麻烦不复存在,而不仅如此,我们可以利用它的特性来为我们分析数据提供极大的帮助。
前面代码中的基址(即Eax的值),当我们对它进行分析时(Tool——>Dissect data/structures),如果启用了Mono特性,甚至可以直接看到它设计时的结构定义!
如上图,我们知道了这个基址是属于GirlData类的实例的,虽然名字是GirlData,但这里记录的其实是玩家的信息,比如金钱,生命,攻击力等等信息……它们的结构对我们来说是透明的!
三、用dnSpy进行分析
本节假设阅读者具有一定的编程经验,最好有C#的开发经验。
dnSpy是一款开源的.NET调试与逆向工具。
可以在其Github发布页[github.com]获取它的最新版本。
这里只进行最基础的讨论,dnSpy的使用方法可以参考帮助或自己摸索。
打开游戏的安装目录,并进入以下文件夹:
game_DataManaged
拖拽Assembly-CSharp.dll文件到dnSpy中即可查看游戏的源码,如下图:
结合dnSpy,你可以实现很多有趣的修改,而且不会影响到游戏文件本身。
例如上一章里我们实现了一个金钱的人造指针,但它有一个问题,就是只有买了东西才会生效,这也是很多修改器会有“先打开物品栏”之类说明的原因。
这种方式不太优雅,也不太方便,所以我们可以看看有没有更好的解决方法。
观察GirlData类,并对它进行分析(Ctrl+Shift+R),我们可以看到它虽然不是静态类,但其实是以单例的形式初始化于GameTool类的静态字段中:
进一步分析这个字段,可以看到作为公开字段,它会被很多成员访问:
这里有一个重要的基础概念,即,对于静态字段而言,它的内存地址是以硬编码的形式直接写入生成的代码中的,也就是说,我们可以通过定位这些成员,来实现对这个字段的内存地址的定位。
在这么多方法中,我选择了MainUI.showMoney方法,因为它很简单:
public void showMoney() { this.txt.text = string.Empty + GameTool.roleData.Money; }
我们可以在CE中看看它编译后是什么样子的(注意:成员和类用冒号连接):
注意我选中的那条Mov指令,对静态字段的访问以硬编码的形式直接存在。
所以我们只要写一个脚本读取这个地址就可以了:
[ENABLE] {$lua} LaunchMonoDataCollector() {$asm} //这里我使用了搜索的方式,以保证游戏更新后依然兼容。 //即使不兼容,也会报错避免影响游戏运行。 aobscanregion(codeMainUIGetMoney,MainUI:showMoney,MainUI:showMoney+FF,8B 05) registersymbol(codeMainUIGetMoney) label(addrGirlDataBase) registersymbol(addrGirlDataBase) alloc(newmem,4) newmem: addrGirlDataBase: readmem(codeMainUIGetMoney+2,4) [DISABLE] unregistersymbol(codeMainUIGetMoney) unregistersymbol(addrGirlDataBase) dealloc(newmem)
使用这个脚本,我们可以直接访问这个地址而无需额外的操作了,下图就是游戏中金钱的地址:
本节内容只是一个简单例子,你可以用dnSpy+CE做到更多的事情,运行时不再是游戏修改的阻碍,而是助力。限制你的只有你的想象力以及耐心而已。
四之上、CT表单(请在记事本中把两部分连起来再统一复制到CE中)
<?xml version=”1.0″ encoding=”utf-8″?> <CheatTable> <CheatEntries> <CheatEntry> <ID>6</ID> <Description>”激活脚本”</Description> <Options moHideChildren=”1″ moManualExpandCollapse=”1″ moDeactivateChildrenAsWell=”1″/> <LastState Activated=”1″/> <VariableType>Auto Assembler Script</VariableType> <AssemblerScript>[ENABLE] {$lua} LaunchMonoDataCollector() {$asm} aobscanregion(codeStarBoxGetInstance,StarBox:get_Instance,StarBox:get_Instance+FF,8B 05) registersymbol(codeStarBoxGetInstance) label(addrStarBoxBase) registersymbol(addrStarBoxBase) aobscanregion(codeMainUIGetMoney,MainUI:showMoney,MainUI:showMoney+FF,8B 05) registersymbol(codeMainUIGetMoney) label(addrGirlDataBase) registersymbol(addrGirlDataBase) aobscanregion(codeSMUIStart,SmUI:Start,SmUI:Start+FF,B8 ?? ?? ?? ?? 89 38) registersymbol(codeSMUIStart) label(addrSMUIBase) registersymbol(addrSMUIBase) aobscanregion(codeRoleChangeHP,Role:ChangeHP,Role:ChangeHP+FF,03 45 0C 89 46 24) registersymbol(codeRoleChangeHP) aobscanregion(codePlayerChangeHP,Player:ChangeHP,Player:ChangeHP+FF,83 C4 10) label(injectRoleChangeHP) label(returnRoleChangeHP) label(switchLockPlayerHP) label(switchLockEnemyHP) registersymbol(switchLockPlayerHP) registersymbol(switchLockEnemyHP) label(orgCode) aobscanregion(codePlayerChangeRage,Player:ChangeRage,Player:ChangeRage+FF,85 C0 0F 9F C0) registersymbol(codePlayerChangeRage) label(injectPlayerChangeRage) label(returnPlayerChangeRage) label(switchLockRage) registersymbol(switchLockRage) aobscanregion(codeCheckTune,<CheckTurn>c__Iterator7:MoveNext,<CheckTurn>c__Iterator7:MoveNext+2FF,41 89 88 B8 00 00 00) registersymbol(codeCheckTune) aobscanregion(codeSMAddRed,SmUI:addRedNum,SmUI:addRedNum+FF,8B 8F 98 00 00 00) registersymbol(codeSMAddRed) label(injectSMAddRed) label(returnSMAddRed) label(switchLockSMRed) registersymbol(switchLockSMRed) aobscanregion(codeSMAddGreen,SmUI:addGreenNum,SmUI:addGreenNum+FF,8B 8F 9C 00 00 00) registersymbol(codeSMAddGreen) label(injectSMAddGreen) label(returnSMAddGreen) label(switchLockSMGreen) registersymbol(switchLockSMGreen) aobscanregion(codeBuyItemDecMoney,ShopItem:ClickBuy,ShopItem:ClickBuy+1FF,2B CA 89 48 4C) registersymbol(codeBuyItemDecMoney) aobscanregion(codeBuyItem,ShopItem:ClickBuy,ShopItem:ClickBuy+FF,8B 40 4C 8B 4F 14) registersymbol(codeBuyItem) label(injectBuyItem) label(returnBuyItem) label(switchLockMoney) registersymbol(switchLockMoney) aobscanregion(codeCheckBrokeCloth,StarBox:CheckEnemyBrokeCloth,StarBox:CheckEnemyBrokeCloth+FF,8B 87 D4 00 00 00) registersymbol(codeCheckBrokeCloth) alloc(newmem,256) newmem: injectRoleChangeHP: cmp [ebp+4],codePlayerChangeHP jne switchLockEnemyHP switchLockPlayerHP: //mov [ebp+0C],05F5E0FF nop 7 jmp orgCode switchLockEnemyHP: //mov [ebp+0C],FA0A1F01 nop 7 orgCode: add eax,[ebp+0C] mov [esi+24],eax jmp returnRoleChangeHP injectPlayerChangeRage: switchLockRage: //mov [esi+0000008C],447A0000 nop 9 nop test eax,eax setg al jmp returnPlayerChangeRage injectSMAddRed: switchLockSMRed: //mov [edi+00000098],#10000 nop 9 nop mov ecx,[edi+00000098] jmp returnSMAddRed injectSMAddGreen: switchLockSMGreen: //mov [edi+0000009C],#10000 nop 9 nop mov ecx,[edi+0000009C] jmp returnSMAddGreen injectBuyItem: switchLockMoney: //mov [eax+4C],#9999999 nop 7 mov eax,[eax+4C] mov ecx,[edi+14] jmp returnBuyItem addrStarBoxBase: readmem(codeStarBoxGetInstance+2,4) addrGirlDataBase: readmem(codeMainUIGetMoney+2,4) addrSMUIBase: readmem(codeSMUIStart+1,4) codeRoleChangeHP: jmp injectRoleChangeHP nop returnRoleChangeHP: codePlayerChangeRage: jmp injectPlayerChangeRage returnPlayerChangeRage: codeSMAddRed: jmp injectSMAddRed nop returnSMAddRed: codeSMAddGreen: jmp injectSMAddGreen nop returnSMAddGreen: codeBuyItem: jmp injectBuyItem nop returnBuyItem: [DISABLE] unregistersymbol(codeStarBoxGetInstance) unregistersymbol(addrStarBoxBase) unregistersymbol(codeMainUIGetMoney) unregistersymbol(addrGirlDataBase) unregistersymbol(codeSMUIStart) unregistersymbol(addrSMUIBase) codeRoleChangeHP: add eax,[ebp+0C] mov [esi+24],eax unregistersymbol(codeRoleChangeHP) unregistersymbol(switchLockPlayerHP) unregistersymbol(switchLockEnemyHP) codePlayerChangeRage: test eax,eax setg al unregistersymbol(codePlayerChangeRage) unregistersymbol(switchLockRage) codeCheckTune: db 41 unregistersymbol(codeCheckTune) codeSMAddRed: mov ecx,[edi+00000098] unregistersymbol(codeSMAddRed) unregistersymbol(switchLockSMRed) codeSMAddGreen: mov ecx,[edi+0000009c] unregistersymbol(codeSMAddGreen) unregistersymbol(switchLockSMGreen) codeBuyItem: mov eax,[eax+4C] mov ecx,[edi+14] codeBuyItemDecMoney: db 2B CA unregistersymbol(codeBuyItem) unregistersymbol(switchLockMoney) unregistersymbol(codeBuyItemDecMoney) codeCheckBrokeCloth: db 8B 87 D4 00 00 00 unregistersymbol(codeCheckBrokeCloth) dealloc(newmem) </AssemblerScript> <CheatEntries> <CheatEntry> <ID>15</ID> <Description>”金钱”</Description> <LastState Value=”9816029″ RealAddress=”1D951CC4″/> <VariableType>4 Bytes</VariableType> <Address>addrGirlDataBase</Address> <Offsets> <Offset>4C</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>7</ID> <Description>”回合”</Description> <LastState Value=”3″ RealAddress=”1E4CD0B8″/> <VariableType>4 Bytes</VariableType> <Address>addrStarBoxBase</Address> <Offsets> <Offset>B8</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>8</ID> <Description>”玩家生命”</Description> <LastState Value=”2200″ RealAddress=”1E0FA0EC”/> <VariableType>4 Bytes</VariableType> <Address>addrStarBoxBase</Address> <Offsets> <Offset>24</Offset> <Offset>4C</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>10</ID> <Description>”玩家怒气”</Description> <LastState Value=”1000″ RealAddress=”1E0FA154″/> <VariableType>Float</VariableType> <Address>addrStarBoxBase</Address> <Offsets> <Offset>8C</Offset> <Offset>4C</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>12</ID> <Description>”敌人生命”</Description> <LastState Value=”6600″ RealAddress=”1E0FA1B4″/> <ShowAsSigned>1</ShowAsSigned> <VariableType>4 Bytes</VariableType> <Address>addrStarBoxBase</Address> <Offsets> <Offset>24</Offset> <Offset>48</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>22</ID> <Description>”敌人愉悦”</Description> <LastState Value=”1000″ RealAddress=”161DE9E0″/> <VariableType>4 Bytes</VariableType> <Address>addrSMUIBase</Address> <Offsets> <Offset>80</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>23</ID> <Description>”敌人痛苦”</Description> <LastState Value=”1000″ RealAddress=”161DE9E4″/> <VariableType>4 Bytes</VariableType> <Address>addrSMUIBase</Address> <Offsets> <Offset>84</Offset> <Offset>0</Offset> </Offsets> </CheatEntry> <CheatEntry> <ID>27</ID> <Description>”超多金钱”</Description> <LastState/> <VariableType>Auto Assembler Script</VariableType> <AssemblerScript>[ENABLE] codeBuyItemDecMoney: nop 2 switchLockMoney: db C7 40 4C 7F 96 98 00
四之下、CT表单(请在记事本中把两部分连起来再统一复制到CE中)
[DISABLE] codeBuyItemDecMoney: db 2B CA switchLockMoney: db 0F 1F 80 00 00 00 00 </AssemblerScript> </CheatEntry> <CheatEntry> <ID>34</ID> <Description>”超多生命”</Description> <LastState/> <VariableType>Auto Assembler Script</VariableType> <AssemblerScript>[ENABLE] switchLockPlayerHP: db C7 45 0C FF E0 F5 05 [DISABLE] switchLockPlayerHP: db 0F 1F 80 00 00 00 00 </AssemblerScript> </CheatEntry> <CheatEntry> <ID>35</ID> <Description>”超多怒气”</Description> <LastState/> <VariableType>Auto Assembler Script</VariableType> <AssemblerScript>[ENABLE] switchLockRage: db C7 86 8C 00 00 00 00 00 7A 44 [DISABLE] switchLockRage: db 66 0F 1F 84 00 00 00 00 00 90 </AssemblerScript> </CheatEntry> <CheatEntry> <ID>36</ID> <Description>”锁定回合”</Description> <LastState/> <VariableType>Auto Assembler Script</VariableType> <AssemblerScript>[ENABLE] codeCheckTune: db 90 [DISABLE] codeCheckTune: db 41 </AssemblerScript> </CheatEntry> <CheatEntry> <ID>33</ID> <Description>”无条件爆衣”</Description> <LastState/> <VariableType>Auto Assembler Script</VariableType> <AssemblerScript>[ENABLE] codeCheckBrokeCloth: mov eax,#999 nop [DISABLE] codeCheckBrokeCloth: db 8B 87 D4 00 00 00 </AssemblerScript> </CheatEntry> <CheatEntry> <ID>17</ID> <Description>”敌人HP变化方案”</Description> <DropDownList ReadOnly=”1″ DescriptionOnly=”1″ DisplayValueAsItem=”1″>C7 45 0C 01 1F 0A FA:直死 C7 45 0C FF E0 F5 05:不死 0F 1F 80 00 00 00 00:原始 </DropDownList> <LastState Value=”0F 1F 80 00 00 00 00″ RealAddress=”00AD0019″/> <ShowAsHex>1</ShowAsHex> <VariableType>Array of byte</VariableType> <ByteLength>7</ByteLength> <Address>switchLockEnemyHP</Address> </CheatEntry> <CheatEntry> <ID>25</ID> <Description>”愉悦度变化方案”</Description> <DropDownList ReadOnly=”1″ DescriptionOnly=”1″ DisplayValueAsItem=”1″>66 0F 1F 84 00 00 00 00 00 90:原始 C7 87 98 00 00 00 10 27 00 00:最大 C7 87 98 00 00 00 00 00 00 00:不变 </DropDownList> <LastState Value=”66 0F 1F 84 00 00 00 00 00 90″ RealAddress=”00AD003F”/> <ShowAsHex>1</ShowAsHex> <VariableType>Array of byte</VariableType> <ByteLength>10</ByteLength> <Address>switchLockSMRed</Address> </CheatEntry> <CheatEntry> <ID>24</ID> <Description>”痛苦度变化方案”</Description> <DropDownList ReadOnly=”1″ DescriptionOnly=”1″ DisplayValueAsItem=”1″>66 0F 1F 84 00 00 00 00 00 90:原始 C7 87 9C 00 00 00 10 27 00 00:最大 C7 87 9C 00 00 00 00 00 00 00:不变 </DropDownList> <LastState Value=”66 0F 1F 84 00 00 00 00 00 90″ RealAddress=”00AD0054″/> <ShowAsHex>1</ShowAsHex> <VariableType>Array of byte</VariableType> <ByteLength>10</ByteLength> <Address>switchLockSMGreen</Address> </CheatEntry> </CheatEntries> </CheatEntry> </CheatEntries> </CheatTable>