您好,登錄后才能下訂單哦!
本篇文章為大家展示了如何淺析iOS手游逆向和保護,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
隨著手游的發展,隨之而來的手游逆向破解技術也越來越成熟,尤其是Andorid方面,各種破解文章比比皆是,相對而言,iOS方面關于手游的逆向分析文章比較少,網易易盾移動安全專家呂鑫垚將通過分析一款unity游戲和一款cocos-lua游戲來剖析一般向的游戲破解及保護思路。
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手游逆向和保護,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。