淺析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關鍵字得到如圖資訊。
根據以上結果來看,不是明文儲存做了加密,而且看頭幾個位元組很有可能是採用了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安全開發工程師
崗位描述
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
相關文章
- iOS Block淺淺析2019-03-10iOSBloC
- IOS代理淺析2013-12-15iOS
- 技術淺析:前端沙箱資料安全保護的機制2024-01-31前端
- 淺析iOS-Cordova2018-11-20iOS
- iOS 併發概念淺析2016-04-26iOS
- 淺析DDOS攻擊防護思路2019-10-22
- iOS 介面效能優化淺析2019-03-04iOS優化
- iOS應⽤簽名原理淺析2020-10-10iOS
- iOS記憶體管理淺析2015-05-07iOS記憶體
- iOS MVC、MVVM、MVP架構模式淺淺析2019-03-17iOSMVCMVVMMVP架構模式
- iOS 設計模式淺析 0 – 前言2018-11-27iOS設計模式
- iOS元件化通用工具淺析2018-06-12iOS元件化
- iOS 設計模式淺析 1 - 策略2017-12-16iOS設計模式
- iOS 設計模式淺析 0 - 前言2017-12-16iOS設計模式
- iOS效能優化過程淺析2014-12-01iOS優化
- 逆向淺析常見病毒的注入方式系列之一-----WriteProcessMemory2020-08-19SSM
- 築牢國家安全屏障|《關鍵資訊基礎設施安全保護條例》淺析2021-08-24
- iOS 設計模式淺析 2 - 橋接2017-12-25iOS設計模式橋接
- iOS開發-XML&JSON淺析2015-10-27iOSXMLJSON
- 保護範圍和物件2024-03-20物件
- IOS學習之淺析深拷貝與淺拷貝2018-05-27iOS
- 淺析 Flutter 與 iOS 的檢視橋樑2020-08-06FlutteriOS
- 15 Oracle Data Guard Scenarios 保護場景2020-03-24OracleiOS
- 淺析雲伺服器常見的維護技巧2020-07-02伺服器
- 真實模式和保護模式2017-03-14模式
- 【科普】等級保護與分級保護的區別和聯絡!2022-04-15
- iOS逆向工程2016-08-31iOS
- 螢幕設定成淺綠色可以保護眼睛2008-04-16
- 淺談資料庫系統安全保護機制2011-07-06資料庫
- 【iOS開發】iOS App的加固保護原理:使用ipaguard混淆加固2023-11-10iOSAPP
- 逆向被虛擬機器所保護的二進位制檔案2020-08-19虛擬機
- iOS 富文字常用封裝(NSAttributedString淺析)2019-03-04iOS封裝
- 淺析iOS獲量思路:搭配渠道守正出奇2019-06-05iOS
- 淺析iOS-MAS&鏈式程式設計思想2018-11-20iOS程式設計
- 乙女手遊賣點在哪裡?淺析大廠乙女手遊的設計思路與特色2020-05-11
- 淺析HDFS架構和設計2019-07-25架構
- 淺析mybatis中${}和#{}取值區別2021-09-11MyBatis
- 淺析Spring的IoC和DI2018-08-20Spring