今天不想学习,想玩游戏,所以就打开了一个肉鸽游戏,尝试开挂。

参考:【CheatEngine基础教程】四、Unity3D游戏《晚上nano好》修改实战


先打开游戏目录,看看是哪类游戏

  • rpgmaker:解包+编辑器用工程文件打开
  • gamemaker:js反混淆得到源代码
  • unity il :ce+dnspy
  • unity il2cpp:恢复符号表ida改cpp代码+ce

在Managed文件夹中:

image-20240501224552961

很明显可以看出是unity游戏

首先要明确目标:

这是一个卡牌rougelike游戏,玩家操作2~4个角色+一个主角打败复数的敌人

修改金钱,技能点(升级人物),人物血量,每回合费用

那么开始


修改基本数据

由于这个游戏是unity游戏,可以直接通过ce的mono分析模块来方便地检测内容并hook函数

image-20240501232651548

这样打开这个功能,然后搜索金钱数量,直到找到对应的值

image-20240501232847246

右键,查找是什么改写了这个值

得到:

image-20240501233125213

详细分析一下这个代码,会发现这里的地址是写死的,以字面量直接放到内容里面,这是为什么呢?

由于unity是cs语言,cs在编译时只生成il中间代码,在CLR虚拟机中执行,相当于java里面的虚拟机。这样,它边执行边编译,直接把对应的值写死在编译出的代码里面,然后直接执行,这样当然可以每次都使用字面量而每次都可以不一样。

这也说明不能通过找多级指针来寻找基址,需要另一种方法。

由于开启了mono分析功能,在检测界面点击:显示反汇编程序:

image-20240501233819497

可以看到已经把函数名翻译出来了,那么我们可以从函数名下文章直接找值,由于函数名是一定的,从函数名开始的偏移也是一定的,这样就可以直接找到对应的变量而不需要找基址。可以看到这里的方法是金币的set方法

在dnspy中查看

image-20240501234129448

显然找get更好

在ce中ctrl+g跳转:PlayData:get_Gold

只有1c的长度

image-20240501234429636

可以直接从11行取出rax+25c作为gold的值,但是get和set在这里都有rax+25c的偏移,可以怀疑这里有一个结构体,如果可以找到结构体开始的值,相当于整个内容都有了,只不过还有解决每个值对应的内容是什么的问题。这时mono分析功能又派上用场了:

ctrl+d:

把我们金钱的地址填进去-结构-定义新的结构:

它把所有信息显示出来了(还有偏移量):

image-20240501234828639

一开始的值就是上面函数中的字面量,soul就是技能点,把这两个偏移记住

在内存查看中-工具-自动汇编-写自动汇编脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[ENABLE]
{$lua}
if syntaxcheck then return end
if (LaunchMonoDataCollector()==0) then error("No Mono") end
;判断mono功能是否开启
{$asm}


aobscanregion(getGoldMethod,PlayData:get_Gold,PlayData:get_Gold+FF,48 B8)
;取到对应函数对应位的对应值(结构体开始位置)
alloc(Pridata,8)
;生成标签,以标签开头的内容作为标签区域的代码,标签内容是可读可写可执行的
registersymbol(Pridata)
;把标签升为全局变量,可以在其它位置使用,比如制作指针的时候
Pridata:
readmem(getGoldMethod+2,8)
;将对应位置写入全局变量
[DISABLE]
dealloc(Pridata)
unregistersymbol(Pridata)

这样我们保存后就保存了对应的结构体指针

然后根据偏移:

image-20240501235648619

做出指针,后面的soul同理,不再赘述

在这个结构体中还有人物血量:

同理把找到的结构体开头作为基址来偏移就好

image-20240501235927670

最后还有cost,每回合费用,它不在这个结构体里面,重新搜索,如法炮制:

先看结构体偏移8c

找到setAP函数和getAP函数,这两个函数似乎都没有硬编码的地址,也比较合理,每次开一个战斗都要重新生成一次,但是可以看到:

image-20240502000544674

明显结构体开头在r15,但是拿不到值,干脆直接注入代码:

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
[Enable]
{$asm}
alloc(newmem,512,BattleTeam:SetAP+11d)
label(returnhere)
label(originalcode)
label(exit)
alloc(Battledata,8)
registersymbol(Battledata);创建全局变量
newmem:;插入获取的代码
mov [Battledata],r15
originalcode:;原来的代码
mov [r15+0000008C],edi

exit:
jmp returnhere

BattleTeam:SetAP+11d:;把目标地址改成跳转
jmp newmem
nop 2
returnhere:

[Disable]
BattleTeam:SetAP+11d:;还原代码
mov [r15+0000008C],edi
dealloc(Battledata)
dealloc(newmem)

{$asm}

这样就拿到结构体头了,不过因为是代码注入,必须要调用一次这个函数之后才可以得到值。

同理就得到AP的值了。


锁血和秒杀


角色信物

这个游戏有一个设定,解锁角色需要一些条件。然后如果想要看后日谈,需要解锁所有角色,然后用对应的礼物给对应的角色3次,并在最后打最终boss的时候带上他们并通关,这需要花费的时间太长了,很离谱,所以准备修改一下。

在dnspy中搜索角色数据,任意搜索一下,找到了一个用于返回这一个角色数据的函数:

image-20240506212320587

通过这个查找被什么使用

找到给礼物的函数:

image-20240506212840126

这个函数表明可以通过charid来查找对应的charData