起因
这几天放假,闲着无聊就想着找点单机游戏打发下时间,偶然间发现个叫部落幸存者的类模拟城市的游戏,看着还行于是玩了一会。玩着玩着发现不对劲,感觉游戏节奏有点不太好把控,于是拿出 Cheat Engine(下面简称CE) 打算加快一下进度。这个游戏没有对数据进行保护,所以基本搜索几下就能找到想要的数据,不过每次启动游戏这些数值的地址就会变掉,谷歌了一下发现需要去找基址,找了一圈也没找到。鉴于之前没用过CE的其他功能。干脆就学习一下怎么用CE好了。
分析一下
首先得明确自己想做什么,这个游戏总体来说难度还是不算高,但是对我这种急性子来说节奏有点偏慢了,游戏里面有一个银币系统,这个银币前期很难收集,对我这种急性子来说想快点体验到游戏后期内容的话,通过修改银币可以很大程度加快游戏进程;其次就是这个游戏的物资种类比较多,关联性也比较强,仓库很容易就放满了,玩到后面一大片地全部造仓库了,因此也可以考虑将仓库容量改大一点。
明确了目的以后,接下来就要琢磨怎么解决了,首先打开游戏目录,一眼就看到一个UnityCrashHandler64.exe摆在那里,Unity的游戏大多都是用C#编写的,部分Unity游戏甚至可以直接反编译成C#代码,这样的话甚至都不用CE,直接改C#代码就行了。不过之后版本的Unity提供了IL2CPP,可以将IL进一步编译成原生,这样的话想再看到C#代码就比较难了。打开Data文件夹里面有个il2cpp_data文件夹,顿时眉头一紧,虽然有Il2CppDumper这样的工具来拆包,不过还是需要用IDA来进一步分析,我玩不明白IDA,因此暂时先放弃这条路…
继续在游戏文件夹里面摸索,发现了一个没用IL2CPP编译的“URP”版本,不知为何这个版本并没有进行IL2CPP编译。有了这个版本,那就意味着可以直接看到游戏的代码。可以在这个版本的Data文件夹下找到Managed文件夹,里面全是托管dll,默认情况下游戏逻辑都在Assembly-CSharp.dll这个类库中,不过也有很多开发者会将游戏逻辑拆分出来,比如这个游戏就放在了GameLogic.dll中,用dnSpy之类的工具打开就能直接看到C#代码了。
开始操作
首先打开游戏,启动CE,用CE附加游戏进程,然后启用CE的mono功能
通过多次变动银币数值的方法搜索到银币的地址,按Ctrl+F6或者右键点击“找出是什么改写了这个地址”,接下来再次对银币进行操作,就能在这里看到了
接着按Ctrl+D或点击“显示反汇编程序”,如果上一步开启了mono功能,这时候就能直接在这里看到类名和函数名了
这里能很明显看出来调用了CoinMgr类下面的AddCoin函数,通过查看C#源代码可以知道,CoinMgr是一个静态类,AddCoin也是静态函数,并且只需要一个入参
知道这些以后,我们可以打开CE的Lua脚本功能尝试直接调用这个函数:
1 2 |
local addCoin = mono_findMethod('', 'CoinMgr', 'AddCoin') print(mono_invoke_method(0, addCoin, 0, {{type=vtDword, value=1000}})) -- 注意这里的type不能写int,C#中的int对应过来是vtDword |
执行成功后,进入游戏发现银币也增加了1000个,不过还不算完,因为总不能每次都打开lua控制台来执行代码吧,如果想在CE中直接双击修改的话,也可以通过注入的方式将银币的地址注册成别名来实现修改。回到刚才的反汇编界面,可以看到在+49的前面,将银币的地址放进了rax
在这里按下Ctrl+A或者点击菜单的“工具”->“自动汇编”,然后按Shift+Ctrl+F或者点击“模板”->“完全注入”,生成注入模板,再将其分配到当前的作弊表。
这时候双击编辑这个脚本,在ENABLE处检查是否已经打开mono功能,然后将里面alloc的地址替换为C#里面的函数名(记得带上偏移量),然后注册一个叫coinPointer的符号,在newmem下面将rax写入coinPointer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
[ENABLE] {$lua} if syntaxcheck then return end if (LaunchMonoDataCollector()==0) then error("No Mono") end {$asm} alloc(newmem,2048,CoinMgr:AddCoin+49) label(returnhere) label(originalcode) label(exit) label(coinPointer) registersymbol(coinPointer) newmem: mov [coinPointer],rax originalcode: mov [rax],ecx exit: jmp returnhere coinPointer: db 00 00 00 00 CoinMgr:AddCoin+49: jmp newmem nop 7 returnhere: [DISABLE] dealloc(newmem) CoinMgr:AddCoin+49: mov [rax],ecx unregistersymbol(coinPointer) |
此时将脚本启用,只要银币发生变动,我们就可以通过这个coinPointer直接找到银币地址
之后只需要双击修改它的值就能随意修改银币数量了。
OK,银币问题解决了,接下来是仓库扩容,这个稍微麻烦一点,因为游戏里没有将仓库容量的数值展示出来,因此这里就得从代码入手了。经验上来说,一般这种存储的东西,名字都带点什么Inventory,Storage,Container之类的,全局搜一搜,在搜Storage的时候,一个叫BagData的类进入了我的视线:
从经验上来说,这个weightLimit就是容量上限,再继续看看是谁在读写这个变量
然后就发现了这么几个函数,从名字不难猜出,IsFull是判断是否已经装满,RemainWeight是返回剩余容量,而他们都调用了一个共同的函数:WeightUpper,这个就是容量上限了,再进一步查看一下引用,BuildingData类和CitizenData类都有这个类作为属性,那么可以大概知道这个函数做了些什么,首先if判断是否是建筑,如果建筑的类型为2(这个2应该就对应了仓库类型的建筑)的话,则使用建筑的GetBrokenNum函数来加权计算容量(例如建筑耐久破损之类的);然后再判断是否为村民,是的话返回这个村民的加权可携数(例如装备了背包或者手推车之类的),如果不是就直接把weightLimit返回出去。
当然,分析归分析,我们还是得以事实为准,那要怎么验证呢?既然都有C#类库了,直接找到这个函数,在它return之前给他*20来个超级加倍,例如建筑,在第一个ret之前插入ldc.i4.s, 20,然后再插入mul,就实现了*20
然后保存dll,进入游戏,发现都已经存了100%的东西了,村民还在往里面搬东西,直到库存达到2000%的时候才提示已装满
既然确定了是这个函数,那么接下来就在CE中如法炮制,先打开反汇编界面,然后Ctrl+G跳转到地址,输入BagData:WeightUpper跳转到这个函数,然后往下翻,找到调用GetBrokenNum的地方,以下面的call为注入点注入
步骤大概跟上面的差不多,在生成好代码之后插入启用mono的代码,然后将地址替换为函数名,接着在下面的call r11的后面插入imul eax,eax,14,注意这个14是16进制,换算回10进制的话就是20。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{$lua} if syntaxcheck then return end if (LaunchMonoDataCollector()==0) then error("No Mono") end mono_symbolLookupCallback("BagData:WeightUpper") {$asm} define(address,"BagData:WeightUpper"+d0) define(bytes,41 FF D3 E9 5C 00 00 00) [ENABLE] assert(address,bytes) alloc(newmem,$1000,"BagData:WeightUpper") label(code) label(return) newmem: code: call r11 imul eax,eax,14 jmp "BagData:WeightUpper"+134 jmp return address: jmp newmem nop 3 return: [DISABLE] address: db bytes dealloc(newmem) |
这样的话只要勾选激活了这段代码,游戏里的建筑容量就会变成20倍,当然,村民的话也可以如法炮制。
总结
虽然困扰我的2个问题解决了,但是对我来说现在还只是学到点皮毛,因为能做到这些事的根本原因是因为开发商留了未经IL2CPP编译的mono版本,如果没有的话,那么整个过程会更加的复杂,得先使用Il2CppDumper将函数列表dump出来,然后把游戏文件拖进IDA去分析,并且有些游戏还对global-metadata.dat进行了加密(例如某神),这时候的分析会变得更加复杂,接下来如果有时间的话再继续深入学习吧,希望不久的将来我也可以像耍耍一样IDA轻松玩。