淺析iOS手遊逆向和保護

網易易盾發表於2019-04-10

背景介紹


隨著手遊的發展,隨之而來的手遊逆向破解技術也越來越成熟,尤其是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


相關文章