概述
最近在玩Steam某款塔防游戏,玩了1000多小时,难度低的地图都过去了,点击模式过不去,竞速模式因为手残成绩不太理想,所以萌生了破解、逆向的念头,研究一下
省流
部分功能破解成功,但是被官方抓住了,封号了,无法联机(也就是长草,但是本地模式还是可以玩的
所以大家练习的时候,记得开小号尝试,大号被封了就很emo
游戏版本是 34.3.6116
长草
unity游戏破解思路
打开游戏安装目录,发现是 unity 游戏
unity游戏破解思路比较固定
运行时有两种: mono
或者il2cpp
mono
步骤简单点,就是 dnspy 分析代码+修改il字节码(步骤较少,难度不一定)
il2cpp
步骤多点,如下所示
Il2CppDumper
提取c#代码结构,得到 Assembly-CSharp.dll
、script.json
、stringliteral.json
和其他文件
dnspy
分析Assembly-CSharp.dll
找到关键方法(函数)
ida
分析 libil2cpp.so
(安卓)GameAssembly.dll
(Windows)
ida
加载script.json
、stringliteral.json
- 找到关键修改点,用汇编转16进制网站,得到修改后的16进制,用
010
修改、保存(记得备份)
分析
步骤1、2、3、4,我之前的帖子已经描述过了,这里就不再赘述
某unity游戏逆向破解
这里直接进入第五步
首先玩一下游戏,得到一些概念
概念 |
关键字 |
防御塔 |
Tower Hero |
生命值 |
Hp |
伤害 |
Damage |
buff |
buff |
技能 |
skill |
攻击范围 |
Range Distance |
升级 |
Upgrade |
金币 |
gold``cost gain (获得金币) |
飞艇 |
ddt moab |
折扣 |
discount (猴村的折扣) |
找了很多地方,尝试了一些修改,但是效果不太理想,这里放1个有效果的
升级折扣
dnspy
搜索 GetTowerUpgradeCost
(为什么搜索这个?因为我用关键字找了很多的地方,其它地方没有效果,只有这里有效果,所以才贴出来:keai )
看到签名如下
public float GetTowerUpgradeCost(Tower tower, int path, int tier, float overrideBaseCost = -1f, bool isParagonUpgrade = false)
{
return default(float);
}
可知:方法返回一个 float浮点数,表示当前防御塔升级所需要消耗的金币
用ida看一下实现
看红色区域
LABEL_45:
v41 = *(_QWORD *)(a1 + 24);
if ( !v41 )
goto LABEL_50;
LOBYTE(v16) = FreeUpgrade == 0;
if ( (float)((float)((float)v16 * v14)
* (float)(1.0
- Assets_Scripts_Simulation_Simulation__GetSimulationBehaviorDiscount(// 得到折扣值
v41,
a2,
a3,
a4,
LODWORD(v34)).m128_f32[0])) < 0.0 )
return 0.0;
else
return (float)(int)Assets_Scripts_Simulation_SMath_Math__RoundToNearestInt(v42, 5);// 得到浮点数临近的int(floor)
大概的意思是:
- 得到折扣系数
- 如果折扣小于0(我实在想不到怎样可以小于0,那只能是程序员防御编程了,避免出bug,严谨!),那么就返回0.0
- 否则用cost*discount, 再floor一下返回(RoundToNearestInt的作用)
其实,这一个方法里面有很多的修改点
- 最后一行的v42就是折扣系数,5应该是原始价格(这里可能是ida识别错误,毕竟找不到v42的来演),那么可以修改v42或5
- 如果折扣值小于0,就返回0,可以修改为去掉判断,直接返回0(jmp)
- GetAreaDiscount得到折扣区域,我们可以加大这个返回值
- GetDiscountMultiplier得到折扣系数,我们可以加大这个返回值
修改返回值,还比较费劲,如果代码返回立即数还好说,汇编也能改,如果返回寄存器或者需要寻址,就费劲了,所以我选择了最简单的,修改跳转逻辑,也就是2
可以看到,ida里的if语句,在汇编里是
il2cpp:0000000180B5A0AE 0F 5B D2 cvtdq2ps xmm2, xmm2
il2cpp:0000000180B5A0B1 F3 41 0F 59 D0 mulss xmm2, xmm8
il2cpp:0000000180B5A0B6 F3 0F 10 05 82 9C A1 01 movss xmm0, cs:Y
il2cpp:0000000180B5A0BE F3 0F 5C C1 subss xmm0, xmm1
il2cpp:0000000180B5A0C2 F3 0F 59 D0 mulss xmm2, xmm0
il2cpp:0000000180B5A0C6 0F 2F F2 comiss xmm6, xmm2
il2cpp:0000000180B5A0C9 77 3F ja short loc_180B5A10A
看到ja
就应该能想到jmp
ja
是满足某种条件之后才跳转,跳到返回0的地方,我们改为无脑跳转jmp
就好了
首先看一下调整偏移量
77 3f
是 ja 0x41
我们改为jmp 0x41
就好
可以看到,77
改为eb
就行
转换网站是 Online-Assembler-and-Disassembler
ida改完看一下效果
看起来是有效果的,那么上 010 修改一下
010偏移和ida有所差异(不知道为啥),我是通过搜索16进制找到的
搜索 F3 0F 59 D0 0F 2F F2 77 3F
找到了唯一的一个地方(如果出现多个,那么就继续多复制几个字节,直到只有一个为止)
修改77
为eb
直接进游戏吧
有效果的
效果是:塔的升级都是免费的;放置塔要花钱,无法免费升级到模范塔
作弊和反作弊
玩了几局,效果可以的
当天也没有封号
但是第二天再登录游戏,就长草、封号了
猜测是游戏的时候,上传的那些数据,游戏服务器会做离线计算,发现不对,就封号
后面再开了一个小号,等封号之前赶紧进行竞速比赛,发现最后无法提交成绩
看来提交成绩的时候,还会再做一次数据校验,于是抓包分析了一下
以下是步骤和分析结果,结论就是:本人太菜,反作弊失败
抓包
安卓上抓包,很简单,wifi 设置代{过}{滤}理,或者用 potsern 把流量导到电脑上,或者骚气的,直接用 eCapture 抓包
电脑上,需要一定的思路和技巧
第一招: hosts
- wireshark 抓dns包,得到域名
- 修改hosts为本地 https服务
- https服务流量转到charles上
- charles通过设置 dns解析服务器,把流量发到正确的服务器上
比较费劲的是第三步,需要自己写代码实现,费了好大劲才搞出来
第二招,v2clash
这个也是偶然想到的,之前破解 羊了个羊
小程序的时候,就是用这招抓电脑版微信小程序的流量,这次试了一下,对电脑版游戏也适用
v2clash可以安装虚拟网卡,通过设置规则,让指定进程流量发到抓包软件上,就可以抓到游戏的包了(仅限http/https类型的包,如果游戏是tcp的,那得想别的办法)
以下贴几个请求和样例吧,因为有签名和加密,加密逻辑没有逆向出来,就不多写了
DNS 查询
A static-api.nkstatic.com A 104.18.16.97 A 104.18.17.97
A api.ninjakiwi.com A 104.19.235.38 A 104.19.234.38
A bfb.ninjakiwi.com A 104.19.234.38 A 104.19.235.38
A analytics.ninjakiwi.com A 104.19.234.38 A 104.19.235.38
A fast-static-api.nkstatic.com A 104.18.16.97 A 104.18.17.97
可以发现ip都是 104.19
或者 104.18
这些都是 cloudflare 的地址
/unity/time
得到服务器时间
{
"error": null,
"data": "{\"date\":{\"time\":1675178987230}}",
"sig": "392ae763cc5171fd893a2ef1640d6a26373120e5"
}
杰哥买东西
每在商店买一次东西,都会发起请求一次
连续买两次,那么在延迟几秒之后,只会发起一个请求,数量是2
比如下面买了三个辣椒
兔子是 PetRabbit
POST /btd6/mobile/geraldoshopdata HTTP/1.1
Host: bfb.ninjakiwi.com
Accept: */*
Accept-Encoding: deflate, gzip
Cookie: xxxx
user-agent: btd6-windowsplayer-34.3.6116
donottrack: 0
Content-Type: application/json
signature: 01f261aa3a490fbe4b5a385a5fcc6ad2
X-Unity-Version: 2021.3.12f1
Content-Length: 745
{
"game_id": "5158",
"attempt_id": "0",
"round_id": "78",
"geraldoshopitem_ShootyTurret": "0",
"geraldoshopitem_StackOfOldNails": "0",
"geraldoshopitem_CreepyIdol": "0",
"geraldoshopitem_JarOfPickles": "0",
"geraldoshopitem_RareQuincyActionFigure": "0",
"geraldoshopitem_SeeInvisibilityPotion": "0",
"geraldoshopitem_TubeOfAmazoGlue": "0",
"geraldoshopitem_SharpeningStone": "0",
"geraldoshopitem_WornHerosCape": "0",
"geraldoshopitem_BladeTrap": "0",
"geraldoshopitem_BottleHotSauce": "3", //辣椒
"geraldoshopitem_Fertilizer": "0",
"geraldoshopitem_PetRabbit": "0", // 兔子
"geraldoshopitem_RejuvPotion": "0",
"geraldoshopitem_GenieBottle": "2",
"geraldoshopitem_ParagonPowerTotem": "0",
"userId": "xxx",
"created_at": "2023-02-01 12:21:21",
"session_id": "1675252467",
"_delta": "5"
}
换英雄 selectHero
POST /btd6/mobile/selectHero HTTP/1.1
Host: bfb.ninjakiwi.com
Accept: */*
Accept-Encoding: deflate, gzip
Cookie: xxx
user-agent: btd6-windowsplayer-34.3.6116
donottrack: 0
Content-Type: application/json
signature: f05362162af86562455edee0cae4081d
X-Unity-Version: 2021.3.12f1
Content-Length: 133
{
"hero_name": "Geraldo",
"userId": "zzz",
"created_at": "2023-02-01 12:02:32",
"session_id": "1675252467",
"_delta": "6"
}
响应
{
"send": true,
"error": ""
}
开始游戏 startTrack
POST /btd6/mobile/startTrack HTTP/1.1
Host: bfb.ninjakiwi.com
Accept: */*
Accept-Encoding: deflate, gzip
Cookie: xxxx
user-agent: btd6-windowsplayer-34.3.6116
donottrack: 0
Content-Type: application/json
signature: 5f4397120971317beb34496ee198f965
X-Unity-Version: 2021.3.12f1
Content-Length: 409
{
"game_mode": "MagicOnly", //仅魔法
"resumed_game": "no",
"coop_mode": "no",
"map_name": "Carved", // 地图
"difficulty": "Hard", // 困难模式
"overwrote_save": "1",
"monkey_money": "52204", // 猴钞数量
"knowledge_points": "0",
"game_id": "5158",
"attempt_id": "0",
"hero_selected": "Geraldo", // 英雄
"hero_skin": "Geraldo", // 英雄皮肤
"event_id": "",
"rank": "155",
"is_restart": "false",
"userId": "xxxx",
"created_at": "2023-02-01 12:03:00",
"session_id": "1675252467",
"_delta": "1"
}
响应
{
"send": true,
"error": ""
}
改变目标 changeTargetingPrio
POST /btd6/mobile/changeTargetingPrio HTTP/1.1
Host: bfb.ninjakiwi.com
Accept: */*
Accept-Encoding: deflate, gzip
Cookie: xxx
user-agent: btd6-windowsplayer-34.3.6116
donottrack: 0
Content-Type: application/json
signature: 001c45a0dc7437980986af166483205c
X-Unity-Version: 2021.3.12f1
Content-Length: 159
{
"towerName": "Alchemist-220", // 炼金220
"prevOrNext": "Prev",
"userId": "xxx",
"created_at": "2023-02-01 12:14:14",
"session_id": "1675252467",
"_delta": "3"
}
{
"send": false,
"error": "disabled"
}
游戏结束之后 新增猴钞
POST /bank/balances HTTP/1.1
Host: api.ninjakiwi.com
Accept-Encoding: deflate, gzip
Cookie: xxx
User-Agent: btd6-windowsplayer-34.3
Content-Type: application/json
Accept: application/json
X-Unity-Version: 2021.3.12f1
Content-Length: 294
{
"data": "{\"accountHolder\":\"xxx\",\"wallets\":[\"NK_ACCDATA\"]}",
"auth": {
"session": "xxx",
"appID": 11,
"skuID": 35,
"device": "no_linkxxxx"
},
"sig": "95ae9e69bfbea2b049df7261a51e4328",
"nonce": "2998913795344284556"
}
可以看到这里的响应体里出现了 sig
,说明这个响应体被签名了, 直接进行中间人攻击是没有用的,得找到验签逻辑,破解掉才行
特殊 /storage/static/multi
游戏启动的时候,会触发这个请求,猜测是 获取每日活动之类的,竞速、boss等
但是结果是加密的,无法直接阅读
这个请求会一直发到服务器,猜测官方也是通过这个请求来实现反作弊的,但是因为不知道怎么解密,所以看不到明文,进行不下去了