浅析iOS手游逆向和保护

背景介绍

随着手游的发展,随之而来的手游逆向破解技术也越来越成熟,尤其是Andorid方面,各种破解文章比比皆是,相对而言,iOS方面关于手游的逆向分析文章比较少,网易易盾移动安全专家吕鑫垚将通过分析一款unity游戏和一款cocos-lua游戏来剖析一般向的游戏破解及保护思路。

识别Unity游戏

iOS平台的ipa包可以通过压缩软件解压,一般来说Unity的游戏有如下文件目录特征:

浅析iOS手游逆向和保护

破解思路

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#的接口。

浅析iOS手游逆向和保护

有了接口以后,我们就可以搜索一般游戏修改的关键字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看下:

浅析iOS手游逆向和保护

搜索lua luajit关键字得到如图信息。

浅析iOS手游逆向和保护

判定是lua脚本游戏。我们把lua脚本拖进游戏看下一般来说肯定是加密了,或者编译为luac/luajit形式,不然就太容易被破解了。

浅析iOS手游逆向和保护

根据以上结果来看,不是明文存储做了加密,而且看头几个字节很有可能是采用了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",然后向上追溯就很容易找到这个函数。

浅析iOS手游逆向和保护

对比下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()

浅析iOS手游逆向和保护


浅析iOS手游逆向和保护


浅析iOS手游逆向和保护

有了解密后的脚本我们就可以通过修改脚本达到作弊的效果,因为有了源码我们甚至可以写一个脱机挂出来,这对游戏的危害极大。

防护

可以看到lua脚本如果只加密危害是很大的,所以lua游戏需要保障lua脚本的安全可以从以下几点入手:

对lua编译为luac 或者 luajit 然后在此基础上对lua引擎修改opcode,然后修改luajit的bytecode增大逆向的难度iOS虽然strip了符号,但是由于lua是开源的很容易定位到luaL_loadbuff,所以有必要加上字符串加密和代码逻辑混淆来保护游戏的安全。

注:以上游戏仅供研究需要,如有侵权,请联系删除。

附一则“猪厂”招聘

网易易盾iOS安全开发工程师

岗位描述

1、负责网易移动端(iOS)安全技术的研究
2、负责网易移动端(iOS)安全保护方案的研发

岗位要求

1、 本科及以上学历,丰富的iOS平台开发经验
2、 扎实的Objective-C编程基础,熟悉C/C++开发
3、 熟悉汇编,掌握iOS端常见的***技术
4、 熟悉IDA Pro、LLDB、 CYCRIPT等调试分析工具,具备较强的逆向分析能力;
5、 有丰富的iOS越狱开发经验
6、 有APP/游戏加密保护经验优先
7、 有较好的学习能力和沟通能力,较强的分析、解决问题能力

有意向的同学,可投递简历至邮箱:ethernet2012@163.com

原文地址:https://blog.51cto.com/13610827/2376813

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


当我们远离最新的 iOS 16 更新版本时,我们听到了困扰 Apple 最新软件的错误和性能问题。
欧版/美版 特别说一下,美版选错了 可能会永久丧失4G,不过只有5%的概率会遇到选择运营商界面且部分必须连接到iTunes才可以激活
一般在接外包的时候, 通常第三方需要安装你的app进行测试(这时候你的app肯定是还没传到app store之前)。
前言为了让更多的人永远记住12月13日,各大厂都在这一天将应用变灰了。那么接下来我们看一下Flutter是如何实现的。Flutter中实现整个App变为灰色在Flutter中实现整个App变为灰色是非常简单的,只需要在最外层的控件上包裹ColorFiltered,用法如下:ColorFiltered(颜色过滤器)看名字就知道是增加颜色滤镜效果的,ColorFiltered( colorFilter:ColorFilter.mode(Colors.grey, BlendMode.
flutter升级/版本切换
(1)在C++11标准时,open函数的文件路径可以传char指针也可以传string指针,而在C++98标准,open函数的文件路径只能传char指针;(2)open函数的第二个参数是打开文件的模式,从函数定义可以看出,如果调用open函数时省略mode模式参数,则默认按照可读可写(ios_base:in | ios_base::out)的方式打开;(3)打开文件时的mode的模式是从内存的角度来定义的,比如:in表示可读,就是从文件读数据往内存读写;out表示可写,就是把内存数据写到文件中;
文章目录方法一:分别将图片和文字置灰UIImage转成灰度图UIColor转成灰度颜色方法二:给App整体添加灰色滤镜参考App页面置灰,本质是将彩色图像转换为灰度图像,本文提供两种方法实现,一种是App整体置灰,一种是单个页面置灰,可结合具体的业务场景使用。方法一:分别将图片和文字置灰一般情况下,App页面的颜色深度是24bit,也就是RGB各8bit;如果算上Alpha通道的话就是32bit,RGBA(或者ARGB)各8bit。灰度图像的颜色深度是8bit,这8bit表示的颜色不是彩色,而是256
领导让调研下黑(灰)白化实现方案,自己调研了两天,根据网上资料,做下记录只是学习过程中的记录,还是写作者牛逼
让学前端不再害怕英语单词(二),通过本文,可以对css,js和es6的单词进行了在逻辑上和联想上的记忆,让初学者更快的上手前端代码
用Python送你一颗跳动的爱心
在uni-app项目中实现人脸识别,既使用uni-app中的live-pusher开启摄像头,创建直播推流。通过快照截取和压缩图片,以base64格式发往后端。
商户APP调用微信提供的SDK调用微信支付模块,商户APP会跳转到微信中完成支付,支付完后跳回到商户APP内,最后展示支付结果。CSDN前端领域优质创作者,资深前端开发工程师,专注前端开发,在CSDN总结工作中遇到的问题或者问题解决方法以及对新技术的分享,欢迎咨询交流,共同学习。),验证通过打开选择支付方式弹窗页面,选择微信支付或者支付宝支付;4.可取消支付,放弃支付会返回会员页面,页面提示支付取消;2.判断支付方式,如果是1,则是微信支付方式。1.判断是否在微信内支付,需要在微信外支付。
Mac命令行修改ipa并重新签名打包
首先在 iOS 设备中打开开发者模式。位于:设置 - 隐私&安全 - 开发者模式(需重启)
一 现象导入MBProgressHUD显示信息时,出现如下异常现象Undefined symbols for architecture x86_64: "_OBJC_CLASS_$_MBProgressHUD", referenced from: objc-class-ref in ViewController.old: symbol(s) not found for architecture x86_64clang: error: linker command failed wit
Profiles >> 加号添加 >> Distribution >> "App Store" >> 选择 2.1 创建的App ID >> 选择绑定 2.3 的发布证书(.cer)>> 输入描述文件名称 >> Generate 生成描述文件 >> Download。Certificates >> 加号添加 >> "App Store and Ad Hoc" >> “Choose File...” >> 选择上一步生成的证书请求文件 >> Continue >> Download。
今天有需求,要实现的功能大致如下:在安卓和ios端实现分享功能可以分享链接,图片,文字,视频,文件,等欢迎大佬多多来给萌新指正,欢迎大家来共同探讨。如果各位看官觉得文章有点点帮助,跪求各位给点个“一键三连”,谢啦~声明:本博文章若非特殊注明皆为原创原文链接。