萌新一枚,这篇算是一个笔记吧
起因是玩某个国外的单机内购游戏,然后受不了肝度打算改存档
刚买的米10,root需要先刷开发版再申请解锁,so....
(模拟器上这破游戏打不开,不然root后就很轻松了(:з」∠))
主要思路:
安卓有备份与恢复机制,大部分应用允许从备份数据恢复。
这是无root下覆写内部存储数据最轻松的方式。
不过应用自己也能禁止或者管理备份文件,具体查Android Developer Documentation
手机:MIUI 12,安卓版本11
开始折腾:
解包备份
首先备份,只备份想要的应用。
然后把备份文件拉到电脑上去
MIUI上是存储设备\MIUI\backup\AllBackup\
bak文件拉出来,拖到HxD里看二进制
Android的备份文件很简单,主要是安卓备份文件头+tar压缩文件,当然国产MIUI还有自己额外的文件头。
文件头:
选中的区域就是Android备份文件头,
ANDROID BACKUP标识,备份版本(5.0),加密模式(none)
选中区域上方是MIUI备份文件头
选中区域下方就是压缩文件开始部分
去掉MIUI文件头(选区上方数据),然后用abe.jar解包成tar文件↗github
java -jar abe.jar unpack xxxx.bak xxx.tar
abe.jar实现的是比较通用的安卓备份协议,然而MIUI里备份设置超级简单,就是MIUI文件头+Android备份文件头+tar文件内容,无压缩无加密,所以直接在HxD里把两个文件头去了直接当tar也行...
tar文件直接7zip解压就行,至此,应该能拿到备份的数据了:
apk和cache,Android/data/包名下的数据,应用私有的数据
Unity逆向
然后是逆向Unity,不过小游戏一般没什么壳和加密,所以直接上
apk解压,assets/bin/Data/,资源文件基本都在这儿,一般拿Unity Studio↗或者disUnity↗解压一下就行
然而更多情况是遇到新版本和加密,解析不出来(doge
disunity支持的unity版本确实过老了,个人觉得Unity Studio更好用,而且还有GUI(短期记忆每天被迫查文档...
虽然我最后还是没能提取资源,有空再研究下(瘫
反编译代码
所以美术资源先放一边,先来代码
Unity现在应该都是il2cpp backend了,mono就不提了,所以直接定位路径:
运行库so:lib\arm64-v8a\libil2cpp.so
元数据文件:设备\Android\data\应用包名\il2cpp\Metadata\global-metadata.dat
然后把两个文件丢给Il2CppDumper↗,dump一下class和方法声明
Il2CppDumper.exe ibil2cpp.so global-metadata.dat
生成一堆文件,对于我来说里面最有用的是dump.cs
,所有C#类和方法声明,外加每个方法的虚拟地址
然后开始递归搜索,搜Diamond查找相关方法,然后在ida中查看相应的Assembly-CSharp.dll
文件,分析具体代码...
实际上根本分析不动(╯‵□′)╯︵┻━┻,但至少现在有汇编可以看了
存档文件
回头看存档文件,apps\com.ssicosm.dungeon_princess_2_free\sp\com.ssicosm.dungeon_princess_2_free.v2.playerprefs.xml
800KB+,一看就是存档(用Preference Key-Value存档是认真的吗...)
点进去大概是这种画风:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="LFZGUwhpJ11cQitXDFRvBTl5FUxZWQhgBFRFU1A%3D">0%2FevWhQCAEK4%2FBE%3D</string>
<string name="MFZZQjkFOm9VVxZZC2cBaSlGEVFfWFM%3D">DwIABV3MAg%3D%3D</string>
<string name="MFZZQjkAOm9VVxZZC2cCaSpg">VW5pdAUCAGnZ1gg%3D</string>
<string name="LFZGUwhpJ0pRVQNEFnRVVxJeAEpvBzl5FUxZWQhgBFRFU1E%3D">SW52ZRQCAGnZ1gg%3D</string>
<string name="LFZGUwhpLV1cWwNCKFlZWjkCOndAQg9ZC25RWhNTVg%3D%3D">SW52ZRQCAGnZ1gg%3D</string>
%3D,很明显,URL编码,先过一遍URL解码
LFZGUwhpJ11cQitXDFRvBTl5FUxZWQhgBFRFU1A%3D
=>LFZGUwhpJ11cQitXDFRvBTl5FUxZWQhgBFRFU1A=
末尾等号,多半是base64之类的,然后直接base64解码...
...获得了一堆乱码
然后只能回头分析代码,看来是简单加密过了(doge
存档解密
众所周知,AES, DES 是超级常用的加密算法,
然后我顺着C#和Unity相关的密码学库(Encrypt)找引用...找了半天没找到,淦(时间成本+⑨)
只能从存档相关变量入手,比如Diamond
这个变量,
直接在dump.cs
里搜,找到相关类Data_Manager_Cntr
,是个数据存储工具人,
查关键字save,找到SaveStageDataAll()
方法,对应虚拟地址0xA7FDD4
,离文件存储指日可待
IDA启动,载入DummyDll下的Assembly-CSharp.dll
分析
实际上完全没必要看懂代码,直接找B 0xABCDEF
之类的地址跳转指令和调用函数指令,
一路跳到真正的加密解密的地方:ObscuredInt
, ObscuredByte
, EncryptIntValue
看汇编我怀疑是异或,因为有EOR这个指令,异或计算是最高效简便的办法,至少能直接拦住改存档的萌新数个小时(比如我)
实际上看到一半啃不动了,打开iLSpy↗,看了一下包名,毕竟小型游戏或多或少会用些SDK和社区插件,除了我这种动不动就想要造一个轮子的人(专门生产不能用的方轮子)
然后查到了有个叫anti-cheat toolkit的插件,C#源代码(旧?新版本)直接到手,完美(:з」∠),剩下的就很简单了
看了下源码,和ida中比对了一下,虽然有的地方不同,但变化不大。
加密算法非常简单,就是异或!
作者指定一个key,(这个key可能和设备唯一标示符有关),然后数据转成字节流,按位与key进行异或就行了,key长度不够的话就循环用key
对于一个键值对,键名会被初始key异或混淆一下,然后键名+初始key组成一个新的key来异或键值对的值,然后再加上3bytes的类型信息和4bytes的原始数据的xxhash
异或用来混淆的话,加密解密计算就很简单了,异或奇数次,加密,异或偶数次,解密(不用遇到数论题真是太好了)
现在问题就是怎么获取key
如果能挂断点分析的话,应该能很轻松地拿到了
打开安卓模拟器,游戏启动失败...不对,可能设备相关,我模拟器拿到的key可能不管用
Android_ID高版本下,每个应用获得的ID与 硬件信息,软件信息,APP本身签名信息关联,所以很有可能每个应用拿到不一样的ANDROID_ID。Unity中用deviceUniqueIdentifier获得唯一标示符,参考↗
手机没root,告辞(当然可以考虑强制让系统dump内存,可是...C#的GC会把内存搬来搬去)
还是静态分析吧(误
进入游戏变动游戏数值,比如Diamond 16->15,然后备份,拉取存档,和原文比较,
找到变动的值,S2lhbQUCAI5W1RE=
=> VGlhbQUCAEi9msI=
对应的键:IVFRWwlYAQ==
好吧,看不出来什么,转成二进制
S2lhbQUCAI5W1RE=
=>0x5469616d 05 02 00 48bd9ac2
VGlhbQUCAEi9msI=
=>0x4b69616d 05 02 00 8e56d511
按照加密算法,`0x5469616d
和0x4b69616d
是加密数据,其他是类型信息0x05 02 00
和哈希信息0x48bd9ac2
,0x8e56d511
所以
0x5469616d
xor key = 16
0x4b69616d
xor key = 15
解得key=0x5b 69 61 6d
但是这个key只对这个变量起作用,并且键值对的键也作为key的一部分,所以,我们拿这个key去还原对应的键IVFRWwlYAQ==
对应二进制为0x2151515b095801
,获得Diam四个字母,注意到这和Diamond相似,且正好匹配7个字节,那就可以认为这个键的真实值是Diamand字符串了
得到真正的原始key="0x45383036663665",7个字节的key,先扔进xml解密一下
注意到解密出artifac这个单词(artifact),遗物当然得拼全了,于是更新下我们的key到8位0x4538303666366538
再次解密,这次拿装备名字试试,比如inven_Br怎么看都应该是inven_Bracer,得到key=0x453830366636653830366636
注意到key非常长,但对一些长字符串数据仍解密不成功,考虑到循环,尝试缩短key,得到key=0x653830366636
这个就是真正的key了,解密后数据就是
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="Inven_BeltMail_3_OptionValue6">1.7000000476837158 @20@2@0</string>
<string name="Unit_3_Weapon_1_Option5"> @15@2@0</string>
<string name="Unit_6_Weapon_2_LV">0 @5@2@0</string>
<string name="Inven_BracersLeather_1_OptionValue7">0.0 @20@2@0</string>
总之看起来非常舒服就是了233
另外anti-cheat toolkit也对内存数据部分进行了异或混淆,所以就算内存查找也多半搜不到真实数据,需要一些特别的技巧
打包备份文件
这部分本来应该很简单的,我们不是有abe.jar吗,直接java -jar abe.jar pack xxx.tar xxx.ab
不就行了....
然后我就凉了(MIUI直接恢复失败)
首先是tar打包的问题,abe.jar对应的github readme提示过,先把原备份文件的文件单给拉下来(文件在tar包中存储的顺序很重要)
tar tf 原备份文件.tar | grep -F "com.包名.name" > package.list
然后再根据此列表打包,
tar cf 打包的压缩名.tar -T package.list
package.list存储了你要压缩的文件,这样能保证严格按照安卓备份的文件顺序打包
注意linux和windows下打包产生的tar可能还是不满足Android/MIUI备份文件要求,
虽然MIIUI告诉我恢复成功了,但是app数据从原先8MB变成了100KB,很明显没有完全成功啊)
抓取debug日志分析,adb.exe找不到的话用这个来抓取日志↗
05-16 00:57:08.924 W/BpBinder(30448): Slow Binder: BpBinder transact took 3851ms, interface=miui.app.backup.IBackupManager, code=3 oneway=false
05-16 00:57:08.924 I/ACC_EVENT_TRIGGERED( 7597): TYPE: 2048 PACKAGE: com.miui.backup
05-16 00:57:08.924 I/AUTOSTART_DEBUG( 7597): EVENT IS VALID TYPE: 2048 PACKAGE: com.miui.backup
05-16 00:57:08.925 E/Backup:BackupManager(30448): IOException
05-16 00:57:08.925 E/Backup:BackupManager(30448): java.io.IOException: write failed: EPIPE (Broken pipe)
05-16 00:57:08.925 E/Backup:BackupManager(30448):