本篇文章为大家展示了如何浅析iOS手游逆向和保护,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
背景介绍
随着手游的发展,随之而来的手游逆向破解技术也越来越成熟,尤其是Andorid方面,各种破解文章比比皆是,相对而言,iOS方面关于手游的逆向分析文章比较少,网易易盾移动安全专家吕鑫垚将通过分析一款unity游戏和一款cocos-lua游戏来剖析一般向的游戏破解及保护思路。
识别Unity游戏
iOS平台的ipa包可以通过压缩软件解压,一般来说Unity的游戏有如下文件目录特征:
破解思路
Unity游戏会在 /Data/Managed/Metadata下生产资源文件global-metadata.dat。游戏中使用的字符串都被保存在了一个global-metadata.dat的资源文件里,只有在动态运行时才会将这些字符串读入内存。这使得用IDA对游戏进行静态分析变得更加困难。那么为了解决这个困难,有人造了轮子,即Il2CppDumper。此可读取global-metadata.dat文件中的信息,并与可执行文件结合起来。
github:https://github.com/Perfare/Il2CppDumper 打开Il2CppDumper,会弹出一个窗口,第一个选择macho执行程序,第二个选择global-metadata.dat,然后选择对应的模式一般选auto,然后会生成如下的dump.cs 里面就是这个游戏用到的c#的接口。
有了接口以后,我们就可以搜索一般游戏修改的关键字battle,player,maxhp,fight等,然后我们定位到如果所示的类FightRoleData是我们战斗的时候角色数据来源,还有一个叫battlemanager的类,这个类是一个战斗管理者,包括开始战斗,暂停战斗,结束战斗。
public class FightRoleData : ICloneable // TypeDefIndex: 2414 { // Fields public long Sid; // 0x10 public long OwnerId; // 0x18 public long Uid; // 0x20 public int Power; // 0x28 public int Level; // 0x2C public int Sex; // 0x30 public int FlagType; // 0x34 public int RoleUnit; // 0x38 public int Sit; // 0x3C public int AttackType; // 0x40 public int Race; // 0x44 public int Professional; // 0x48 public int Star; // 0x4C public int Quality; // 0x50 public int Impression; // 0x54 public int Awaken; // 0x58 public int IsNpc; // 0x5C public int Soul; // 0x60 public int Formation; // 0x64 public int SkinID; // 0x68 public int AwakenLv; // 0x6C public int[][] Skills; // 0x70 public int[] Runes; // 0x78 public double Hp; // 0x80 public double MaxHp; // 0x88 public double Rage; // 0x90 public double MaxRage; // 0x98 public double Aggro; // 0xA0 public double MoveSpeed; // 0xA8 public double Attack; // 0xB0 public double PhysisDefense; // 0xB8 public double MagicDefense; // 0xC0 ... ... ... // Properties public ERolePosType PostitionType { get; } public ERoleGender Gender { get; } public bool IsAwaken { get; } // Methods public virtual void Init(ErlArray erlData); // RVA: 0x100EDDB68 Offset: 0xEDDB68 private static double _getProperty(ErlArray attrData, int index, bool[] checker, ERoleProperty property); // RVA: 0x100EDE8A0 Offset: 0xEDE8A0 public ERolePosType get_PostitionType(); // RVA: 0x100EDE93C Offset: 0xEDE93C public ERoleGender get_Gender(); // RVA: 0x100EDE964 Offset: 0xEDE964 public bool get_IsAwaken(); // RVA: 0x100EDE97C Offset: 0xEDE97C public object Clone(); // RVA: 0x100EDE98C Offset: 0xEDE98C public void .ctor(); // RVA: 0x100EDE994 Offset: 0xEDE994 } // Namespace: public class BattleManager : MonoBehaviour // TypeDefIndex: 3127 { // Fields ... ... ... // Properties public Camera GameCamera { get; set; } public GameObject CameraBase { get; } public bool Loading { get; set; } public BattleView battleView { get; set; } public string BattleMusic { get; } public Dictionary`2<string, RoleModelConfig> RoleModelConfigDic { get; } public int TargetFrame { get; } public static BattleManager Instance { get; } public DragonBallBattle Battle { get; } public bool Pause { get; set; } public List`1<BattleRoleController> BattleRoleControllers { get; } public bool IsSkipSuperSkill { get; } private bool _startAnimPlaying { get; } // Methods ... ... ... public void StartBattle(); // RVA: 0x101BBB1EC Offset: 0x1BBB1EC public void SkipBattle(); // RVA: 0x101BE18B0 Offset: 0x1BE18B0 ... ... ... }
至此,我们可以很容易实现两个功能跳过战斗,修改我们角色的攻击力,第一个功能可以通过hook StartBattle()方法然后获得this指针也就是BattleManager对象,然后我们根据BattleManager对象来调用SkipBattle()方法就可以了,第二个方式的话我们可以修改FightRoleData的数据来实现,那我们我们首先来看下FightRoleData在哪些地方被用到了,通过搜索可以发现这么个类:
// Namespace: BattleSystem public static class BattleAPI // TypeDefIndex: 2490 { // Methods private static T _GetConfig(long id); // RVA: 0x1000E98B4 Offset: 0xE98B4 public static DragonBallBattle Create(BattleScene scene, string hexData); // RVA: 0x100B06CFC Offset: 0xB06CFC public static DragonBallBattle Create(BattleScene scene, byte[] dataBytes); // RVA: 0x100B0950C Offset: 0xB0950C public static DragonBallBattle Create(BattleScene scene, BattleData data, optional CallBack`1<DragonBallBattle> beforeInit); // RVA: 0x100B06E04 Offset: 0xB06E04 public static BattleRole CreateBattleRole(BattleRoleConfig roleConfig, FightRoleData roleData, BattleScene scene, DragonBallBattle battle, Dictionary`2<long, List`1<int[]>> seqCache, optional double initialCD, optional double autoCD); // RVA: 0x100B0B3A0 Offset: 0xB0B3A0 private static int[] _getUniqueAttackSequence(int[] seq, long sid, Dictionary`2<long, List`1<int[]>> cache, YKRandom random); // RVA: 0x100B0CC28 Offset: 0xB0CC28 private static BattleRole _createBattleRolePartner(BattlePartnerConfig partnerConfig, BattleScene scene, int[] level, DragonBallBattle battle); // RVA: 0x100B0A4B4 Offset: 0xB0A4B4 public static void ApplyProperty(BattleRoleData roleData, FightRoleData netData); // RVA: 0x100B0CDB0 Offset: 0xB0CDB0 private static BattleRole[] _getFormatBattleRoles(BattleScene scene, List`1<FightRoleData> data, BattleFormation formatiom, int battleIndex, DragonBallBattle battle, Dictionary`2<long, List`1<int[]>> seqCache, double[] initialCDModifier, double[] autoCD); // RVA: 0x100B09C1C Offset: 0xB09C1C public static int ServerIndexToConfigIndex(int index, ERolePosType posType); // RVA: 0x100B0E2A8 Offset: 0xB0E2A8 public static void ImportConfig(IConfigImporter importer); // RVA: 0x100B0E390 Offset: 0xB0E390 }
其中CreateBattleRole这个函数用到了FightRoleData的数据,那么我们可以通过hook CreateBattleRole这个函数,同时修改第三个参数(第一个参数是this指针)对应的roledata的偏移里面的数值比如0xB0偏移位置的attack的值达到修改攻击力的目的。
防护
Unity游戏在iOS中虽然将il转成了cpp的形式,这在一定程度上增大了逆向难度,因为转成了汇编形式不容易从代码层面去分析功能。但是因为il2cpp本身的冗余性,太多的字符串、符号信息被保留了。分析者很容易通过这些信息找到突破口,所以这里给出几点意见:
-
加密global-metadata.dat
-
在c#层面进行函数符号混淆(由于函数符号混淆容易出错所以建议对核心的几个类进行混淆)
-
字符串加密,代码混淆
-
服务端不要信任客户端,增加对数据的校验,比如我上面修改了攻击力,服务器在下发roledata的时候就需要对下发的roledata进行签名,如果我客户端修改了数据,服务器校验的时候就数据签名异常,不予以信任。
谈了点Unity游戏,现在我们来谈谈一款cocos-lua游戏。
识别Lua游戏
一般来说通过这两方面来看是不是lua脚本游戏,首先解压ipa,然后进入资源目录一般来说是src或者res,里面有类似lua,luac后缀,保险一点我们把二进制拖进ida看下:
搜索lua luajit关键字得到如图信息。
判定是lua脚本游戏。我们把lua脚本拖进游戏看下一般来说肯定是加密了,或者编译为luac/luajit形式,不然就太容易被破解了。
根据以上结果来看,不是明文存储做了加密,而且看头几个字节很有可能是采用了xxtea这种加密方式(这种方式是cocos官方提供的而且特征很明显,加密后将sign追加在文件头部作为标识。加密的key则是直接写在代码里面的)
破解思路
Lua游戏的话一般来说这么2种思路:
-
获取lua脚本,替换lua脚本
-
因为lua脚本的动态特性,我们只需要通过lua引擎去加载我们的lua脚本就能达到劫持数据的作用
我们这边通过dump的方式来获取脚本,可以通过hook luaL_loadbuffer来获取解密后的脚本,但是iOS跟安卓还是有些不同,因为安卓lua是通过so来加载的,所以必定有导出函数luaL_loadbuffer。但是iOS lua已经集成到二进制中了,所以符号自然就被strip掉了,这个时候我们可以通过字符串配合lua源码来定位,比如我这边选择的字符串是”error loading module '%s' from file",然后向上追溯就很容易找到这个函数。
对比下f5内容与luaL_loadbuffer原型
int luaL_loadbuffer (lua_State *L, const char *buff, size_t sz, const char *name);
现在我们就开始编写代码来dump脚本,这边我用frida来实现,原因是frida对于这些一次性的需求实在是太好用了,不需要编译,不需要重启设备,开箱即用。
script = session.create_script(""" var baseAddr = Module.findBaseAddress('QuickMud-mobile'); var luaL_loadbuffer = baseAddr.add(0x2DF644); Interceptor.attach(luaL_loadbuffer, { onEnter: function(args) { var name = Memory.readUtf8String(args[3]); var obj = {} obj.size = args[2].toInt32() obj.name = name; obj.content = Memory.readCString(args[1], obj.size); send(obj); } } ); """) def write(path, content): print('write:', path) folder = os.path.dirname(path) if not os.path.exists(folder): os.makedirs(folder) open(path, 'w').write(content) def on_message(message, data): if message['payload']['name']: name = message['payload']['name'] name = “/Add/Your/Dump/Path/"+ name content = message['payload']['content'].encode('utf-8') dirName = os.path.dirname(name) if not os.path.exists(dirName): os.makedirs(os.path.dirname(name)) if name.endswith('.lua'): write(name, content) script.on('message', on_message) script.load() sys.stdin.read()
有了解密后的脚本我们就可以通过修改脚本达到作弊的效果,因为有了源码我们甚至可以写一个脱机挂出来,这对游戏的危害极大。
防护
可以看到lua脚本如果只加密危害是很大的,所以lua游戏需要保障lua脚本的安全可以从以下几点入手:
-
对lua编译为luac 或者 luajit 然后在此基础上对lua引擎修改opcode,然后修改luajit的bytecode增大逆向的难度
-
iOS虽然strip了符号,但是由于lua是开源的很容易定位到luaL_loadbuff,所以有必要加上字符串加密和代码逻辑混淆来保护游戏的安全。
上述内容就是如何浅析iOS手游逆向和保护,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注亿速云行业资讯频道。
原创文章,作者:carmelaweatherly,如若转载,请注明出处:https://blog.ytso.com/222449.html