分析 unity il2cpp 编译的游戏,先解包找出 global-metadata.dat 和 libil2cpp.so 文件,尝试用 Il2CppDumper 进行分析,但是分析失败了
应该是 libil2cpp.so 被加密了(在 010 editor 中对 global-metadata.dat 运行 UnityMetadata 模板,显示模板执行成功;对 libil2cpp.so 运行 ELF 模板报错)
解密 libil2cpp.so 在 libmain.so 中找到如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bool __fastcall sub_9FC (__int64 a1, __int64 a2, __int64 a3) { size_t v5; void *v6; const void *v7; v5 = (int )((*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 1344LL ))(a1, a3) + 1 ); v6 = malloc (v5); v7 = (const void *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL ))(a1, a3, 0LL ); memcpy (v6, v7, v5); (*(void (__fastcall **)(__int64, __int64, const void *))(*(_QWORD *)a1 + 1360LL ))(a1, a3, v7); sub_BD4(a1, v6, "libunity.so" , &qword_3030); sub_BD4(a1, v6, "libil2cpp.so" , &qword_3038); free (v6); return qword_3030 != 0 ; }
最开始的想法是 hook dlopen,在 libil2cpp.so 加载完整时 dump 出来,这时候得到的 libil2cpp.so 就是解密后的,但是测试的时候发现有 frida 检测,直接运行 dump 脚本会显示 Process terminated
尝试绕过 frida 检测 frida-server 会默认开启端口 27042(主控制流端口) 和 27043(文件传输端口),app 可能会通过检测这些端口是否被占用来判断 frida 是否存在
进行端口转发
1 2 3 ./fs -l 0.0.0.0:11223 adb forward tcp:11223 tcp:11223 frida -H 127.0.0.1:11223 -f com.com.sec2023.rocketmouse.mouse -l .\hook_dec_so.js
dump libil2cpp.so 的 frida 脚本
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 function dumpSo (soName ) { const mod = Process .findModuleByName (soName); if (!mod) { console .log ("[-] Module not found: " + soName); return ; } console .log ("[*] Base: " + mod.base + ", Size: " + mod.size ); const dump_path = "/data/data/com.com.sec2023.rocketmouse.mouse/files/" + soName; const buffer = new ArrayBuffer (mod.size ); const view = new Uint8Array (buffer); mod.enumerateRanges ('r--' ).forEach (function (range ){ const offset = range.base .sub (mod.base ).toInt32 (); console .log ("[*] Reading range: 0x" + offset.toString (16 ) + ",size:" + range.size ); try { const data = ptr (range.base ).readByteArray (range.size ); const dataView = new Uint8Array (data); view.set (dataView, offset); } catch (e) { console .log ("[-] Failed to read range" + e.message ); } }); const file = new File (dump_path, "wb" ); file.write (buffer); file.close (); console .log ("[+] Dumped to: " + dump_path); }Interceptor .attach (Module .findExportByName (null , 'dlopen' ), { onEnter : function (args ) { this .path = Memory .readUtf8String (args[0 ]); }, onLeave : function (retval ) { if (!retval.isNull () && this .path && this .path .indexOf ("libil2cpp.so" ) !== -1 ) { setTimeout (function ( ) { dumpSo ("libil2cpp.so" ); }, 2000 ); } } });
成功 dump
1 2 3 4 5 6 7 8 9 10 [Remote::com.com.sec2023.rocketmouse.mouse ]-> [*] Base: 0 x6ef3e0f000, Size : 20758528 [*] Reading range : 0 x0,size :2842624 [*] Reading range : 0 x2b6000,size :11878400 [*] Reading range : 0 xe0a000,size :2453504 [*] Reading range : 0 x1062000,size :655360 [*] Reading range : 0 x1102000,size :626688 [*] Reading range : 0 x13b8000,size :8192 [*] Reading range : 0 x13ba000,size :8192 [*] Reading range : 0 x13bc000,size :65536 [+] Dumped to: /data /data /com.com.sec2023.rocketmouse.mouse/files/libil2cpp.so
直接 adb pull /data/data/com.com.sec2023.rocketmouse.mouse/files/libil2cpp.so 到本地会显示没有权限 所以先在 /data/data/com.com.sec2023.rocketmouse.mouse/files 目录下把 dump 下来的 so 移动到有访问权限的 /sdcard/Download 目录下,然后在传到本地
1 2 mv libil2cpp.so /sdcard/Download adb pull /sdcard/Download/libil2cpp.so D:\aaa\TXgame2023-1
这时候再去 010 里面对 dump 下来的 libil2cpp.so 运行 ELF 模板,显示模板执行成功
用 Il2CppDumper 对 dump 下来的 libil2cpp.so 和 global-metadata.dat 进行分析
修复 dump 下来的 so 文件 但是直接把这个文件扔进 ida 里面去恢复符号会分析失败 o.o 是因为 dump 下来的文件段和节的偏移是在虚拟空间中的偏移,而 ida 分析时的偏移是按照实际文件中的偏移来算的 这里可以使用 SoFixer 来修复
ida 打开 fix.so, File->Script file 选择 Il2CppDumper 里面的 ida_with_struct_py3.py,然后再选择刚刚 dump 得到的文件里面的 script.json 和 il2cpp.h 就可以恢复符号表了。除此之外,ida 中也要设置基地址,在 Edit->Segments->Rebase program…填入 hook 出的 base 地址
修改金币数量 在 ida 中搜索 MouseController 找到相关函数,看到有个 CollectCoin
写 frida 脚本 hook TssSdtInt__op_Implicit 返回值让它等于 1000 先在 dump.cs 中找到相关的地址偏移
1 2 3 4 5 public static int op_Implicit (TssSdtInt v ) { }private void CollectCoin (Collider2D coinCollider ) { }
hook 脚本
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 function hook_coin ( ){ const mod = Process .findModuleByName ("libil2cpp.so" ); if (!mod){ console .log ("[-] Module not found" ); return ; } let baseAddr = mod.base ; console .log ("[*] libil2cpp.so base address: " + baseAddr); let coin_Offset = 0x4652E4 ; let op_Implicit_Offset = 0x464794 ; let coin_addr = baseAddr.add (coin_Offset); let op_Implicit_addr = baseAddr.add (op_Implicit_Offset); let coin_end_addr = coin_addr.add (0x160 ) Interceptor .attach (op_Implicit_addr,{ onEnter :function (args ){ this .returnAddr = this .context .lr }, onLeave :function (retval ){ if (this .returnAddr >= coin_addr && this .returnAddr < coin_end_addr){ console .log ("[*] Original coin value: " + retval.toInt32 ()); retval.replace (1000 ); console .log ("[*] Modified coin value: " + retval.toInt32 ()); } } }); }setImmediate (hook_coin,2000 );
成功 hook,游戏界面也显示了 flag
小键盘分析 游戏还有个 mod menu 菜单
本题目标为制作 RocketMouse 注册机, 获取 flag,其中注册机算法需要使用 C、C++ 语言实现,要求根据 Mod Menu 中生成的任意随机 Token,均能计算出相应的正确结果,所以现在需要分析注册机算法
ida 搜索 keyboard 可以看到好几个混淆函数,在 dump.cs 中找到相关的偏移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void iI1Ii (SmallKeyboard.iII1i _info ) { }private void iI1Ii (GameObject go ) { }private void iI1Ii (ulong i1I ) { }private void oO0oOo0 () { }private string oO0oOoO () { }
写脚本跟踪一下这些函数
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 function trace ( ) { let mod = Process .findModuleByName ("libil2cpp.so" ); if (mod == null ) { setTimeout (trace, 1000 ); return ; } console .log ("libil2cpp.so Base Address: " + mod); function readIl2CppString (ptr ){ if (ptr.isNull ()) return "null" ; try { return ptr.add (0x14 ).readUtf16String (); } catch (e) { return "Cannot read string (" + ptr + ")" ; } } function traceMethod (offset, className, methodSignature ) { let funcAddr = mod.base .add (offset); try { Interceptor .attach (funcAddr, { onEnter : function (args ) { console .log ("method call" ); console .log (" nameSpaze: class:" + className); console .log (" methodPointer offset :" + offset.toString (16 ).toUpperCase ()); console .log (" " + methodSignature); console .log (" method end" ); console .log ("" ); }, onLeave : function (retval ) { } }); console .log (`[+] Hooked ${className} at ${offset.toString(16 )} ` ); } catch (e) { console .log (`[-] Failed to hook ${offset.toString(16 )} : ${e.message} ` ); } } let cls = "SmallKeyboard" ; traceMethod (0x465880 , cls, "private void iI1Ii(iII1i _info)" ); traceMethod (0x465FDC , cls, "private void iI1Ii(GameObject go)" ); traceMethod (0x465AB0 , cls, "private void iI1Ii(UInt64 i1I)" ); traceMethod (0x465E90 , cls, "private void oO0oOo0()" ); traceMethod (0x466184 , cls, "private string oO0oOoO()" ); traceMethod (0x46618C , cls, "private void Start()" ); traceMethod (0x466300 , cls, "public void _ctor()" ); }setImmediate (trace);
点击小键盘上的数字时触发
1 2 3 4 5 6 7 8 9 10 11 method call nameSpaze : class :SmallKeyboard methodPointer offset :465 FDC private void iI1Ii(GameObject go) method end method call nameSpaze : class :SmallKeyboard methodPointer offset :465880 private void iI1Ii(iII1i _info) method end
点击 OK 键时触发
1 2 3 4 5 6 7 8 9 10 11 method call nameSpaze : class :SmallKeyboard methodPointer offset :465 AB0 private void iI1Ii(UInt64 i1I) method end method call nameSpaze : class :SmallKeyboard methodPointer offset :465 E90 private void oO0oOo0() method end
ida 中看到
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 void SmallKeyboard__iI1Ii (SmallKeyboard_o *this, SmallKeyboard_iII1i_o *info, const MethodInfo *method) { __int64 v5; __int64 v6; __int64 v7; unsigned int KeyType; struct System_String_o *v9 ; struct System_String_o *v10 ; __int64 *v11; _QWORD *v12; System_String_o *v13; __int64 v14; Il2CppObject *v15; System_String_o *iIIIi; uint64_t v17; const MethodInfo *v18; const MethodInfo *v19; UnityEngine_GameObject_o *inputObj; Il2CppObject *Component_object; if ( (qword_6EF5FA2378 & 0x100000000000000L L) == 0 ) { sub_6EF5191B58(off_6EF5EF9A68, info); sub_6EF5191B58(off_6EF5EFDB28, v5); sub_6EF5191B58(off_6EF5EEB4F0[0 ], v6); sub_6EF5191B58(off_6EF5EF5E40[0 ], v7); HIBYTE(qword_6EF5FA2378) = 1 ; } if ( !info ) LABEL_19: sub_6EF5191C40(); KeyType = info->fields.KeyType; if ( KeyType < 2 ) { v10 = System_String__Concat_8591696(this->fields.iIIIi, info->fields.SValue, 0LL ); LABEL_10: this->fields.iIIIi = v10; goto LABEL_16; } if ( KeyType == 2 ) { v11 = (__int64 *)off_6EF5EFDB28; v12 = off_6EF5EF9A68; v13 = System_String__Concat_8591696(*(System_String_o **)off_6EF5EF5E40[0 ], this->fields.iIIIi, 0LL ); v14 = *v11; v15 = (Il2CppObject *)v13; if ( !*(_DWORD *)(v14 + 224 ) ) j_il2cpp_runtime_class_init_0(v14); UnityEngine_Debug__LogWarning(v15, 0LL ); iIIIi = this->fields.iIIIi; if ( !*(_DWORD *)(*v12 + 224LL ) ) j_il2cpp_runtime_class_init_0(*v12); v17 = System_Convert__ToUInt64_8763844(iIIIi, 0LL ); SmallKeyboard__iI1Ii_4610736(this, v17, v18); SmallKeyboard__oO0oOo0(this, v19); } else if ( KeyType == 3 ) { v9 = this->fields.iIIIi; if ( !v9 ) goto LABEL_19; v10 = System_String__Remove_8642964(v9, v9->fields._stringLength - 1 , 0LL ); goto LABEL_10; } LABEL_16: inputObj = this->fields.inputObj; if ( !inputObj ) goto LABEL_19; Component_object = UnityEngine_GameObject__GetComponent_object_( inputObj, *(const MethodInfo_521C18 **)off_6EF5EEB4F0[0 ]); if ( !Component_object ) goto LABEL_19; ((void (__fastcall *)(Il2CppObject *, struct System_String_o *, const MethodInfo *))Component_object->klass->vtable[75 ].methodPtr)( Component_object, this->fields.iIIIi, Component_object->klass->vtable[75 ].method); }
在 dump.cs 中找到 KeyType
1 2 3 4 5 6 7 8 9 public enum SmallKeyboard.KeyboardType { public int value__; public const SmallKeyboard.KeyboardType Number = 0 ; public const SmallKeyboard.KeyboardType Character = 1 ; public const SmallKeyboard.KeyboardType EnterKey = 2 ; public const SmallKeyboard.KeyboardType BackSpace = 3 ; }
可以看出 EnterKey 对应的值是 2,这个应该就对应的是小键盘中的 OK 键
hook 一下 SmallKeyboard__iI1Ii_4610736; 和 SmallKeyboard__oO0oOo0;的参数
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 function readil2cppString (ptr ){ if (ptr.isNull ()){ return "" ; } try { let length = ptr.add (0x10 ).readInt (); return ptr.add (0x14 ).readUtf16String (length); }catch (e){ return "(error reading string)" ; } }function hook_SmallKeyboard ( ){ let mod = Process .findModuleByName ("libil2cpp.so" ); if (!mod){ console .log ("[-] Module not found" ); return ; } let baseAddr = mod.base ; console .log ("[*] libil2cpp.so base address: " + baseAddr); let func_offsets = [0x465AB0 , 0x465E90 ]; func_offsets.forEach (function (offset, index ){ let func_addr = baseAddr.add (offset); console .log ("[*] Hooking function" + index + " at offset: " + offset.toString (16 )); Interceptor .attach (func_addr, { onEnter : function (args ){ console .log ("[*] Function " + index + " (offset: 0x" + offset.toString (16 ) + ") called" ); this .instance = args[0 ]; this .arg2 = args[1 ]; this .arg3 = args[2 ]; console .log (" |-- instance (this): " + this .instance ); console .log (" |-- arg2: " + this .arg2 ); console .log (" |-- arg3: " + this .arg3 ); let str = readil2cppString (this .arg2 ); console .log (" |-- arg2 string value: " + str); }, onLeave : function (retval ){ console .log ("[*] Function " + index + " returned" ); } }); }) } setImmediate (hook_SmallKeyboard);
输出中可以看到 function 0 (SmallKeyboard__iI1Ii_4610736) 其中的 args2 就是小键盘中的输入(输入为 1111)
1 2 3 4 5 6 7 8 9 10 11 12 [*] Function 0 (offset: 0x465ab0) called |-- instance (this): 0x70a2458a00 |-- arg2: 0x457 |-- arg3: 0x0 |-- arg2 string value: (error reading string) [*] Function 0 returned [*] Function 1 (offset: 0x465e90) called |-- instance (this): 0x70a2458a00 |-- arg2: 0x0 |-- arg3: 0x0 |-- arg2 string value: [*] Function 1 returned
在 ida 中点进 SmallKeyboard__iI1Ii_4610736 函数
它从一个叫 g_sec2023_p_array_ptr 的全局结构体中,读取偏移量为 0x48 的函数指针,并跳转执行该函数; U P 重新反编译一下可以看到它是全局表 g_sec2023_p_array_ptr 里面的第十个函数 那么现在就去看看 libsec2023.so
libsec2023.so 分析 在 libsec2023.so 的导出表中看到 g_sec2023_p_array_ptr,点进去可以看到一个跳转表,我们要找的 0x48 偏移处的就是 sub_31164 这个函数
用 frida hook 这个函数游戏会显示 hack detected,然后闪退,应该是有检测
由于目前没法使用 stackplz 或者 rwProcmem33,原因详见记一次失败的把 rwProcMem33 编译进 MI MIX2 内核的尝试 ,没法找到具体的检测函数,这部分的内容就暂且跳过
emmm 后续来了,买了台 pixel6,我爽学内核爽用 stackplz,回顾上文 hook 函数退出,可能是有 crc 校验,之后又去尝试在 libsec2023.so 里面 hook openat 等函数但没 hook 到,估计是通过 SVC 调用来对抗 frida hook ps: SVC(SuperVisor Call) 是 ARM 架构中的一条汇编指令,能够从用户态切换到内核态,允许应用程序请求操作系统执行特权操作 如果直接调用系统库的的函数,frida 可以轻松拦截到,但是如果通过 SVC 指令调用内核提供的系统调用接口,frida 就无法直接拦截,因为它是直接写汇编,不调用 libc 的 open
1 2 3 4 5 6 7 8 9 10 11 long raw_syscall_openat (int dirfd, const char *pathname, int flags) { long ret; __asm__ volatile ( "mov x8, #56\n" "svc #0\n" : "=r" (ret) : "r" (dirfd), "r" (pathname), "r" (flags) : "x8" , "memory" ) ; return ret; }
于是用 stackplz 追踪内核,果然发现 libsec2023.so 在时调用 openat 读取 /proc/self/maps
1 2 3 4 5 6 7 8 9 10 11 12 13 [12070|12092|Thread-927] openat(dirfd=-100, *pathname=0x79e1e7cbd9(/proc/self/maps), flags=0x0, mode=0o666(S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)) LR:0x79e1e285a4 PC:0x79e1e29ff0 SP:0x79e1cfdb70, Backtrace: # 00 pc 0000000000020ff0 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 01 pc 000000000001f5a0 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 02 pc 000000000002db14 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 03 pc 000000000002e2a0 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 04 pc 000000000002e354 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 05 pc 0000000000039630 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 06 pc 0000000000038ea4 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 07 pc 0000000000036498 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 08 pc 000000000003633c /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 09 pc 0000000000011f40 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so # 10 pc 0000000000080e6c /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+236) # 11 pc 00000000000736d0 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
SVC 调用
根据栈回溯往前寻找,在 0x36498 处跳转的地址不是常量,而是寄存器 x8 中保存的地址
hook x8 寄存的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let soName = "libsec2023.so" ;let offset_instr = 0x36498 ;function hookCmpInstruction ( ) { const mod = Process .findModuleByName (soName); if (!mod) { console .log ("[-] Module not found: " + soName); return ; } let moduleBase = mod.base ; let targetAddress = moduleBase.add (offset_instr); console .log ("Found " + soName + " at: " + moduleBase); Interceptor .attach (targetAddress, { onEnter : function (args ) { let x8 = this .context .x8 console .log ("x8 " + x8); let x0 = this .context .x0 ; } }); }setImmediate (hookCmpInstruction,0 );
hook 结果:0x6de98390ac - 0x6de9802000 = 0x370ac
1 2 3 [Remote::com. com. sec2023. rocketmouse. mouse ]-> Found libsec2023. so at : 0x6de9802000 Found libsec2023. so at : 0x6de9802000 x8 0x6de98390ac
分析 sub_370ac
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 000370 ac uint64_t sub_370ac (void * arg1) 000370c4 uint64_t x20 = _ReadMSR(SystemReg: tpidr_el0)000370 c8 int64_t x8 = *(x20 + 0x28 )000370e0 *(arg1 + 0x38 ) += 1 000370e8 void var_50000370e8 sub_36558(&var_50, arg1)000370f 0 int64_t x0_1 = *(arg1 + 0x20 )000370f 0 000370f 4 if (x0_1 != 0 )000370f 8 sub_413a4(mem: x0_1)000370f 8 00037100 __builtin_memset(dest: arg1 + 0x20 , ch: 0 , count: 0x18 )00037130 int32_t x19_100037130 00037130 switch (sub_37254(arg1) != 0 ? 1 : 0 )00037134 case 0 00037134 int64_t * x22_1 = *(arg1 + 0x20 )00037134 int64_t x23_1 = *(arg1 + 0x28 )00037144 int64_t x8_700037144 00037144 x8_7 = x23_1 == x22_1 ? 8 : 0x18 00037144 00037158 switch (x8_7)00037184 case 0x18 00037184 while (true )00037184 int64_t x8_13 = x22_1[1 ]00037194 int64_t x9_100037194 00037194 x9_1 = x8_13 == 0 ? 0x30 : 0x20 00037194 000371 a8 switch (x9_1)000371b 0 case 0x20 000371b 0 uint64_t x2_1 = zx.q(x22_1[2 ].d)000371b c int64_t x1_1 = *x22_1 + x8_13000371 c4 int32_t var_54 = 0xeecf7326 000371 dc int64_t x8_15000371 dc 000371 dc if (sub_376cc(&var_54, x1_1, x2_1) == x22_1[3 ].d)000371 dc x8_15 = 0x28 000371 dc else 000371 dc x8_15 = 0x38 000371 dc 000371f 0 switch (x8_15)000371f 0 case 0x38 000371f 0 goto label_3720c000371f 0 0003715 c x22_1 = &x22_1[4 ]0003716 c int64_t x8_100003716 c 0003716 c x8_10 = x23_1 == x22_1 ? 8 : 0x18 0003716 c 00037180 switch (x8_10)00037180 case 8 00037180 break 00037180 case 0x18 00037180 continue 00037180 00037218 x19_1 = 0 0003720 c case 1 0003720 c label_3720c:0003720 c (*(arg1 + 0x18 ) ^ 0x1d3a3590 )(zx.q(*(arg1 + 8 )))00037210 x19_1 = -1 00037210 00037220 sub_36580(&var_50)00037220 00037230 if (*(x20 + 0x28 ) == x8)0003724 c return zx.q(x19_1)0003724 c 00037250 __stack_chk_fail()00037250 noreturn
核心逻辑大概是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 switch (sub_37254(arg1) != 0 ? 1 : 0 ) case 0 : while (true ) int64_t offset = x22_1[1 ]; int64_t size = x22_1[2 ]; int64_t ptr = *x22_1 + offset; int32_t seed = 0xeecf7326 ; if (sub_376cc(&seed, ptr, size) != x22_1[3 ]) { goto label_3720c; } x22_1 = &x22_1[4 ]; x19_1 = 0 ;
查看 sub_376cc 函数验证推测,这个函数里面有两个间接跳转,通过 hook br 跳转的寄存器值再在 bn 的 mlil 中 Set Value,得到完整的反编译代码,是一个标准的 crc32 算法
绕过 crc 校验:
把 csel x8, x8, x9, eq 指令改成 csel x8, x8, x8, eq
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function anti_sec2023 ( ) { Java .perform (function ( ) { var currentApplication = Java .use ("android.app.ActivityThread" ).currentApplication (); var libso = Process .findModuleByName ("libsec2023.so" ); console .log ("[name]:" , libso.name ); console .log ("[base]:" , libso.base ); console .log ("[size]:" , ptr (libso.size )); console .log ("[path]:" , libso.path ); const basePtr = ptr (libso.base ); Memory .protect (basePtr, libso.size , 'rwx' ); basePtr.add (0x371DC ).writeByteArray ([0x08 ,0x01 ,0x88 ,0x9A ]); console .log ("Patched libsec2023.so at offset 0x371DC" ); }); }setImmediate (anti_sec2023,1000 );
后面发现 0x36498 处指令所在的函数开头就有个 sleep,从这里其实就能够看出端倪了
解混淆 ps:bn 中 rebase 在 Analysissub_31164 中调用了 sub_3b8cc,点进去看到有跳转 bn 没有分析出来,可以通过把 Sections 中的 .data 设置为 Read-only data 解决
对于去除 CSEL-BR/CSET-BR 这类结构的间接跳转,一种思路通过模拟执行计算出跳转地址,再 patch 汇编指令,但是这种方式对于期望结构外的跳转汇编不好处理;另外一种方式,通过根据 workflow 相关原理修改 bn 中的 IL 来实现
加密算法 加密一 sub_3b9d4 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 0003b 9d4 void sub_3b9d4 (int64_t arg1) 0003b9d8 int64_t x8 = 0 0003b 9f0 jump_table_72c40[0 ]0003b 9f0 0003b a04 while (true ) 0003ba04 int64_t x10_3 = 3 0003b a18 jump_table_72c40[2 ]0003b a24 int32_t var_4 = 0 0003b a2c int32_t x11_3 = 0x18 0003b a2c 0003b a48 while (true )0003b a48 *(&var_4 + x10_3) =0003b a48 (*(arg1 + (x8 << 2 )) u>> x11_3).b ^ x10_3.b0003b a4c x10_3 -= 1 0003b a58 int64_t x12_50003b a58 0003b a58 x12_5 = x10_3 s>= 0 ? 0x10 : 0x18 0003b a58 0003b a68 x11_3 -= 8 0003b a68 0003b a70 switch (x12_5)0003b a70 case 0x10 0003b a70 continue 0003b a70 case 0x18 0003b a70 break 0003b a70 0003b a94 var_4:3.b ^= 0x86 0003b a9c var_4:2.b -= 0x5e 0003b aa0 int64_t x11_7 = 3 0003b aa8 var_4:1.b ^= 0xd3 0003b ab0 var_4.b -= 0x1c 0003b abc *(arg1 + (x8 << 2 )) = 0 0003b ac4 jump_table_72c40[4 ]0003b ad0 int32_t x10_4 = 0 0003b ad8 int32_t x12_11 = 0x18 0003b ad8 0003b ae8 while (true )0003b ae8 char x14_3 = *(&var_4 + x11_7) - x12_11.b0003b aec *(&var_4 + x11_7) = x14_30003b af8 x11_7 -= 1 0003b b00 x10_4 += zx.d(x14_3) << x12_110003b b0c *(arg1 + (x8 << 2 )) = x10_40003b b10 int64_t x13_50003b b10 0003b b10 x13_5 = x11_7 s>= 0 ? 0x20 : 8 0003b b10 0003b b20 x12_11 -= 8 0003b b20 0003b b28 switch (x13_5)0003b b28 case 8 0003b b28 break 0003b b28 case 0x20 0003b b28 continue 0003b b28 0003b b2c x8 += 1 0003b b38 int64_t x10_50003b b38 0003b b38 x10_5 = x8 u< 2 ? 0 : 0x28 0003b b38 0003b b4c switch (x10_5)0003b b4c case 0 0003b b4c continue 0003b b4c case 0x28 0003b b4c break
frida hook 一下函数的参数和返回值,输入 11111111(即 A98AC7)
用 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 def _u8 (x ): return x & 0xFF def encrypt_word (w: int ) -> int : res = 0 for i in range (4 ): b = (w >> (8 * i)) & 0xFF t = b ^ i if i == 3 : t = t ^ 0x86 elif i == 2 : t = _u8(t - 0x5E ) elif i == 1 : t = t ^ 0xD3 else : t = _u8(t - 0x1C ) v = _u8(t - (i * 8 )) res |= (v << (8 * i)) return res & 0xFFFFFFFF def decrypt_word (w_enc: int ) -> int : res = 0 for i in range (4 ): v = (w_enc >> (8 * i)) & 0xFF t = _u8(v + (i * 8 )) if i == 3 : t = t ^ 0x86 elif i == 2 : t = _u8(t + 0x5E ) elif i == 1 : t = t ^ 0xD3 else : t = _u8(t + 0x1C ) b = t ^ i res |= (b << (8 * i)) return res & 0xFFFFFFFF if __name__ == "__main__" : x = 11111111 e = encrypt_word(x) d = decrypt_word(e) print ("input:" , x) print ("encrypted (dec):" , e) print ("encrypted (hex):" , hex (e)) print ("decrypted:" , d)
加密二 在第一次加密之后有个 _byteswap,然后再把结果传给第二个加密函数 sub_3a924
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 0003b 8cc uint64_t sub_3b8cc (int64_t arg1) 0003b8e4 uint64_t x20 = _ReadMSR(SystemReg: tpidr_el0)0003b 8e8 int64_t x8 = *(x20 + 0x28 )0003b 8f4 uint32_t var_58 = (arg1 u>> 0x20 ).d0003b 8f4 int32_t var_54 = arg1.d0003b 8fc sub_3b9d4(&var_58) 0003b 904 int32_t temp0 = _byteswap(var_58)0003b 908 int32_t var_50 = 0 0003b 908 int32_t var_4c = temp00003b 920 0003b 920 int32_t x0_2 = sub_3a924(sub_3a054(), &var_4c, 4 , &var_50, 4 )0003b 928 int64_t x8_3 = 0x10 0003b 934 ASSERT(x8_3, ConstantValue: 0x10 )0003b 934 int64_t x8_40003b 934 0003b 934 x8_4 = x0_2 != 0 ? 0x18 : 0x10 0003b 934 0003b 93c ASSERT(x8_4, ConstantValue: 0x10 )0003b 960 int32_t x19_1 = var_500003b 968 int32_t temp0_1 = _byteswap(var_54)0003b 96c var_50 = 0 0003b 96c var_4c = temp0_10003b 984 sub_3a924(sub_3a054(), &var_4c, 4 , &var_50, 4 ) 0003b 984 0003b 990 switch (x0_2 != 0 ? 1 : 0 )0003b 998 case 1 0003b 998 sub_367b0()0003b 998 0003b 9ac if (*(x20 + 0x28 ) == x8)0003b 9cc 0003b 9cc return zx.q(x19_1) | zx.q(var_50) << 0x20 0003b 9cc 0003b 9d0 __stack_chk_fail()0003b 9d0 noreturn ``` `byteswap` 那一行对应汇编:对 32 位值做了一个字节序反转(大小端序转换) ```asm 0003b 900 e80b40b9 ldr w8, [sp, #0x8 {var_58}]0003b 904 0809 c05a rev w8, w8
然后把反转后的值传给 sub_3a924 函数,进行第二次加密 hook sub_3a924 函数,这个函数在 sub_3b8cc 中被调用了两次
由上文可知经过第一次 enc2 后得到密文 e4 ca 94 6d ab 50 3d 6d
可以看到它是先对密文的高 32 位进行加密,然后对低 32 位进行加密
在 sub_3a924 里面可以看到好多都是类型都是 uint64_t*,还有 +0x680 和 +0x580,这里其实应该是 JNIEnv* 结构体指针,但是 bn 中没有 JNI 相关的类型,需要自己导入 可以参考项目 jni_helper
先用 extract_jni.py 生成 apk 的 signature.json 文件
1 2 3 pip3 install androguard rich pyelftools python extract_jni.py mouse_pre.aligned.signed.apk -o signature.json
然后在 bn 中 Run Script 选择jni_helper.py,根据弹窗导入 signature.json 文件 之后按 Y 把类型改成 JNIEnv*,清晰多了
hook 一下 JNI 函数 GetStaticMethodID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hook_GetStaticMethodID ( ) { console .log ("hook JNIgetStaticMethodID" ); let symbols = Module .load ("libart.so" ).enumerateSymbols (); for (let i = 0 ; i < symbols.length ; i++) { let symbol = symbols[i]; if (symbol.name .indexOf ("art" ) >= 0 && symbol.name .indexOf ("JNI" ) >= 0 && symbol.name .indexOf ("GetStaticMethodID" ) >= 0 && symbol.name .indexOf ("CheckJNI" ) < 0 ) { console .log (symbol.name ); Interceptor .attach (symbol.address , { onEnter : function (args ) { var Name = args[2 ].readUtf8String (); var sig = args[3 ].readUtf8String (); let whoCallIt = DebugSymbol .fromAddress (this .returnAddress ).toString (); console .log ("getStaticMethodID:" , Name , sig, whoCallIt); } }) } } }
hook 结果中可以看到它调用了 encryt 方法
1 2 3 4 5 6 7 hook JNIgetStaticMethodID_ZN3art3JNIILb1EE17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS7_ _ZN3art3JNIILb0EE17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS7_ getStaticMethodID : isDebuggerConnected ()Z 0 x6de982e220 libsec2023.so!0 x2d220getStaticMethodID : encrypt ([B)[B 0 x6de983ba48 libsec2023.so!0 x3aa48getStaticMethodID : encrypt ([B)[B 0 x6de983ba48 libsec2023.so!0 x3aa48getStaticMethodID : isDebuggerConnected ()Z 0 x6de982e220 libsec2023.so!0 x2d220
但是在 apk 中搜索找不到这个方法,推测通过动态加载 dex 来进行类的调用 用 frida-dexdump 导出内存中的 dex 文件
1 frida-dexdump -H 127.0.0.1:13131 -f com.com.sec2023.rocketmouse.mouse
然后把 dump 出来的 dex 挨个反编译找 encrypt 方法,用 jadx 看会有混淆,用 jeb 反编译可以直接去掉
用 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 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 def ror32 (v, r ): return ((v >> r) | ((v & ((1 << r) - 1 )) << (32 - r))) & 0xFFFFFFFF key = [50 , -51 &0xFF , -1 &0xFF , -104 &0xFF , 25 , -78 &0xFF , 0x7C , -102 &0xFF ]def encrypt_4bytes (b: bytes ) -> bytes : assert len (b) == 4 v = ( (b[0 ] << 24 ) | (b[1 ] << 16 ) | (b[2 ] << 8 ) | b[3 ] ) & 0xFFFFFFFF v1 = ror32(v, 7 ) out = [ (v1 >> 24 ) & 0xFF , (v1 >> 16 ) & 0xFF , (v1 >> 8 ) & 0xFF , v1 & 0xFF ] for i in range (4 ): out[i] ^= key[i] out[i] = (out[i] + i) & 0xFF return bytes (out)def rol32 (v, r ): return ((v << r) | (v >> (32 - r))) & 0xFFFFFFFF def decrypt_4bytes (b: bytes ) -> bytes : assert len (b) == 4 tmp = list (b) for i in range (4 ): tmp[i] = (tmp[i] - i) & 0xFF tmp[i] ^= key[i] v1 = ( (tmp[0 ] << 24 ) | (tmp[1 ] << 16 ) | (tmp[2 ] << 8 ) | tmp[3 ] ) & 0xFFFFFFFF v = rol32(v1, 7 ) return bytes ([ (v >> 24 ) & 0xFF , (v >> 16 ) & 0xFF , (v >> 8 ) & 0xFF , v & 0xFF ])def rev32_bytes (b4: bytes ) -> bytes : return b4[::-1 ]def encrypt_8bytes (b8: bytes ) -> bytes : assert len (b8) == 8 lo = b8[0 :4 ] hi = b8[4 :8 ] lo = encrypt_4bytes(rev32_bytes(lo)) hi = encrypt_4bytes(rev32_bytes(hi)) return lo + hidef decrypt_8bytes (b8: bytes ) -> bytes : assert len (b8) == 8 lo = rev32_bytes(decrypt_4bytes(b8[0 :4 ])) hi = rev32_bytes(decrypt_4bytes(b8[4 :8 ])) return lo + hi data8 = bytes .fromhex("e4 ca 94 6d ab 50 3d 6d" ) enc8 = encrypt_8bytes(data8) dec8 = decrypt_8bytes(enc8)print ("input :" , data8.hex ())print ("encrypt:" , enc8.hex ()) swapped = enc8[4 :8 ] + enc8[0 :4 ]print ("swapped:" , swapped.hex ())print ("decrypt:" , dec8.hex ())
hook sub_31164 最后的 br 跳转地址
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 function anti_sec2023 ( ) { Java .perform (function ( ) { var currentApplication = Java .use ("android.app.ActivityThread" ).currentApplication (); var libso = Process .findModuleByName ("libsec2023.so" ); console .log ("[name]:" , libso.name ); console .log ("[base]:" , libso.base ); console .log ("[size]:" , ptr (libso.size )); console .log ("[path]:" , libso.path ); const basePtr = ptr (libso.base ); Memory .protect (basePtr, libso.size , 'rwx' ); basePtr.add (0x371DC ).writeByteArray ([0x08 ,0x01 ,0x88 ,0x9A ]); console .log ("Patched libsec2023.so at offset 0x371DC" ); hook_instr (); }); }setImmediate (anti_sec2023,0 );function hook_instr ( ){ let soName = "libsec2023.so" ; let offset_instr = 0x311a0 ; const mod = Process .findModuleByName (soName); if (!mod) { console .log ("[-] Module not found: " + soName); return ; } let moduleBase = mod.base ; let targetAddress = moduleBase.add (offset_instr); console .log ("Found " + soName + " at: " + moduleBase); Interceptor .attach (targetAddress, { onEnter : function (args ) { let x2 = this .context .x2 ; console .log ("x2 " + x2); addr_in_so (x2); } }); }function addr_in_so (addr ){ let process_Obj_Module_Arr = Process .enumerateModules (); console .log (addr); for (let i = 0 ; i < process_Obj_Module_Arr.length ; i++) { if (addr > process_Obj_Module_Arr[i].base && addr < process_Obj_Module_Arr[i].base .add (process_Obj_Module_Arr[i].size )){ console .log (addr.toString (16 )," located in " , process_Obj_Module_Arr[i].name ," offset: " ,(addr - process_Obj_Module_Arr[i].base ).toString (16 )); } } }
到 libil2cpp.so(fix.so) 中的 13b8d64 偏移处
1 2 3 4 .data:00000000013B8D64 loc_13B8D64 ; DATA XREF: .data:00000000013BB168↓o .data:00000000013B8D64 ; .data:00000000013BC168↓o .data:00000000013B8D64 STR X24, [SP,#-0x40]! .data:00000000013B8D68 B sub_465AB4
加密三 XTEA 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 __int64 __fastcall sub_465AB4 (__int64 a1, unsigned __int64 a2) { v2 = off_10F2820[0 ]; if ( (qword_119A380 & 0x100 ) == 0 ) { sub_389B58((__int64)off_10F1A68); sub_389B58((__int64)off_10F68F0[0 ]); sub_389B58((__int64)off_10F1708); sub_389B58((__int64)off_10E75E8[0 ]); sub_389B58((__int64)off_10E43F8[0 ]); sub_389B58((__int64)off_1100098[0 ]); sub_389B58((__int64)off_10F2820[0 ]); BYTE1(qword_119A380) = 1 ; } v5 = (System_UInt32_array *)sub_389B90(*v2, 8u ); v6 = v5; if ( !v5 ) sub_389C40(); max_length = v5->max_length; if ( !max_length ) sub_389C48(); v5->m_Items[0 ] = 0x12345678 ; if ( max_length == 1 ) sub_389C48(); v8 = off_1100098[0 ]; v5->m_Items[1 ] = 0x98765432 ; v9 = (System_Array_o *)sub_389B90(*v8, 0xC7u ); v10.fields.value = *off_10E43F8[0 ]; System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_9068608(v9, v10, 0LL ); v11 = off_10F1A68; if ( !*((_DWORD *)*off_10F1A68 + 56 ) ) j_il2cpp_runtime_class_init_0(); low32_input = System_Convert__ToUInt32_8761148((unsigned int )a2, 0LL ); if ( !LODWORD(v6->max_length) ) sub_389C48(); v6->m_Items[0 ] = low32_input; high32_input = System_Convert__ToUInt32_8761148(HIDWORD(a2), 0LL ); if ( LODWORD(v6->max_length) <= 1 ) sub_389C48(); v14 = off_10F68F0[0 ]; v6->m_Items[1 ] = high32_input; v15 = (OO0OoOOo_Oo0_o *)sub_389C30(*v14); v17 = v15; if ( !v15 ) sub_389C40(); OO0OoOOo_Oo0___ctor(v15, (System_UInt16_array *)v9, 0 , v6, v16); v17->fields.pc = v17->fields.flags; OO0OoOOo_Oo0__oOOoO0o0(v17, v18); v19 = v6->max_length; if ( !v19 ) sub_389C48(); if ( v19 == 1 ) sub_389C48(); v20 = v6->m_Items[0 ]; v21 = v6->m_Items[1 ]; key_ctx = sub_389B90(*v2, 4u ); v23.fields.value = *off_10E75E8[0 ]; System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_9068608((System_Array_o *)key_ctx, v23, 0LL ); if ( !key_ctx ) sub_389C40(); v24 = off_10F1708; key_cnt = *(_DWORD *)(key_ctx + 24 ); sum0 = 0xBEEFBEEF ; sum1 = 0x9D9D7DDE ; rounds = 64 ; do { if ( (sum0 & 3 ) >= key_cnt ) sub_389C48(); key_idx1 = (sum1 >> 13 ) & 3 ; if ( key_idx1 >= key_cnt ) sub_389C48(); v20 += (sum0 - *(_DWORD *)(key_ctx + 4LL * (sum0 & 3 ) + 32 )) ^ (((v21 << 7 ) ^ (v21 >> 8 )) + v21); --rounds; sum0 -= 0x21524111 ; v21 += (sum1 + *(_DWORD *)(key_ctx + 4LL * key_idx1 + 32 )) ^ (((v20 << 8 ) ^ (v20 >> 7 )) - v20); sum1 -= 0x21524111 ; } while ( rounds ); v30 = *(System_String_o **)(a1 + 56 ); if ( !*((_DWORD *)*v11 + 56 ) ) j_il2cpp_runtime_class_init_0(); result = System_Convert__ToUInt32_8761612(v30, 0LL ); if ( !v21 && (_DWORD)result == v20 ) **((_DWORD **)*v24 + 23 ) = -1 ; return result; }
可以看到下半部分有一个很明显的 64 轮循环是变种的 XTEA
用 frida hook 一下 key 的值
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 function hook_xtea_key ( ){ let mod = Process .findModuleByName ("libil2cpp.so" ); if (!mod){ console .log ("[-] Module not found" ); return ; } let baseAddr = mod.base ; let offset = 0x465C60 ; Interceptor .attach (baseAddr.add (offset),{ onEnter :function (args ){ console .log ("[*] sub_389B90 called" ); this .instance = args[0 ]; this .arg2 = args[1 ]; console .log (" |-- instance (this): " + this .instance ); console .log (" |-- arg2: " + this .arg2 ); console .log (hexdump (this .context .x20 ,{ offset : 0 , length : 64 , header : true , ansi : true })); }, onLeave :function (retval ){ console .log ("[*] sub_389B90 returned:" + retval); } }) }setImmediate (hook_xtea_key,0 );
得到密钥 0x7b777c63, 0xc56f6bf2, 0x2b670130, 0x76abd7fe 还原出加解密
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 def u32 (x ): return x & 0xFFFFFFFF def xtea_variant_encrypt (v0, v1, key ): v0 = u32(v0) v1 = u32(v1) sum0 = 0xBEEFBEEF sum1 = 0x9D9D7DDE delta = 0x21524111 for _ in range (64 ): v0 = u32(v0 + ((sum0 - key[sum0 & 3 ])^ (((v1 << 7 ) ^ (v1 >> 8 )) + v1))) sum0 = u32(sum0 - delta) v1 = u32(v1 + ((sum1 + key[(sum1 >> 13 ) & 3 ])^ (((v0 << 8 ) ^ (v0 >> 7 )) - v0))) sum1 = u32(sum1 - delta) return v0, v1def xtea_variant_decrypt (v0, v1, key ): v0 = u32(v0) v1 = u32(v1) delta = 0x21524111 sum0 = u32(0xBEEFBEEF - 64 * delta) sum1 = u32(0x9D9D7DDE - 64 * delta) for _ in range (64 ): sum1 = u32(sum1 + delta) v1 = u32(v1 - ((sum1 + key[(sum1 >> 13 ) & 3 ])^ (((v0 << 8 ) ^ (v0 >> 7 )) - v0))) sum0 = u32(sum0 + delta) v0 = u32(v0 - ((sum0 - key[sum0 & 3 ])^ (((v1 << 7 ) ^ (v1 >> 8 )) + v1))) return v0, v1 key = [0x7b777c63 , 0xc56f6bf2 , 0x2b670130 , 0x76abd7fe ] input_high = 0x6418873c input_low = 0xfa17d810 v0, v1 = xtea_variant_encrypt(input_high, input_low, key)print ("Encrypted: {:08x} {:08x}" .format (v0, v1)) p0, p1 = xtea_variant_decrypt(v0, v1, key)print ("Decrypted: {:08x} {:08x}" .format (p0, p1))assert p0 == input_high and p1 == input_low
加密四 vm 在魔改的 xtea 之前还有混淆的函数
1 2 3 OO0OoOOo_Oo0___ctor(v15, (System_UInt16_array *)v9, 0 , v6, v16); v17->fields.oOOO0Oo0 = v17->fields.oOOO0O0O; OO0OoOOo_Oo0__oOOoO0o0(v17, v18);
找到这个类,在其他混淆函数中还看到有 mba 表达式
可以用 ida 插件去一下混淆
重命名之后可以发现这些混淆函数好多都是在进行一些基本的计算操作
点进 ctor 下面的那个混淆函数,又有个 while(1) 循环
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 void OO0OoOOo_Oo0__oOOoO0o0 (OO0OoOOo_Oo0_o *this, const MethodInfo *method) { struct System_UInt16_array *bytecode ; __int64 pc; _UNKNOWN **v5; const MethodInfo_7C771C **v6; _DWORD *v7; int32_t v8; struct System_UInt16_array *v9 ; int32_t v10; System_Collections_Generic_Dictionary_TKey__TValue__o *dispatchTable; Il2CppObject *Item; if ( (qword_119A3D8 & 0x100 ) == 0 ) { sub_389B58((__int64)off_10EDED0[0 ]); sub_389B58((__int64)off_10EB448); BYTE1(qword_119A3D8) = 1 ; } bytecode = this->fields.bytecode; if ( !bytecode ) LABEL_17: sub_389C40(); pc = this->fields.pc; if ( (unsigned int )pc >= LODWORD(bytecode->max_length) ) LABEL_15: sub_389C48(); v5 = off_10EB448; v6 = (const MethodInfo_7C771C **)off_10EDED0[0 ]; while ( 1 ) { v7 = *v5; v8 = bytecode->m_Items[pc]; if ( !*((_DWORD *)*v5 + 56 ) ) { j_il2cpp_runtime_class_init_0(); v7 = *v5; } if ( *(_DWORD *)(*((_QWORD *)v7 + 23 ) + 68LL ) == v8 ) break ; v9 = this->fields.bytecode; if ( !v9 ) goto LABEL_17; v10 = this->fields.pc; if ( v10 >= SLODWORD(v9->max_length) ) break ; dispatchTable = (System_Collections_Generic_Dictionary_TKey__TValue__o *)this->fields.dispatchTable; this->fields.pc = v10 + 1 ; if ( !dispatchTable ) goto LABEL_17; Item = System_Collections_Generic_Dictionary_int__object___get_Item(dispatchTable, v8, *v6); if ( !Item ) goto LABEL_17; ((void (__fastcall *)(Il2CppClass *, void *))Item[1 ].monitor)(Item[4 ].klass, Item[2 ].monitor); bytecode = this->fields.bytecode; if ( !bytecode ) goto LABEL_17; pc = this->fields.pc; if ( (unsigned int )pc >= LODWORD(bytecode->max_length) ) goto LABEL_15; } }
然后去 hook 一下运算相关的指令
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 function hook_vm ( ){ let mod = Process .findModuleByName ("libil2cpp.so" ); if (!mod){ console .log ("[-] Module not found" ); return ; } let baseAddr = mod.base ; console .log (`[+] VM Hook attached at: ${baseAddr} ` ); Interceptor .attach (baseAddr.add (0x465BCC ), { onEnter : function (args ){ console .log ("low32_input: " + this .context .x0 ); } }); Interceptor .attach (baseAddr.add (0x465BF0 ), { onEnter : function (args ){ console .log ("high32_input: " + this .context .x0 ); } }); console .log ("vm start ==============" ); Interceptor .attach (baseAddr.add (0x46AEA4 ), { onEnter : function (args ){ let op1 = this .context .x10 ; let op2 = this .context .x11 ; console .log (`[ADD] ${op1} + ${op2} // Res: ${op1.add(op2)} ` ); } }); Interceptor .attach (baseAddr.add (0x46AF20 ),{ onEnter :function (args ){ console .log (`[SUB] ${this .context.x10} - ${this .context.x11} ` ); } }); Interceptor .attach (baseAddr.add (0x46AFAC ),{ onEnter :function (args ){ console .log (`[MUL] ${this .context.x10} * ${this .context.x11} ` ); } }); Interceptor .attach (baseAddr.add (0x46B028 ),{ onEnter :function (args ){ console .log (`[LShift] ${this .context.x10} << ${this .context.x11} ` ); } }); Interceptor .attach (baseAddr.add (0x46B0A4 ),{ onEnter :function (args ){ console .log (`[RShift] ${this .context.x10} >> ${this .context.x11} ` ); } }); Interceptor .attach (baseAddr.add (0x46B124 ),{ onEnter :function (args ){ console .log (`[AND] ${this .context.x10} & ${this .context.x11} ` ); } }); Interceptor .attach (baseAddr.add (0x46B1A8 ),{ onEnter :function (args ){ console .log (`[XOR] ${this .context.x10} ^ ${this .context.x12} ` ); } }); Interceptor .attach (baseAddr.add (0x46B20C ),{ onEnter :function (args ){ console .log (`[LT] ${this .context.x10} < ${this .context.x11} ` ); } }); Interceptor .attach (baseAddr.add (0x46B270 ),{ onEnter :function (args ){ console .log (`[EQ] ${this .context.x10} == ${this .context.x11} ` ); } }); }setImmediate (hook_vm);
hook 结果
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 low32_input: 0x10d817fa high32_input: 0x3c871864 [RShift] 0x21b02ff3 >> 0x18 [AND] 0x10 & 0xff [SUB] 0x18 - 0x8 [LT] 0x10 < 0x0 [RShift] 0x21b02ff3 >> 0x10 [AND] 0x10d8 & 0xff [SUB] 0x10 - 0x8 [LT] 0x8 < 0x0 [RShift] 0x21b02ff3 >> 0x8 [AND] 0x10d817 & 0xff [SUB] 0x8 - 0x8 [LT] 0x0 < 0x0 [RShift] 0x21b02ff3 >> 0x0 [AND] 0x10d817fa & 0xff [SUB] 0x0 - 0x8 [LT] 0xfffffff8 < 0x0 [SUB] 0xfa - 0x1b [XOR] 0xd7 ^ 0x17 [ADD] 0xd8 + 0xa8 // Res: 0x180 [XOR] 0x36 ^ 0x10 [XOR] 0xdf ^ 0xdf [LShift] 0xde << 0x0 [LShift] 0xfe << 0x0 [AND] 0xdf & 0xff [ADD] 0xdf + 0x0 // Res: 0xdf [ADD] 0x4 + 0x1 // Res: 0x5 [ADD] 0x0 + 0x8 // Res: 0x8 [LT] 0x8 < 0x19 [XOR] 0xdd ^ 0xd5 [LShift] 0xdc << 0x8 [LShift] 0xfe << 0x8 [AND] 0xdd00 & 0xff00 [ADD] 0xdd00 + 0xdf // Res: 0xdddf [ADD] 0x5 + 0x1 // Res: 0x6 [ADD] 0x8 + 0x8 // Res: 0x10 [LT] 0x10 < 0x19 [XOR] 0x190 ^ 0x180 [LShift] 0x18f << 0x10 [LShift] 0xfe << 0x10 [AND] 0x1900000 & 0xff0000 [ADD] 0x900000 + 0xdddf // Res: 0x90dddf [ADD] 0x6 + 0x1 // Res: 0x7 [ADD] 0x10 + 0x8 // Res: 0x18 [LT] 0x18 < 0x19 [XOR] 0x3e ^ 0x26 [LShift] 0xffffffbd << 0x18 [LShift] 0xfe << 0x18 [AND] 0x3e000000 & 0xff000000 [ADD] 0x3e000000 + 0x90dddf // Res: 0x3e90dddf [ADD] 0x7 + 0x1 // Res: 0x8 [ADD] 0x18 + 0x8 // Res: 0x20 [LT] 0x20 < 0x19 [RShift] 0x790e2fc7 >> 0x18 [AND] 0x3c & 0xff [SUB] 0x18 - 0x8 [LT] 0x10 < 0x0 [RShift] 0x790e2fc7 >> 0x10 [AND] 0x3c87 & 0xff [SUB] 0x10 - 0x8 [LT] 0x8 < 0x0 [RShift] 0x790e2fc7 >> 0x8 [AND] 0x3c8718 & 0xff [SUB] 0x8 - 0x8 [LT] 0x0 < 0x0 [RShift] 0x790e2fc7 >> 0x0 [AND] 0x3c871864 & 0xff [SUB] 0x0 - 0x8 [LT] 0xfffffff8 < 0x0 [SUB] 0x64 - 0x2f [XOR] 0xbe ^ 0x18 [ADD] 0x87 + 0x37 // Res: 0xbe [XOR] 0xbc ^ 0x3c [ADD] 0x35 + 0x0 // Res: 0x35 [LShift] 0xffffffb4 << 0x0 [LShift] 0xfe << 0x0 [AND] 0x35 & 0xff [ADD] 0x35 + 0x0 // Res: 0x35 [ADD] 0x4 + 0x1 // Res: 0x5 [ADD] 0x0 + 0x8 // Res: 0x8 [LT] 0x8 < 0x19 [ADD] 0xae + 0x8 // Res: 0xb6 [LShift] 0xb5 << 0x8 [LShift] 0xfe << 0x8 [AND] 0xb600 & 0xff00 [ADD] 0xb600 + 0x35 // Res: 0xb635 [ADD] 0x5 + 0x1 // Res: 0x6 [ADD] 0x8 + 0x8 // Res: 0x10 [LT] 0x10 < 0x19 [ADD] 0xbe + 0x10 // Res: 0xce [LShift] 0xcd << 0x10 [LShift] 0xfe << 0x10 [AND] 0xce0000 & 0xff0000 [ADD] 0xce0000 + 0xb635 // Res: 0xceb635 [ADD] 0x6 + 0x1 // Res: 0x7 [ADD] 0x10 + 0x8 // Res: 0x18 [LT] 0x18 < 0x19 [ADD] 0xa4 + 0x18 // Res: 0xbc [LShift] 0xbb << 0x18 [LShift] 0xfe << 0x18 [AND] 0xbc000000 & 0xff000000 [ADD] 0xbc000000 + 0xceb635 // Res: 0xbcceb635 [ADD] 0x7 + 0x1 // Res: 0x8 [ADD] 0x18 + 0x8 // Res: 0x20 [LT] 0x20 < 0x19
根据 trace 结果写出原本的加解密
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 def encrypt (low, high ): l_bytes = list (low.to_bytes(4 , 'big' )) h_bytes = list (high.to_bytes(4 , 'big' )) l_out = [0 ] * 4 l_out[0 ] = (l_bytes[0 ] ^ 0x36 ) + 0x18 l_out[1 ] = ((l_bytes[1 ] + 0xA8 ) & 0xff ) ^ 0x10 l_out[2 ] = (l_bytes[2 ] ^ 0xD7 ) + 0x1D l_out[3 ] = l_bytes[3 ] - 0x1B res_low = 0 for b in l_out: res_low = (res_low << 8 ) | (b & 0xff ) h_out = [0 ] * 4 h_out[0 ] = (h_bytes[0 ] ^ 0x98 ) + 0x18 h_out[1 ] = h_bytes[1 ] + 0x37 + 0x10 h_out[2 ] = (h_bytes[2 ] ^ 0xBE ) + 0x10 h_out[3 ] = h_bytes[3 ] - 0x2F res_high = 0 for b in h_out: res_high = (res_high << 8 ) | (b & 0xff ) return res_low, res_high in_low = 0x10d817fa in_high = 0x3c871864 out_low, out_high = encrypt(in_low, in_high)print (f"Input Low: {hex (in_low)} " )print (f"Calc Low: {hex (out_low)} " ) print ("-" * 20 )print (f"Input High: {hex (in_high)} " )print (f"Calc High: {hex (out_high)} " )
参考文章 细品sec2023安卓赛题 腾讯游戏安全大赛2023安卓初赛