前几天重拾空洞骑士,但是我个人对这个游戏不是特别上头,加上已经完全忘了进度在哪,只能瞎逛。闲来无事打算简单逆向空洞骑士看看。
此文章仅供学习用途。请勿将本文章内容用作非法用途。
我主要关注的是最经典的三个部分:血量,金钱和MP,在空洞骑士中应该叫做HP,GEO和MP(灵魂?)。
分析
开局不利
本来想通过非常传统的:
- 扫描特定值
- 不断寻找引用和偏移方式寻找基址和这几个部分的偏移
无奈怎么都找不到。搜了一圈发现现在CE已经支持直接扫描特定地址的指针,我尝试了一下发现效果很好,经过几次重开的尝试后找到了这三个部分对应的指针 (找到的指针都是四五层的指针,怪不得我逆不出来),他们可以在重启游戏后重新使用。很开心地玩了一会。
在某次保存存档退出到主页面,并再次重进后,我发觉这几个部分对应的指针已失效,不能用于修改内存,也就是说我找到的地址并不适用于所有情况,痛定思痛后打算转换思路。
转换思路
我通过简单的搜索后了解到CE目前版本具有对Unity游戏的支持,提供了用于简化分析Unity游戏逆向的Mono features
。
在attach到unity游戏后,通过Mono->Activate mono features
开启支持。
此时你可以选择Mono
菜单中的.net info
选项查看方法签名等元数据。
在Assembly-CSharp
程序集中保存了游戏对象和脚本等的元数据,通过对其中的类进行查询,我找到了PlayerData
类。其实是直接看别人教程找到的
CE可以查看到PlayerData
类中包含一个Static field
,名为_instance
。通过和我之前逆向出来的数据对比,我发现在计算_instance
和Fields的Offset后的值与第一次进入存档的三个部分的数据是相同的。这意味着现在的问题是如何获取到这个_instance
field 的值,通过定长的offset即可获得需要的三个部分内存地址,而且获取此field的操作多半是永久可用的。
翻看此类的方法,发现一个get_instance
的方法,无parameter,返回PlayerData对象,推测此类采用单例模式,虽然内存可能会存在多个PlayerData类似结构的对象,但是只会有一个有效的对象?或许这也能说清楚扫描指针的过程中存在非常多的指针的情况了。
验证
现在为了验证猜想,我找到了mono_invoke_methodAPI,此API在mono feature开启的情况下,可以调用mono的方法,此外,还需要获取methodId
才能调用,因此还找到了mono_findMethodAPI。
在封装并使用官方示例的my_mono_invoke_method
后调用get_instance
方法获取单例实例的地址,经过和_instance
的比较后发现值相同。所以现在我写一个lua的脚本把他们封装起来
1 |
|
这样,通过直接调用get_plater_data_inst_addr
获取所需对象的地址。
获取三个数据的地址
在PlayerData
中的Fields翻看发现了geo
的offset是0x1c4
,当前具有的MP是MPCharge
的offset是0x1cc
,health
是0x190
。这些东西的偏移量加上_instace
的地址正好是之前找出来初次加载存档可用的地址。
在刚才已取得了全局单一实例的地址,此获取方法只使用了CE的mono支持,没有使用任何临时变量,所以理论上应该在任何情况下可以使用。
接下来写一点代码
- 在开头定义需要用到的offset。
因为我不会lua,这里想定义成const,我不清楚有没有,反正我大写了就当是常量吧。
1 |
|
- 封装成获取addr的函数
1 |
|
这样就可以使用类似于WriteInteger(geo_addr(),val)
的方式来写入数据了。
写成这样可能是为了某种意义上可读性,我也没啥明确理由,写起来复杂了点,但是我个人认为看起来是舒服了。
绘制form
CE支持绘制form,但是市面上的教程真的非常少,而且我修改内存的方法需要使用到CE的mono api并传入非二元值,这并不是一个可以简单地在主页面下方的address栏实现的操作,因此我考虑如何在CE上绘制form(CE这么叫窗口的,我不是写桌面应用的,似乎windows winform上窗口叫这个名字?)
在Table
菜单中选择create form
,可以看出CE是很支持直接绘制出来一个form的,我绘制出来的是这个效果:
button支持设置OnClick
事件trigger,但是需要注意的是在Event栏中选中OnClick表格时需要点击...
来主动在Cheat Table中创建trigger,即使是手动写的同名函数也需要这么点一下,不然不好用。我是这么踩进坑的。
按照这个过程创建出来三个函数。trigger代码如下
1 |
|
你可以看到,第一个函数是我专门抽离出来进行封装的过程,这个过程每个功能都会被使用,区别只是参数不同,所以我就抽出来封装了。
我辛苦封装就是为了能在调用的时候写这么简短又好看的代码。希望别金玉其外,败絮其中
完整代码
1 |
|
EXT
这是一些和本文关联性不大但是比较有意思的想法或者实现。
EXT1: 伤害关闭
原生思路
早期分析时候,在找到HP地址时通过反向查找写入此地址的代码片段,然后被狠狠地攻击。
定位到某块代码块中的特定一行,上方有一个sub的指令,将其修改为nop后就可以被攻击也不掉血了。
这段记忆不够准确而且我也不太想查证了,主要想说下边mono拓展思路
mono 拓展思路
开启CE的mono支持后,此部分代码块有了元数据:PlayerData:TakeHealth
即PlayerData
类的TakeHealth
方法,在.net info
面板中可以查看到其函数签名TakeHealth System.Void(int amount)
。
以下代码和游戏版本强相关,请查验后复制粘贴,否则可能导致未知后果
对个函数的汇编代码打断点并简单分析和调试后,我发现在这块代码块的开头,rdi寄存器内的数据会从rdx复制到rdi,数据是1(被小虫子创了一下),随后会开始对rdi的数据处理和计算。因此最简单的方法就是直接把这个指令替换成给rdi赋值为0
也就是mov rsi,0
。
- 打开
Memory View
- 选中
PlayerData:TakeHealth
的mov rsi,rdx
处 - 选择
Tools
–>Auto Assemble
Template
–>Code Injection
–>OK
Template
–>Cheat Table Framework code
- 粘贴
1 |
|
简单看看这坨DSL,可以看到其实我没看太懂,大概思路是Active后会allocate一块新内存并将现存内存复制过去,并修改PlayerData:TakeHealth+17
部分的内存为jmp newmem
,这么做一般是因为jmp指令比较短,跳转到新开辟的内存可以更自由完成功能,新内存的标签是newmem
,如果你观察CE生成的代码,你会发现originalcode
标签下是原指令,他们应该会被复制到新开辟的newmem
处,这时候我们修改这块内存的代码为mov rsi,0
就完成了之前我们的需求。当我们在Address栏取消Active时它就deallocate这块newmem并将PlayerData:TakeHealth+17
部分的内存重置回原来的内存。
这个DSL基本就是汇编指令加了一些给CE用的特殊用途的语法糖,不看文档还是勉强能看懂的这是坏习惯大家不要学,一定要看说明书/官方文档
File
–>Assign to current cheate table
这样就将这个东西/这个功能/我也不知道叫什么这个代码添加到CE主页面下方的Address栏(正确说法应该是cheate table
吧,但是我主要就是用来存地址的)
想启用就点击左边的选项框,对于一个地址来说好像是锁定值。
回血对应的是AddHealth
方法,所以修改这个不会影响回血。
因为直接修改内存的原因,UI上的血量不会发生变化,即使受伤也不会刷新,只有手动回一次血后才能更新UI上的血量显示。
EXT2: 其他有意思的Fields
PlayerData
类中还有很多其他有意思的Fields,浅浅看了几个,但是都没有验证
- has.*Dash 开启后可能始终进行各种冲刺?
- healthBlue 似乎对应当前蓝血的数量?
- has.*Key 对应一些区域的钥匙?
- kills.* 已击杀的虫子的counter?
- etc.
总结
通过这两坨代码就实现了修改GEO,MP和HP以及锁血。相比于临时修改地址的不可复用,自动搜索指针时还要一个一个尝试各种指针找到最正确的哪个指针来说,简单了很多。
如果要逆向分析这种没有anti-cheat的unity游戏,最好还是要有一定的unity开发,或者至少是dotnet和mono平台的开发经验,应该更好处理。说的我都想学unity了
如果没有看到_instance
这个static field以及get_instance
方法我可能完全没有头绪来处理。别的游戏也很可能不用这种实例方式,所以还是太局限了。
这好像是摸鱼这几个月来我质量最高的笔记,所以我把它改成教程分类!
空洞骑士挺好玩的,就是之前太久没玩导致现在我不知道在什么阶段也不知道该往哪里探索了。
还有那个泪水之城的举盾的守卫真难打,气死我了😠难度跟一个小boss一样,但是遍地都是。
我发现逆向这种没加密和anti-cheat的游戏和打游戏本身一样爽
EXT部分本来就可以在form上实现,但是我懒了EXT部分是先于form部分实现的,我不想迁移了。