各位,很久不见,我是长期摸鱼的Floeice😇。原有的BLOG(floe-ice.cn)因为尚在服役、服务器和域名双双过期等不可抗力因素,已经化为历史尘埃。转眼本人重新回归到了跟生活对线的状态,是时候也有时间开始做一些新的产出了。
目前手边正有一款Roguelike的单机手游,最大的乐趣兼痛点自不用说,就是装备的词条(Affix)打造。下文将以修改词条洗炼逻辑为主要目的,尝试从不同角度开展对应逆向工程。
APP: com.wingjoy.coderustle2 (ver 1.8.16) OS: Redmi K40 with HyperOS 1.0.6.0 (Android 13) Tools: frida 16.1.0 / frida-tools 12.1.0 / frida-il2cpp-bridge 0.9.1
前期准备 0x00 分析架构 使用LibChecker分析该游戏架构,确认为Unity il2cpp。使用PADumper 从内存中获取libil2cpp.so及global-metadata.dat,而后使用Il2CppDumper 获取DummyDll及dump.cs以供后续分析。此处发现,libil2cpp.so中并不包含Assembly-CSharp.dll这个关键的游戏核心Assembly,但存在HybridCLR.Runtime.dll第三方C#热更新框架,其官方文档附后:HybridCLR介绍 。
通过查看原始apk内容,发现该游戏base.apk\assets\Dll目录下存在25个dll.bytes、1个VersionInfo.json配置文件。查看可知其dll命名规则为“name+md5”,同时明确Assembly-CSharp以HotFix形式加载:
1 { "isNormalUpdate" : true , "currentBuildVersionUpdateTips" : "版本(1.8.16)修复了一些bug,建议更新!" , "isInWhiteList" : false , "version" : "1.8.16" , "dllHashs" : { "assembly-CSharp" : { "name" : "Assembly-CSharp" , "size" : 4497424 , "md5" : "57955f3fa376dc99d8d1d6b238507d7b" , "isHot" : true , "persistent" : false } } , "sumSize" : 0 }
经测试,Assembly-CSharp、CodeRustle.WingjoyFramework、WingjoyDebugger.Runtime、WingjoyUtilities.Runtime、WinjoyFramework.Runtime共5个dll.bytes文件为加密状态,其他可被dnSpy正确识别为dll。由此,可基本确认游戏核心逻辑由HybridCLR以AOT或HotFix形式进行加载更新,上述5个加密dll.bytes文件于运行时解密。
0x01 分析注入点 使用IDA载入libil2cpp.so,定位至HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(0x1EB35DC),查看其交叉引用并向上定位至Wingjoy.Framework.Runtime.Launcher._LoadAotAndHotfixDlls_d__32.MoveNext(0x1DB35B8)。可以发现该函数通过调用Wingjoy.Framework.Runtime.WingjoyFramework.GetEncryptSetting(0x10B2058)、Wingjoy.Framework.Runtime.Launcher.BytesReaderInStreamingAssets(0x10A87CC)、System.Reflection.Assembly.Load_23524532(0x166F4B4)及HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(0x1EB35DC)等函数分别执行加密参数获取、dll.bytes文件读取并解密、Assembly加载等流程。
1.dll.bytes解密流程 进入Wingjoy.Framework.Runtime.WingjoyFramework.GetEncryptSetting:
可知这段函数构造一个RijndaelManaged对象(AES),读取并设置RijndaelManaged的Key/IV/Mode = CBC/Padding = PKCS7/BlockSize = 128,最终返回配置好的RijndaelManaged实例,伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 RijndaelManaged GetEncryptSetting () { if (!inited) { sub_C4E34C(globalObj1); sub_C4E34C("€3*@{" ); sub_C4E34C("€2*@{" ); inited = true ; } var rij = new RijndaelManaged(); byte [] key = Encoding.UTF8.GetBytes("€2*@{" ); rij.Key = key; byte [] iv = Encoding.UTF8.GetBytes("€3*@{" ); rij.IV = iv; rij.Mode = CipherMode.CBC; rij.Padding = PaddingMode.PKCS7; rij.BlockSize = 128 ; return rij; }
其中全局字符串€3*@{、€2*@{实际为Key和IV的占位符,需动态获取,可通过hook System.Security.Cryptography.RijndaelManaged.CreateDecryptor(0x12662C0)的Args[1]、Args[2]来获得。其函数原型为:
1 2 3 4 public override ICryptoTransform CreateDecryptor ( byte [] rgbKey, byte [] rgbIV ) ;
2.Assembly加载流程 在正式分析之前,先学习一下他人的正向开发教程:HybridCLR热更+简单实战 ,重点查看加载AOT元数据DLL任务及加载热更DLL任务部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private async UniTask _load_hotfix_dlls() { var dlls = await Addressables.LoadAssetsAsync<TextAsset>(hotUpdateDllLabelRef, null ); foreach (var asset in dlls) { Debug.Log("加载热更DLL:" + asset.name); Assembly.Load(asset.bytes); Debug.Log("加载热更DLL:" + asset.name + "完成" ); } }private async UniTask _load_meta_data_for_aot_dlls() { HomologousImageMode mode = HomologousImageMode.SuperSet; var aots = await Addressables.LoadAssetsAsync<TextAsset>(aotMetadataDllLabelRef, null ); foreach (var asset in aots) { LoadImageErrorCode errorCode = RuntimeApi.LoadMetadataForAOTAssembly(asset.bytes, mode); if (errorCode == LoadImageErrorCode.OK) { continue ; } Debug.LogError($"加载AOT元数据DLL:{asset.name} 失败,错误码:{errorCode} " ); } }
可知HotFix形式的dll通过Assembly.Load加载,AOT元数据dll通过RuntimeApi.LoadMetadataForAOTAssembly加载。
现在我们回到IDA,System.Reflection.Assembly.Load_23524532(0x166F4B4)的函数原型为:
1 public static Assembly Load (byte [] rawAssembly ) ;
HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(0x1EB35DC)的函数原型为:
1 public unsafe static extern int LoadMetadataForAOTAssembly (byte * dllBytes, int dllSize, int mode ) ;
rawAssembly、dllBytes对应的byte[]指针/数组在il2cpp中均遵循Il2CppArray布局:
1 2 3 4 5 6 struct Il2CppArray { Il2CppObject obj; Il2CppArrayBounds* bounds; uint32_t max_length; T m_Items[0 ]; };
其内存结构为:
1 2 3 4 5 6 +0x00 Il2CppClass* klass +0x08 MonitorData* monitor +0x10 NULL (一维数组) +0x18 uint32_t length +0x1C padding +0x20 uint8_t data[length]
因此通过hook Args[0]+0x18即可获得Assembly长度,而后hook Args[0]+0x20即可获得全部数据。
0x02 解密Assembly-CSharp.dll.bytes的2种方式 没啥好说的,这游戏没壳(曾经有用过FairGuard,但大概后续没再续费😥),直接frida一把梭。
手机端在root权限下运行server:
电脑端通过以下命令直接注入脚本:
1 frida -Uf com.wingjoy.coderustle2 -l D:\Floeice\com.wingjoy.coderustle2\Hook\frida.js
1.通过动态获取AES Key/IV执行通用解密 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function readIl2cppByteArray (arr ) { if (arr.isNull ()) return null ; const length = arr.add (0x18 ).readU32 (); const data = arr.add (0x20 ); return data.readByteArray (length); }function tryHook ( ) { const base = Module .findBaseAddress ("libil2cpp.so" ); if (!base) return ; const offset = 0x12662C0 ; const target = base.add (offset); console .log ("[+] Hook CreateDecryptor @" , target); Interceptor .attach (target, { onEnter (args ) { try { const key = readIl2cppByteArray (args[1 ]); const iv = readIl2cppByteArray (args[2 ]); console .log ("\n[CreateDecryptor] Key:" ); console .log (hexdump (key, { ansi : true })); console .log ("\n[CreateDecryptor] IV:" ); console .log (hexdump (iv, { ansi : true })); } catch (e) { console .log ("[!] 读取失败:" , e); } } }); return true ; }const timer = setInterval (() => { if (tryHook ()) clearInterval (timer); }, 10 );
执行结果:
可知Key = WingjoyGame_2023 ,IV = WingjoyGame_2017 。分别新建EncryptedDll、DecryptedDll文件夹,将所有dll.bytes文件放入EncryptedDll中,接着写一个批量解密python脚本即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import osfrom pathlib import Pathfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad ENCRYPTED_DIR = Path(r"D:\Floeice\com.wingjoy.coderustle2\EncryptedDll" ) DECRYPTED_DIR = Path(r"D:\Floeice\com.wingjoy.coderustle2\DecryptedDll" ) KEY = b"WingjoyGame_2023" IV = b"WingjoyGame_2017" def try_decrypt (ciphertext: bytes ) -> bytes | None : """尝试解密,如果失败(说明不是加密文件)则返回 None""" cipher = AES.new(KEY, AES.MODE_CBC, IV) plaintext = cipher.decrypt(ciphertext) try : return unpad(plaintext, AES.block_size) except ValueError: return None def decrypt_file (in_path: Path, out_path: Path ): with in_path.open ("rb" ) as f: ciphertext = f.read() result = try_decrypt(ciphertext) if result is None : print (f"[skip] Not encrypted: {in_path} " ) return out_path = out_path.with_suffix(".dll" ) out_path.parent.mkdir(parents=True , exist_ok=True ) with out_path.open ("wb" ) as f: f.write(result) print (f"[ok] Decrypted: {in_path} -> {out_path} " )def decrypt_dir (input_dir: Path, output_dir: Path ): for root, _, files in os.walk(input_dir): root_path = Path(root) for name in files: if not name.endswith(".bytes" ): continue in_path = root_path / name rel_path = in_path.relative_to(input_dir) out_path = output_dir / rel_path decrypt_file(in_path, out_path)def main (): print (f"[*] Input : {ENCRYPTED_DIR} " ) print (f"[*] Output: {DECRYPTED_DIR} " ) if not ENCRYPTED_DIR.is_dir(): print (f"[!] Input directory not found: {ENCRYPTED_DIR} " ) return decrypt_dir(ENCRYPTED_DIR, DECRYPTED_DIR) print ("[*] Done." )if __name__ == "__main__" : main()
执行后得到包含Assembly-CSharp在内的5个解密dll:
2.通过hook Assembly.Load直接获取解密内容 更直接,更强硬🥵,直接将解密后的Assembly-CSharp.dll dump至/sdcard/Download目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import "frida-il2cpp-bridge" ;const RVA_AsmLoadBytes = 0x166F4B4 ;Il2Cpp .perform (() => { const base = Il2Cpp .module .base ; const target = base.add (RVA_AsmLoadBytes); console .log ("[+] Hooking Assembly.Load(byte[]) @" , target); Interceptor .attach (target, { onEnter (args ) { const rawAssembly = args[0 ]; const length = rawAssembly.add (0x18 ).readU32 (); const dataPtr = rawAssembly.add (0x20 ); console .log ("\n[Assembly.Load] length =" , length); if (length === 4497408 ) { console .log ("[+] Detected Assembly-CSharp.dll, dumping..." ); const raw = Memory .readByteArray (dataPtr, length); const path = "/sdcard/Download/Assembly-CSharp.dll" ; const file = new File (path, "wb" ); file.write (raw); file.flush (); file.close (); console .log ("[+] Saved Assembly-CSharp.dll to" , path); } } }); });
同理,此处亦可作为后续Assembly-CSharp.dll替换的关键载点。
额外说明:仅Assembly-CSharp.dll通过Assembly.Load加载,其余24个AOT Assembly均通过RuntimeApi.LoadMetadataForAOTAssembly加载。此处加载逻辑可通过下述脚本自行验证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import "frida-il2cpp-bridge" ;const RVA_LoadMeta = 0x1EB35DC ; const RVA_AsmLoadBytes = 0x166F4B4 ; Il2Cpp .perform (() => { const base = Il2Cpp .module .base ; { const target = base.add (RVA_LoadMeta); let count = 0 ; console .log ("[+] Hooking LoadMetadataForAOTAssembly @" , target); Interceptor .attach (target, { onEnter (args ) { count++; const dllBytes = args[0 ]; const mode = args[1 ].toInt32 (); let length = 0 ; try { length = dllBytes.add (0x18 ).readU32 (); } catch (e) {} console .log (`\n===== LoadMetadataForAOTAssembly #${count} =====` ); console .log ("dllBytes =" , dllBytes); console .log ("mode =" , mode); console .log ("length =" , length); }, onLeave (retval ) { console .log ("RET =" , retval.toInt32 ()); } }); } { const target = base.add (RVA_AsmLoadBytes); let count = 0 ; console .log ("[+] Hooking System.Reflection.Assembly::Load(byte[]) @" , target); Interceptor .attach (target, { onEnter (args ) { count++; const rawAssembly = args[0 ]; let length = 0 ; let dataPtr = ptr (0 ); try { length = rawAssembly.add (0x18 ).readU32 (); dataPtr = rawAssembly.add (0x20 ); } catch (e) {} console .log (`\n===== Assembly.Load(byte[]) #${count} =====` ); console .log ("rawAssembly =" , rawAssembly); console .log ("length =" , length); console .log ("dataPtr =" , dataPtr); }, onLeave (retval ) { console .log ("RET =" , retval); } }); } });
0x03 确认修改逻辑 使用dnSpy打开解密后的Assembly-CSharp.dll,定位到:
1 2 private AffixBase GetNewAffix (EquipmentBase equipment, AffixBase oldAffix )
查看红框标注的装备词条洗炼关键逻辑:
其中CreateAffixParam为构造函数:
每洗炼一次,即赋予一次随机词条属性,同时将Refined = true、Best = value(结合上文,value实际 = false)传入CreateAffixParam。结合游戏实际表现,可知正常逻辑下,洗炼得到的词条一定不为满值(非Best),且除被洗炼词条之外的其他词条均被锁定(被洗炼词条被标记为Refined)。为了实现每条词条均可洗炼,或洗炼必为满值(过于非法且暴力,不推荐🙃),可修改如下:
1 2 bool value = true ; //等效于Best = true ,洗炼必为满值 Refined = false ; //被洗炼词条之外的其他词条不被锁定
修改实现 0x04 修改特定IL指令✔️ 使用dnSpy的“编辑IL指令”功能,分别修改如下:
1 2 3 4 5 //bool value = true ; 0000 ldc.i4.0 --> ldc.i4.1 //Refined = false ; 0037 ldc.i4.1 --> ldc.i4.0 0233 ldc.i4.1 --> ldc.i4.0
0x05 替换并加载Assembly-CSharp.dll✔️ 将上述修改后的Assembly-CSharp.dll保存并push至手机端/data/local/tmp目录下,确认文件权限无误的情况下,执行以下脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import "frida-il2cpp-bridge" ;const RVA_AsmLoadBytes = 0x166F4B4 ;const REPLACE_PATH = "/data/local/tmp/Assembly-CSharp.dll" ;Il2Cpp .perform (() => { const base = Il2Cpp .module .base ; const target = base.add (RVA_AsmLoadBytes); console .log ("[+] Hooking Assembly.Load(byte[]) @" , target); const byteClass = Il2Cpp .corlib .class ("System.Byte" ); Interceptor .attach (target, { onEnter (args ) { const orig = args[0 ]; const origLength = orig.add (0x18 ).readU32 (); console .log ("[+] [Assembly.Load] original length =" , origLength); let file; try { file = new File (REPLACE_PATH , "rb" ); } catch (e) { console .log (e); console .log ("[!] No replacement DLL found at" , REPLACE_PATH ); return ; } const newBytes = file.readBytes (); file.close (); if (!newBytes || newBytes.byteLength === 0 ) { console .log ("[!] Replacement DLL is empty" ); return ; } const newLength = newBytes.byteLength ; console .log ("[+] Replacement DLL loaded, size =" , newLength); const arr = Il2Cpp .array (byteClass, newLength); Memory .writeByteArray (arr.handle .add (0x20 ), newBytes); args[0 ] = arr.handle ; console .log ("[+] Assembly.Load(byte[]) replaced with new DLL" ); } }); });
执行结果:
相对优雅的Native hook示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include "Assembly-CSharp.h" Il2CppArray* createByteArrayFromHex () { const Il2CppImage* corlib = il2cpp_get_corlib (); Il2CppClass* byteClass = il2cpp_class_from_name (corlib, "System" , "Byte" ); Il2CppArray* arr = il2cpp_array_new (byteClass, sizeof (hexData)); memcpy (arr->vector, hexData, sizeof (hexData)); return arr; }HOOK_DEF (void *, Load, Il2CppArray* rawAssembly) { LOGI ("[+] Original dll length: %lu" , rawAssembly->max_length); Il2CppArray* newAssembly = createByteArrayFromHex (); LOGI ("[✓] Replaced with embedded dll, length: %lu" , newAssembly->max_length); return orig_Load (newAssembly); }void doHook () { HOOK_FUNC ((il2cpp_base + 0x166F4B4 ), Load); }
0x06 尝试使用HybridCLR-Hook😕 替换加载dll的方法固然传统有效,可这并不是一个可复用程度高的方案,显得太“重”。经过搜索,偶然发现了一个2年前的Github项目,且现已商业化:IIIImmmyyy/HybridCLR-Hook: Unity HybridCLRHook in runtime ,其开源版本支持对使用了HybirdCLR的热更游戏进行Hook(仅支持入参的替换)。出于好奇,我尝试clone该项目并在使用中尝试修复部分bug。因为实在是过于曲折🙃,以下仅记录关键过程。
1.agent/SymbolFinder.js中的findEnterFrameFromInterpreter 、findNative 均以特征码+指令解析方式尝试获取HybridCLR InterpFrame* InterpFrameGroup::EnterFrameFromInterpreter 及InterpFrame* InterpFrameGroup::EnterFrameFromNative 的真实入口。
2.agent/HybirdCLR.js主要实现了:解析方法结构体信息(根据EnterFrameFromInterpreter或EnterFrameFromNative传入的MethodInfo* method以及StackObject* argBase)、构造方法key、构造runtimeArgs并按pointerSize取出每个参数、执行用户回调以读取/修改参数、注册用户观察点供Hook时按key匹配。
3.SymbolFinder.js#L28 有误,其pattern扫描内容应根据HybridCLR实际版本修改为"E0 C3 21 91 E1 03 1B AA E2 03 19 AA"。
4.SymbolFinder.js#L32 有误,其指向的BL指令偏移地址应对应修改为0xC。
在具备条件且时间充裕的情况下,强烈建议搭配正向开发(Unity 2020.3.x + HybirdCLR )而后在不抹除符号表的情况下进行逆向食用更佳,如:hybridclr_trial 。
完成上述修复后,我尝试hook了一个较为简单的战斗金币掉落函数以测试效果:
1 2 public void AddDropGoldCoins (int count )
1 2 3 4 5 6 7 8 9 10 11 12 export let TestJs ={ test :function ( ){ HybridCLR .observe ("Assembly-CSharp.dll" ,"Wingjoy.CodeRustle.HotFix.UI" ,"BattleUIForm" ,"AddDropGoldCoins" ,1 , function (methodInfo,runtimeArgs ){ let count = runtimeArgs[1 ].readU32 (); console .log ("count: " +count); runtimeArgs[1 ].writeU32 (count * 50 ); }); HybridCLR .attach (); } }
正常掉落:
脚本执行LOG:
Hook后掉落:
可见HybridCLR-Hook对于简单的入参是有效的,其是否适用于其他复杂情况(如:入参为ObscuredValue;是否可以在OnLeave阶段处理函数RET)待后续再作测试。
备忘 0x07 部分物品特征码Hex 此处直接使用GameGuardian 进行搜索,其中物品数量:
1 2 3 4 // 待修正,部分逻辑有误 RealValue = SearchBase - 0x8 ObscuredValue = SearchBase - 0x10 = RealValue ^ cryptoKey cryptoKey = SearchBase - 0x14 = 444444
RealValue与ObscuredValue需要同步修改,否则闪退。
特征码Hex见Google表格 。
0x08 强制开启Debug组件
▶
Frida脚本,包含强制开启DebuggerComponent及查看调用栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 import "frida-il2cpp-bridge" ;const SET_ACTIVE_NATIVE_OFFSET = 0x1D96C9C ; Il2Cpp .perform (() => { const unityAsm = Il2Cpp .domain .tryAssembly ("UnityEngine.CoreModule" ) ?? Il2Cpp .domain .tryAssembly ("UnityEngine" ); if (!unityAsm) { console .log ("[-] UnityEngine assembly not found" ); return ; } const dbgAsm = Il2Cpp .domain .tryAssembly ("WingjoyDebugger.Runtime" ); if (!dbgAsm) { console .log ("[-] WingjoyDebugger.Runtime not loaded" ); return ; } const DebuggerComponent = dbgAsm.image .class ("WingjoyDebugger.Runtime.DebuggerComponent" ); const Behaviour = unityAsm.image .class ("UnityEngine.Behaviour" ); const GameObject = unityAsm.image .class ("UnityEngine.GameObject" ); const UObject = unityAsm.image .class ("UnityEngine.Object" ); const Component = unityAsm.image .class ("UnityEngine.Component" ); const setEnabled = Behaviour .method ("set_enabled" , 1 ); const setActive = GameObject .method ("SetActive" , 1 ); const destroy = UObject .tryMethod ("Destroy" , 1 ) ?? UObject .tryMethod ("Destroy" , 2 ); const getName = UObject .method ("get_name" , 0 ); const getGameObject = Component .method ("get_gameObject" , 0 ); let dbgComponentHandle = ptr (0 ); let dbgGameObjectHandle = ptr (0 ); const nativeSetActive = Il2Cpp .module .base .add (SET_ACTIVE_NATIVE_OFFSET ); Interceptor .attach (nativeSetActive, { onEnter (args ) { if (args[1 ].toInt32 () === 0 ) { args[1 ] = ptr (1 ); console .log ("[*] set_ActiveWindow forced -> true" ); } } }); const dbgStart = DebuggerComponent .tryMethod ("Start" , 0 ); if (!dbgStart) { console .log ("[-] DebuggerComponent.Start not found" ); return ; } console .log ("[+] Hooking DebuggerComponent.Start @" , dbgStart.virtualAddress ); Interceptor .attach (dbgStart.virtualAddress , { onEnter (args ) { try { const comp = new Il2Cpp .Object (args[0 ]); const go = comp.method ("get_gameObject" , 0 ).invoke (); dbgComponentHandle = comp.handle ; dbgGameObjectHandle = go.handle ; let name = null ; try { name = go.method ("get_name" , 0 ).invoke ()?.content ?? null ; } catch (_) {} console .log ("[+] Captured DebuggerComponent this =" , dbgComponentHandle); console .log ("[+] Captured Debugger GameObject =" , dbgGameObjectHandle, "name =" , name); } catch (e) { console .log ("[-] Failed to resolve DebuggerComponent.gameObject:" , e); } } }); console .log ("[+] Hooking Behaviour.set_enabled @" , setEnabled.virtualAddress ); Interceptor .attach (setEnabled.virtualAddress , { onEnter (args ) { if (dbgComponentHandle.isNull ()) return ; if (!args[0 ].equals (dbgComponentHandle)) return ; if (args[1 ].toInt32 () === 0 ) { args[1 ] = ptr (1 ); console .log ("[*] DebuggerComponent.enabled forced -> true" ); console .log ( Thread .backtrace (this .context , Backtracer .ACCURATE ) .map (DebugSymbol .fromAddress ) .join ("\n" ) ); } } }); console .log ("[+] Hooking GameObject.SetActive @" , setActive.virtualAddress ); Interceptor .attach (setActive.virtualAddress , { onEnter (args ) { if (dbgGameObjectHandle.isNull ()) return ; if (!args[0 ].equals (dbgGameObjectHandle)) return ; if (args[1 ].toInt32 () === 0 ) { args[1 ] = ptr (1 ); console .log ("[*] Debugger GameObject SetActive(false) blocked" ); console .log ( Thread .backtrace (this .context , Backtracer .ACCURATE ) .map (DebugSymbol .fromAddress ) .join ("\n" ) ); } } }); if (destroy) { console .log ("[+] Hooking Object.Destroy @" , destroy.virtualAddress ); Interceptor .attach (destroy.virtualAddress , { onEnter (args ) { if (dbgComponentHandle.isNull () && dbgGameObjectHandle.isNull ()) return ; const target = args[0 ]; if (dbgComponentHandle.equals (target) || dbgGameObjectHandle.equals (target)) { console .log ("[!] Destroy called on Debugger target =" , target); console .log ( Thread .backtrace (this .context , Backtracer .ACCURATE ) .map (DebugSymbol .fromAddress ) .join ("\n" ) ); return ; } try { const obj = new Il2Cpp .Object (target); const k = obj.class .fullName ; if (k && (k.includes ("Debugger" ) || k.includes ("Wingjoy" ))) { console .log ("[!] Destroy:" , k, "handle =" , target); console .log ( Thread .backtrace (this .context , Backtracer .ACCURATE ) .map (DebugSymbol .fromAddress ) .join ("\n" ) ); } } catch (_) {} } }); } console .log ("[+] Hooks installed" ); });
脚本执行LOG:
实际效果:
0x09 其他已测试的修改点位 1.提高装备爆率
1 2 private void NormalStageSettlement (MapStageConfig mapStageConfig )
2.传奇宝石掉落时固定满级
1 2 public LegendGem CreateLegendGem (int level = 1 , float expPercentage = 0f )
3.强化成功率100%
1 2 public ObscuredFloat UpgradeReformSuccessRate
1 2 3 4 public ObscuredBool ReformSuccess = true ;private GameManager ()
4.托管战斗时结算界面时间缩短至1秒
1 2 public async void ShowResult (bool isEscape, bool isWin, List<RewardParam> rewardParams, List<RewardParam> clearRewardParams, List<RewardParam> autoDecompose, WoodenStakeBattle woodenStakeBattle )
5.玩家受伤不扣血
1 2 3 4 public ObscuredBool Invincible = true ;private GameManager ()
6.免广告
1 2 3 public void PlayRewardVideo (AdsPlaceType adsPlaceType, Action callback )