浅析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 rolemodelconfigdic { get; }
    public int targetframe { get; }
    public static battlemanager instance { get; }
    public dragonballbattle battle { get; }
    public bool pause { get; set; }
    public list`1 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 beforeinit); // rva: 0x100b06e04 offset: 0xb06e04
    public static battlerole createbattlerole(battleroleconfig roleconfig, fightroledata roledata, battlescene scene, dragonballbattle battle, dictionary`2> seqcache, optional double initialcd, optional double autocd); // rva: 0x100b0b3a0 offset: 0xb0b3a0
    private static int[] _getuniqueattacksequence(int[] seq, long sid, dictionary`2> 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 data, battleformation formatiom, int battleindex, dragonballbattle battle, dictionary`2> 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

  1. 在c#层面进行函数符号混淆(由于函数符号混淆容易出错所以建议对核心的几个类进行混淆)
  2. 字符串加密,代码混淆
  3. 服务端不要信任客户端,增加对数据的校验,比如我上面修改了攻击力,服务器在下发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种思路:

  1. 获取lua脚本,替换lua脚本
  2. 因为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脚本的安全可以从以下几点入手:

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

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


网站地图