看到了 twogoat 师傅的文章 强网拟态2025初赛 Mobile方向just Writeup ,然后自己复现了一下,记录下学习过程
题目分析
查看文件结构发现是 unity il2cpp 编译的
il2cpp 编译过程中,原始的 C# 代码被提前编译(AOT)成了原生 c++ 代码,再由各个平台的C++编译器直接编译成机器码,最终打包进了 libil2cpp.so 这个动态链接库文件中,失去了原来的类名、函数名等符号信息;而 global-metadata.dat 是 Unity 的元数据文件,存储了从 C# 脚本中提取的 类型、方法名、类名、字段、属性、字符串常量、参数信息等描述性数据,通常在通常在 apk 的如下路径:assets/bin/Data/Managed/Metadata/global-metadata.dat
利用 Il2CppDumper 把 libil2cpp.so 和 global-metadata.dat 进行联合分析,可以还原出原本的 C# 结构信息
这道题先用 Il2CppDumper 尝试,结果失败了
推测可能是 global-metadata.dat 或者 libil2cpp.so 被加密了,把这道题的这两个文件与另外题目的进行对比,发现两个都加密了(
global-metadata.dat 从 0x400 开始被加密了;il2cpp.so 连 ELF 头都没有
打算使用 frida-il2cpp-bridge dump 出运行时已经解密的 libil2cpp.so
frida 检测绕过 apk 有 frida 检测,直接 hook 会显示失败
端口转发连接 frida
1 2 3 ./fs -l 0.0 .0.0 :11223 adb forward t cp:11223 t cp:11223
先 hook 一下 android_dlopen_ext 看看 so 的加载流程,找到检测 frida 的 so 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook_dlopen ( ) { Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { this .fileName = args[0 ].readCString () console .log (`dlopen onEnter: ${this .fileName} ` ) }, onLeave : function (retval ){ console .log (`dlopen onLeave fileName: ${this .fileName} ` ) } } ); }setImmediate (hook_dlopen)
可以看到在应用崩溃前最后加载的是 libjust.so, frida 的检测应该是在这里面实现的 frida 注入一次之后再次打开 apk 会直接闪退,需要重启手机才能正常打开
在 js 脚本的 onLeave 部分加上如下代码再次 hook 得到的依旧是上面的结果,可以判断检测点在 JNI_OnLoad 之前
1 2 3 4 if (this .fileName != null && this .fileName .indexOf ("libjust.so" ) >= 0 ){ let JNI_OnLoad = Module .getExportByName (this .fileName , 'JNI_OnLoad' ) console .log (`dlopen onLeave JNI_OnLoad: ${JNI_OnLoad} ` ) }
hook call_constructors
由于 call_constructors(主要负责在.so 文件加载后,调用其中 .init_array里定义的函数) 是 Android 动态链接器内部的私有函数,不是一个导出函数,因此在 hook 前要先找到它相对与 linker 的 偏移地址
readelf -sW /apex/com.android.runtime/bin/linker64 | grep call_constructors
找到设备中 call_constructors 函数的 offset 为 0x52838
1 2 renoir:/ 771: 0000000000052838 888 FUNC LOCAL HIDDEN 11 __dl__ZN6soinfo17call_constructorsE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook_linker_call_constructors ( ) { let linker64_base_addr = Module .getBaseAddress ('linker64' ) let offset = 0x52838 let call_constructors = linker64_base_addr.add (offset) let listener = Interceptor .attach (call_constructors,{ onEnter :function (args ){ console .log ('hook_linker_call_constructors onEnter' ) let secmodule = Process .findModuleByName ("libmsaoaidsec.so" ) if (secmodule != null ){ } } }) }
确定了 hook 点之后,继续定位 frida 的具体检测点 frida 检测中常用的 native 函数
openat() / open() 打开 /proc/self/maps 文件,看发现文件中包含 frida、gumjs、agent.so 等与 Frida 相关的字符串
readlinkat() /proc/self/fd 目录下的文件描述符,或读取 /proc/self/exe (进程可执行文件路径)来检测进程环境
strstr() 匹配关键字符串
pthread_create() 监控是否有可疑线程被创建
snprintf() / sprintf() 应用程序在进行 Frida 检测时,需要使用它们来构造 /proc/self/maps 这样的字符串
hook pthread_create() ,应用要检测 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 function hook_pthread_create ( ){ console .log ("libjust.so --- " + Process .findModuleByName ("libjust.so" ).base ) Interceptor .attach (Module .findExportByName ('libc.so' ,'pthread_create' ),{ onEnter (args ){ let func_addr = args[2 ] console .log (`The thread Called function address is: ${func_addr} ` ) } }) }function hook_linker_call_constructors ( ) { let linker64_base_addr = Module .getBaseAddress ('linker64' ) let offset = 0x52838 let call_constructors = linker64_base_addr.add (offset) let listener = Interceptor .attach (call_constructors,{ onEnter :function (args ){ console .log ('hook_linker_call_constructors onEnter' ) let secmodule = Process .findModuleByName ("libjust.so" ) if (secmodule != null ){ console .log ('the size of libjust.so is ' + secmodule.size ) hook_pthread_create () } } }) }setImmediate (hook_linker_call_constructors)
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 hook_linker_call_constructors onEnter the size of libjust.so is 229376 libjust.so hook_linker_call_constructors onEnter the size of libjust.so is 229376 libjust.so hook_linker_call_constructors onEnter the size of libjust.so is 229376 libjust.so hook_linker_call_constructors onEnter the size of libjust.so is 229376 libjust.so hook_linker_call_constructors onEnter the size of libjust.so is 229376 libjust.so hook_linker_call_constructors onEnter the size of libjust.so is 229376 libjust.so The thread Called function address is : 0 x7cf96cf510 The thread Called function address is : 0 x7cf96cf510 The thread Called function address is : 0 x7cf96cf510 The thread Called function address is : 0 x7cf96cf510 The thread Called function address is : 0 x7cf96cf510 The thread Called function address is : 0 x7cf96cf510 The thread Called function address is : 0 x7db41dbeb0 The thread Called function address is : 0 x7db41dbeb0 The thread Called function address is : 0 x7db41dbeb0 The thread Called function address is : 0 x7db41dbeb0 The thread Called function address is : 0 x7db41dbeb0 The thread Called function address is : 0 x7db41dbeb0
可以看出 libjust.so 创建了六个线程,涉及到的函数地址一个是 0x7cf96d6510,相对于基址的偏移为 0xE510;另一个 0x7db41dbeb0 明显不属于 libjust.so 模块
反编译 libjust.so,定位到函数 sub_E510,里面有 frida 检测相关代码
对这个函数交叉引用找到 pthread_create 的调用点,这是在 sub_C1B4 里面
尝试把sub_C1B4 replace 掉
1 2 3 4 5 6 7 8 9 10 11 12 let isHooked = false function hook_c1b4 ( ){ if (isHooked){ console .log ("function already hooked,skip" ); return ; } let mod = Process .findModuleByName ("libjust.so" ) Interceptor .replace (mod.base .add (0xC1B4 ), new NativeCallback (function ( ){ console .log ("replace sub_c1b4" ); },'void' ,[])); isHooked = true ; }
但是现实情况是这样 replace 之后依旧不能绕过 frida 检测
得知 pthread_create() 最终会执行 clone() 系统调用,那就顺便学习一下 hook clone
clone 函数的声明
1 int clone (int (*fn)(void *), void *stack , int flags, void *arg, ...) ;
使用 clone() 系统调用创建线程的简单实例
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 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <sched.h> #include <unistd.h> #include <sys/syscall.h> #include <sys/wait.h> #define STACK_SIZE (1024 * 1024) int thread_function (void *arg) { printf (" 子线程 -> PID: %d, TID: %ld\n" , getpid(), syscall(SYS_gettid)); printf (" 子线程 -> 参数: %s\n" , (char *)arg); sleep(2 ); printf (" 子线程 -> 退出\n" ); return 0 ; }int main () { void *child_stack; int flags; pid_t pid; printf ("主线程 -> PID: %d, TID: %ld\n" , getpid(), syscall(SYS_gettid)); child_stack = malloc (STACK_SIZE); if (child_stack == NULL ) { perror("malloc" ); exit (1 ); } flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD; char *arg = "这是一个传递给线程的参数" ; pid = clone(thread_function, (char *)child_stack + STACK_SIZE, flags, arg); if (pid == -1 ) { perror("clone" ); free (child_stack); exit (1 ); } printf ("主线程 -> 创建了TID为 %d 的子线程\n" , pid); sleep(5 ); printf ("主线程 -> 退出\n" ); free (child_stack); return 0 ; }
hook clone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hook_clone ( ){ Interceptor .attach (Module .findExportByName (null , 'clone' ), { onEnter : function (args ) { let thread_func = args[0 ]; console .log ('Thread function addr: ' + thread_func); let module = Process .findModuleByAddress (thread_func); if (module ) { console .log ('Thread function is located in module: ' + module .name ); } else { console .log ('Fail to find module' ); } console .log ('backtrace:\n' + Thread .backtrace (this .context , Backtracer .ACCURATE ).map (DebugSymbol .fromAddress ).join ('\n' )); }, onLeave : function (retval ) { } }); }setImmediate (hook_clone);
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 PS D :\aaa \强网拟态2025 > frida -H 127.0 .0 .1 :11223 -f com .DefaultCompany .just -l .\hook_clone .js ____ / _ | Frida 16.6 .6 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands : /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https : . . . . . . . . Connected to 127.0 .0.1 :11223 (id=socket@127 .0.0 .1 :11223 ) Spawned `com.DefaultCompany.just` . Resuming main thread! [Remote ::com.DefaultCompany.just ]-> Thread function addr : 0 x74afda84f8 Thread function is located in module : libc.sobacktrace :0 x74afda82bc libc.so!pthread_create+0 x2600 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x74afda84f8 Thread function is located in module : libc.so Thread function addr : 0 x74afda84f8 Thread function is located in module : libc.sobacktrace :0 x74afda82bc libc.so!pthread_create+0 x2600 x74afda82bc libc.so!pthread_create+0 x260backtrace :0 x74afda82bc libc.so!pthread_create+0 x2600 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x74afda84f8 Thread function is located in module : libc.sobacktrace :0 x74afda82bc libc.so!pthread_create+0 x2600 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x0 Fail to find modulebacktrace :0 x74afdac3d4 libc.so!fork+0 x400 x74afdac3d4 libc.so!fork+0 x40 Thread function addr : 0 x74afda84f80 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x0 Fail to find modulebacktrace :0 x74afdac3d4 libc.so!fork+0 x400 x74afdac3d4 libc.so!fork+0 x400 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x0 Fail to find modulebacktrace :0 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x00 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x00 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x00 x74afda82bc libc.so!pthread_create+0 x260 Thread function addr : 0 x0 Fail to find modulebacktrace :0 x74afdac3d4 libc.so!fork+0 x400 x74afdac3d4 libc.so!fork+0 x40 Thread function addr : 0 x74afda84f8 Thread function is located in module : libc.so Thread function addr : 0 x74afda84f8 [Remote ::com.DefaultCompany.just ]->
提取出 libc.so
1 adb pull /apex/ com.android.runtime /lib64/ bionic/libc.so D:\aaa\强网拟态2025
找到 pthread_create 函数,跳转到其 offset 0x260 处,可以看到调用了 clone
pthread_create 的函数声明
1 2 3 4 5 6 #include <pthread.h> int pthread_create (pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg) ;
pthread_create 调用示例,第三个参数就是要让新线程执行的线程函数的地址
1 2 3 4 int result = pthread_create(&thread_id, NULL , my_thread_function, (void *)message);
在 ida 中关注 a3 的传递
v28 + 96 处存的就是线程函数地址,v28 是 clone 的第四个参数 继续 hook clone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hook_clone1 ( ){ const clone = Module .findExportByName (null , 'clone' ); Interceptor .attach (clone, { onEnter : function (args ){ const thread_arg = args[3 ]; if (!thread_arg.isNull ()){ let func_addr = thread_arg.add (96 ).readPointer (); let module = Process .findModuleByAddress (func_addr); if (module ){ let so_name = module .name ; let so_base = module .base ; let offset = func_addr.sub (so_base); console .log (`Thread created by: ${so_name} !${offset} ,RealAddr: ${func_addr} ` ); } } }, onLeave : function (retval ){ } }); }
hook 结果
1 2 3 4 5 6 7 8 Spawned `com.DefaultCompany.just`. Resuming main thread! [Remote::com.DefaultCompany.just ]-> Thread created by: libutils.so! 0 x12eb0,RealAddr: 0 x7794ee0eb0 Thread created by: libart.so! 0 x6600c0,RealAddr: 0 x76e60600c0 Thread created by: libart.so! 0 x42e9a0,RealAddr: 0 x76e5e2e9a0 Thread created by: libgraphicsenv.so! 0 x9474,RealAddr: 0 x7779a57474 Thread created by: libjust.so! 0 xe510,RealAddr: 0 x766fe4e510 Process terminated [Remote::com.DefaultCompany.just ]->
可以看到这里已经可以得到我们想要的地址了 接着 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 function nop_64 (addr ) { Memory .protect (addr, 4 , 'rwx' ); var w = new Arm64Writer (addr); w.putRet (); w.flush (); w.dispose (); }function hook_and_nop_thread (soname ){ const clone = Module .findExportByName (null , 'clone' ); Interceptor .attach (clone, { onEnter : function (args ){ const thread_arg = args[3 ]; if (!thread_arg.isNull ()){ let func_addr = thread_arg.add (96 ).readPointer (); let module = Process .findModuleByAddress (func_addr); if (module ){ let so_name = module .name ; let so_base = module .base ; let offset = func_addr.sub (so_base); if (so_name.indexOf (soname) >= 0 ){ nop_64 (func_addr); } } } }, onLeave : function (retval ){ } }); }setImmediate (() => hook_and_nop_thread ("libjust.so" ));
但是这样 hook 还是会崩溃
推测还有其他检测的地方,了解到 android crc 检测,其中一种的实现原理是把本地文件 /apex/com.android.runtime/lib64/bionic/libc.so 中的可执行数据与 apk 中/proc/{id}/maps下映射的libc.so可执行段内存进行比较(比较算法大多是 crc32,因为比较简单,计算耗能小,也可以选择其他如 md5、aes等,但很少见),从而判断对应的 so 是否被 inline hook 修改过(inline hook 会修改函数入口处的指令,让其跳转到自定义的 hook 函数)
在 libjust.so 中找到 crc 校验相关的代码,在函数 sub_119F8 中
绕过这个函数
最终绕过检测的 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 function nop_64 (addr ) { Memory .protect (addr, 4 , 'rwx' ); var w = new Arm64Writer (addr); w.putRet (); w.flush (); w.dispose (); }let ispatched = false ;function patch_crcCheck ( ){ if (ispatched) { return ; } const base = Module .findBaseAddress ("libjust.so" ); let crc_addr = null ; if (base){ crc_addr = base.add (0x119F8 ); } nop_64 (crc_addr); ispatched = true ; } function hook_and_nop_thread (soname ){ const clone = Module .findExportByName (null , 'clone' ); Interceptor .attach (clone, { onEnter : function (args ){ const thread_arg = args[3 ]; if (!thread_arg.isNull ()){ let func_addr = thread_arg.add (96 ).readPointer (); let module = Process .findModuleByAddress (func_addr); if (module ){ let so_name = module .name ; let so_base = module .base ; let offset = func_addr.sub (so_base); if (so_name.indexOf (soname) >= 0 ){ nop_64 (func_addr); patch_crcCheck (); } } } }, onLeave : function (retval ){ } }); }setImmediate (() => hook_and_nop_thread ("libjust.so" ));
到这里 frida 检测算是成功绕过了,但是 frida-il2cpp-bridge 依旧使用失败 我只能说通过这道题学到了不少绕过 frida 检测的的技巧吧
真正开始 对于这道题目本身而言,后续不用 frida dump 数据的话,上面做了那么多都没啥用处,下面才是真正开始解题
解密 libil2cpp.so 在 libjust.so 中搜索字符串 il2cpp 看到有一个 dec_il2cpp,进入相关函数,在 “dec_il2cpp” 处网上看找到加密方式是 RC4 + 异或 0x33
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 LABEL_107: for ( j = 0LL ; j != 256 ; ++j ) { v79 = *((unsigned __int8 *)v137 + j); v76 += v79 + (unsigned __int8)aNihaounity[j % v59]; *((_BYTE *)v137 + j) = *((_BYTE *)v137 + (unsigned __int8)v76); *((_BYTE *)v137 + (unsigned __int8)v76) = v79; } ++v77; continue ; } break ; } if ( v64 ) { v80 = 0 ; v81 = 0 ; for ( k = 0 ; ; ++k ) { LOBYTE(v73) = byte_37010; LOBYTE(v74) = byte_3700C; v74 = (double )*(unsigned __int64 *)&v74 + (double )*(unsigned __int64 *)&v74; v73 = ((double )*(unsigned __int64 *)&v73 - v74) * (double )(((unsigned int )(unsigned __int8)byte_37000 + 7 ) / (unsigned __int8)byte_37004 - (unsigned __int8)byte_37008); v136 = v73; if ( ((unsigned int )(unsigned __int8)byte_37000 + 7 ) / (unsigned __int8)byte_37004 == (unsigned __int8)byte_37008 || (unsigned __int8)byte_3700C + 6 * (unsigned __int8)byte_37060 == 3 ) { v74 = (double )(((unsigned int )(unsigned __int8)byte_37000 + 7 ) / (unsigned __int8)byte_37004 - (unsigned __int8)byte_37008); v73 = v136 * v74; if ( k >= (int )(v136 * v74) ) goto LABEL_125; } else if ( k >= 7 ) { goto LABEL_125; } v83 = (_BYTE *)v57; v84 = v58 - v57; do { v85 = *((unsigned __int8 *)v137 + (unsigned __int8)++v81); --v84; v80 += v85; *((_BYTE *)v137 + (unsigned __int8)v81) = *((_BYTE *)v137 + (unsigned __int8)v80); *((_BYTE *)v137 + (unsigned __int8)v80) = v85; *v83++ ^= *((_BYTE *)v137 + (unsigned __int8)(*((_BYTE *)v137 + (unsigned __int8)v81) + v85)) ^ 0x33 ; } while ( v84 ); } } v86 = -1 ; do { while ( 1 ) { LOBYTE(v73) = byte_37010; LOBYTE(v74) = byte_3700C; v74 = (double )*(unsigned __int64 *)&v74 + (double )*(unsigned __int64 *)&v74; v73 = ((double )*(unsigned __int64 *)&v73 - v74) * (double )(((unsigned int )(unsigned __int8)byte_37000 + 7 ) / (unsigned __int8)byte_37004 - (unsigned __int8)byte_37008); v136 = v73; if ( ((unsigned int )(unsigned __int8)byte_37000 + 7 ) / (unsigned __int8)byte_37004 != (unsigned __int8)byte_37008 && (unsigned __int8)byte_3700C + 6 * (unsigned __int8)byte_37060 != 3 ) { break ; } v74 = (double )(((unsigned int )(unsigned __int8)byte_37000 + 7 ) / (unsigned __int8)byte_37004 - (unsigned __int8)byte_37008); v73 = v136 * v74; if ( ++v86 >= (int )(v136 * v74) ) goto LABEL_125; } ++v86; } while ( v86 < 7 ); LABEL_125: v87 = syscall(279LL , "dec_il2cpp" , 0LL );
在 ciberchef 中解密得到解密后的 libil2cpp.so
global-metadata.dat 在 libil2cpp.so 中加密 同样是字符串搜索定位到 global-metadata.dat 相关的函数
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 __int64 __fastcall sub_211B8C (_DWORD *a1, int *a2) { int v4; int v5; int v6; int v7; int v8; int v9; int v10; int *v11; __int64 result; int v13; int v14; int v15; int v16; int v17; int v18; int v19; unsigned __int64 v20; __int64 v21; __int64 v22; __int64 v23; int *v24; unsigned int v25; int v26; __int64 v27; __int64 v28; __int64 v29; int v30[4 ]; char v31[8 ]; __int64 v32; void *v33; __int64 v34; int vars0[2 ]; void *vars8; if ( (sub_211AF8() & 1 ) != 0 ) { v11 = v30; strcpy ((char *)v30, "game.dat" ); } else { v11 = (int *)"global-metadata.dat" ; } result = sub_211D94((int )v11, v4, v5, v6, v7, v8, v9, v10, v30[0 ], v30[2 ], v31[0 ], v32, v33, v34, vars0[0 ], vars8); qword_A322E8 = result; if ( result || (result = sub_211D94( (int )"global-metadata.dat" , v13, v14, v15, v16, v17, v18, v19, v30[0 ], v30[2 ], v31[0 ], v32, v33, v34, vars0[0 ], vars8), qword_A322E8 = result, off_A322D0 = 0LL , result) ) { qword_A322F0 = result; v20 = *(int *)(result + 172 ); *a1 = v20 / 0x28 ; dword_A322F8 = v20 / 0x28 ; *a2 = *(int *)(result + 180 ) >> 6 ; qword_A32300 = sub_267DFC((int )(v20 / 0x28 ), 24LL ); qword_A32308 = sub_267DFC(*(int *)(qword_A322E0 + 48 ), 8LL ); qword_A32310 = sub_267DFC(*(int *)(qword_A322F0 + 164 ) / 0x58u LL, 8LL ); qword_A32318 = sub_267DFC((unsigned __int64)*(int *)(qword_A322F0 + 52 ) >> 5 , 8LL ); v21 = sub_267DFC(*(int *)(qword_A322E0 + 64 ), 8LL ); v22 = qword_A322E0; qword_A32320 = v21; result = 1LL ; if ( *(int *)(qword_A322E0 + 48 ) >= 1 ) { v23 = 0LL ; while ( 1 ) { v24 = *(int **)(*(_QWORD *)(v22 + 56 ) + 8 * v23); v25 = *((unsigned __int8 *)v24 + 10 ); if ( v25 <= 0x1E ) { v26 = 1 << v25; if ( (v26 & 0x13467FFE ) != 0 ) { v27 = *v24; if ( (_DWORD)v27 != -1 ) { v28 = qword_A322E8 + *(int *)(qword_A322F0 + 160 ) + 88 * v27; LABEL_16: *(_QWORD *)v24 = v28; goto LABEL_17; } goto LABEL_15; } if ( (v26 & 0x40080000 ) != 0 ) { v29 = *v24; if ( (_DWORD)v29 != -1 ) { v28 = qword_A322E8 + *(int *)(qword_A322F0 + 104 ) + 16 * v29; goto LABEL_16; } LABEL_15: v28 = 0LL ; goto LABEL_16; } } LABEL_17: v22 = qword_A322E0; if ( ++v23 >= *(int *)(qword_A322E0 + 48 ) ) return 1LL ; } } } return result; }
result 应该是解密后的 global-metadata.dat,关注函数 sub_211D94
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 __int64 __fastcall sub_211D94 ( const char *a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, char a11, int a12, void *a13, char a14, int a15, void *a16) { void *v17; char *v18; size_t v19; char *v20; void *v21; __int64 v22; __int64 v23; __int64 v24; __int64 v25; __int64 v26; char *v28; __int64 v29; __int64 v30; void *v31; void *ptr; __int64 v33; void *v34; void *v35; char *v36; void *v37; sub_26850C( &v30, (int )a1, a2, a3, a4, a5, a6, a7, a8, (int )v28, v29, v30, (int )v31, (char )ptr, v33, v34, (char )v35, (int )v36, v37); v28 = "Metadata" ; v29 = 8LL ; v17 = (void *)((unsigned __int64)(unsigned __int8)v30 >> 1 ); if ( (v30 & 1 ) != 0 ) v18 = (char *)ptr; else v18 = (char *)&v30 + 1 ; if ( (v30 & 1 ) != 0 ) v17 = v31; v36 = v18; v37 = v17; sub_1F3DA0(&v33, &v36, &v28); if ( (v30 & 1 ) != 0 ) operator delete (ptr) ; v19 = strlen (a1); if ( (v33 & 1 ) != 0 ) v20 = (char *)v35; else v20 = (char *)&v33 + 1 ; if ( (v33 & 1 ) != 0 ) v21 = v34; else v21 = (void *)((unsigned __int64)(unsigned __int8)v33 >> 1 ); v28 = (char *)a1; v29 = v19; v36 = v20; v37 = v21; sub_1F3DA0(&v30, &v36, &v28); LODWORD(v36) = 0 ; v22 = sub_1EDFAC(&v30, 3LL , 1LL , 1LL , 0LL , &v36); if ( (_DWORD)v36 ) { sub_267C8C("ERROR: Could not open %s" ); } else { v23 = v22; v24 = sub_267E30(); qword_A327F0 = v24; v25 = sub_1F08B0(v23, &v28); v26 = sub_21A2C8(v24, v25); qword_A327F8 = v26; sub_1EE398(v23, &v36); if ( !(_DWORD)v36 ) goto LABEL_19; sub_267E40(v24); } v26 = 0LL ; LABEL_19: if ( (v30 & 1 ) != 0 ) operator delete (ptr) ; if ( (v33 & 1 ) != 0 ) operator delete (v35) ; return v26; }
最后返回的是 v26,对 v26 进行操作的函数是 sub_21A2C8
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 char *__fastcall sub_21A2C8 (unsigned __int16 *a1, __int64 a2) { __int64 v2; __int64 v4; __int64 v5; char *v6; __int64 i; __int64 v8; __int64 v9; v2 = a1[512 ]; v4 = a2 - 4 * v2; v5 = v4 - 1028 ; v6 = (char *)malloc (v4 - 4 ); memcpy (v6, a1, 0x400u LL); if ( v5 >= 1 ) { for ( i = 0LL ; i < v5; i += 4LL ) { v8 = i + 3 ; v9 = i + i / v2; if ( i >= 0 ) v8 = i; *(_DWORD *)&v6[(v8 & 0xFFFFFFFFFFFFFFFCL L) + 1024 ] = *(_DWORD *)((char *)&a1[2 * v2 + 514 ] + (v8 & 0xFFFFFFFFFFFFFFFCL L)) ^ *(_DWORD *)&a1[2 * (v9 % v2) + 514 ]; } } return v6; }
写个解密脚本
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 #include <stdio.h> #include <stdlib.h> #include <string.h> char *decrypt_metadata (unsigned short *data, long long file_size) { long long v2 = data[512 ]; long long v4 = file_size - 4 * v2; long long v5 = v4 - 1028 ; char *decrypted = (char *)malloc (v4 - 4 ); memcpy (decrypted, data, 0x400 ); if (v5 >= 1 ) { for (long long i = 0 ; i < v5; i += 4 ) { long long v8 = i + 3 ; long long v9 = i + i / v2; if (i >= 0 ) v8 = i; *(unsigned int *)&decrypted[(v8 & 0xFFFFFFFFFFFFFFFCL L) + 1024 ] = *(unsigned int *)((char *)&data[2 * v2 + 514 ] + (v8 & 0xFFFFFFFFFFFFFFFCL L)) ^ *(unsigned int *)&data[2 * (v9 % v2) + 514 ]; } } return decrypted; }int main () { FILE *file = fopen("global-metadata.dat" , "rb" ); if (!file) { printf ("无法打开 global-metadata.dat 文件\n" ); return 1 ; } fseek(file, 0 , SEEK_END); long long file_size = ftell(file); fseek(file, 0 , SEEK_SET); unsigned short *data = (unsigned short *)malloc (file_size); fread(data, 1 , file_size, file); fclose(file); char *decrypted = decrypt_metadata(data, file_size); FILE *output = fopen("dec_global-metadata.dat" , "wb" ); if (output) { long long v2 = data[512 ]; long long decrypted_size = file_size - 4 * v2 - 4 ; fwrite(decrypted, 1 , decrypted_size, output); fclose(output); printf ("解密完成,输出文件:dec_global-metadata.dat\n" ); } else { printf ("无法创建输出文件\n" ); } free (data); free (decrypted); return 0 ; }
il2cppdumper 现在我们已经拿到了解密后的 libil2cpp.so 和 global-metadata.dat,接下来就可以用 il2cppdumper 了
dump 成功,yeah
ida 打开 dec_libil2cpp.so, File->Script file 选择 Il2CppDumper 里面的 ida_with_struct_py3.py,然后再选择刚刚 dump 得到的文件里面的 script.json 和 il2cpp.h 就可以恢复符号表了
关注 FlagChecker 类
找到 FlagChecker__TeaEncrypt
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 FlagChecker__TeaEncrypt (System_UInt32_array *v, System_UInt32_array *k, const MethodInfo *method) { if ( !v ) LABEL_17: sub_2745C8(); v9 = sub_1B5D88((__int64)v, 0LL , v3, v4, v5, v6); v36 = v; v14 = sub_1B5D88((__int64)v, 1LL , v10, v11, v12, v13); if ( ((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 != (unsigned __int8)byte_9D8048 ) { v18 = 0 ; v19 = 0 ; do { LOBYTE(v15) = byte_9D804C; if ( v19 >= 16 ) v20 = 0.0 ; else v20 = 1.0 ; LOBYTE(v16) = byte_9D8050; *(double *)&v16 = (double )*(unsigned __int64 *)&v16 + (double )*(unsigned __int64 *)&v16; *(double *)&v15 = ((double )*(unsigned __int64 *)&v15 - *(double *)&v16) * v20; v37 = *(double *)&v15; if ( ((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 == (unsigned __int8)byte_9D8048 || (unsigned __int8)byte_9D8050 + 30 * (unsigned __int8)byte_A2F928 == 3 ) { if ( ((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 == (unsigned __int8)byte_9D8048 ) { v21 = 0 ; } else { *(double *)&v16 = (double )(((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 - (unsigned __int8)byte_9D8048); *(double *)&v15 = *(double *)&v15 * *(double *)&v16; v21 = (int )(v37 * *(double *)&v16); } } else { v21 = 9 ; } if ( !((((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 - (unsigned __int8)byte_9D8048) * v21) ) break ; if ( !k ) goto LABEL_17; v22 = sub_1B5D88((__int64)k, 0LL , v15, v16, v20, v17); v9 += (v22 + 16 * v14) ^ (v14 + v18) ^ (sub_1B5D88((__int64)k, 1LL , v23, v24, v25, v26) + (v14 >> 5 )); v31 = sub_1B5D88((__int64)k, 2LL , v27, v28, v29, v30); v14 += (v31 + 16 * v9) ^ (v18 + v9) ^ (sub_1B5D88((__int64)k, 3LL , v32, v33, v34, v35) + (v9 >> 5 )); v18 -= 0x61C88647 ; ++v19; } while ( ((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 != (unsigned __int8)byte_9D8048 ); } sub_1B5B88(v36, 0LL , v9); sub_1B5B88(v36, 1LL , v14); }
魔改 tea
获取数据 在 FlagChecker___cctor 中可以看到初始化 Key 和 ReallyCompare
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 void FlagChecker___cctor (const MethodInfo *method) { unsigned __int64 v1; unsigned __int64 v2; int v3; System_Array_o *v4; System_RuntimeFieldHandle_o v5; System_Array_o *v6; struct FlagChecker_StaticFields *static_fields ; System_Array_o *v8; System_RuntimeFieldHandle_o v9; System_Array_o *v10; struct FlagChecker_StaticFields *v11 ; if ( ((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 != (unsigned __int8)byte_9D8048 ) { LOBYTE(v1) = byte_9D804C; LOBYTE(v2) = byte_9D8050; if ( (unsigned __int8)byte_9D8050 + 30 * (unsigned __int8)byte_A2F928 == 3 ) { if ( ((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 == (unsigned __int8)byte_9D8048 ) v3 = 0 ; else v3 = (int )(((double )v1 - ((double )v2 + (double )v2)) * (double )((byte_A2F8C2 & 1 ) == 0 ) * (double )(((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 - (unsigned __int8)byte_9D8048)); } else { v3 = 9 ; } if ( (((unsigned int )(unsigned __int8)byte_9D8040 + 7 ) / (unsigned __int8)byte_9D8044 - (unsigned __int8)byte_9D8048) * v3 ) { sub_2744F4((__int64)&byte___TypeInfo); sub_2744F4((__int64)&FlagChecker_TypeInfo); sub_2744F4((__int64)&Field__PrivateImplementationDetails__29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2); sub_2744F4((__int64)&Field__PrivateImplementationDetails__C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF); sub_2744F4((__int64)&uint___TypeInfo); byte_A2F8C2 = 1 ; } } v4 = (System_Array_o *)sub_274508((__int64)uint___TypeInfo, 4u ); v5.fields.value = Field__PrivateImplementationDetails__C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF; v6 = v4; System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_4166724(v4, v5, 0LL ); static_fields = FlagChecker_TypeInfo->static_fields; static_fields->Key = (struct System_UInt32_array *)v6; sub_274498(static_fields, v6); v8 = (System_Array_o *)sub_274508((__int64)byte___TypeInfo, 0x28u ); v9.fields.value = Field__PrivateImplementationDetails__29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2; v10 = v8; System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_4166724(v8, v9, 0LL ); v11 = FlagChecker_TypeInfo->static_fields; v11->ReallyCompare = (struct System_Byte_array *)v10; sub_274498(&v11->ReallyCompare, v10); }
在 C# 代码中定义一个静态数组并使用数组初始化器给它赋初值时,例如
1 private static readonly byte [] MyData = new byte [] { 0xAF , 0x58 , ..., 0x1A };
编译器不会将这些字节值作为 C# 源代码的一部分存储在 IL 中,相反,它会将这些原始字节数据放在一个特殊的元数据(Metadata)部分,并生成一个指向这块数据的静态字段,这个静态字段就存在于 类中
29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2 和 C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF 分别对应 40 字节(密文)和 16 字节(密钥)数据的哈希值
在 dump.cs 中可以找到 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private struct <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 {}private struct <PrivateImplementationDetails>.__StaticArrayInitTypeSize=40 {}internal sealed class <PrivateImplementationDetails > { internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=40 29F C2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2 ; internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF ; }
根据 offset 在 global-metadata.dat 中提取出对应的数据
解密脚本 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 cipher = [0xAF ,0x58 ,0x64 ,0x40 ,0x9D ,0xB9 ,0x21 ,0x67 ,0xAE ,0xB5 ,0x29 ,0x04 ,0x9E ,0x86 ,0xC5 ,0x43 ,0x23 ,0x0F ,0xBF ,0xA6 ,0xB2 ,0xAE ,0x4A ,0xB5 ,0xC5 ,0x69 ,0xB7 ,0xA8 ,0x03 ,0xD1 ,0xAE ,0xCF ,0xC6 ,0x2C ,0x5B ,0x7F ,0xA2 ,0x86 ,0x1E ,0x1A ] key = [0x12345678 , 0x09101112 , 0x13141516 , 0x15161718 ]def tea_encrypt (v, k ): delta = 0x61C88647 sum = 0 k0, k1, k2, k3 = k[0 ], k[1 ], k[2 ], k[3 ] v0, v1 = v[0 ], v[1 ] for _ in range (16 ): v0 = (v0 + (((v1 << 4 ) + k0) ^ (v1 + sum ) ^ ((v1 >> 5 ) + k1))) & 0xFFFFFFFF v1 = (v1 + (((v0 << 4 ) + k2) ^ (v0 + sum ) ^ ((v0 >> 5 ) + k3))) & 0xFFFFFFFF sum = (sum - delta) & 0xFFFFFFFF return [v0, v1]def tea_decrypt (v, k ): delta = 0x61C88647 sum = ((-16 ) * delta) & 0xFFFFFFFF k0, k1, k2, k3 = k[0 ], k[1 ], k[2 ], k[3 ] v0, v1 = v[0 ], v[1 ] for _ in range (16 ): sum = (sum + delta) & 0xFFFFFFFF v1 = (v1 - (((v0 << 4 ) + k2) ^ (v0 + sum ) ^ ((v0 >> 5 ) + k3))) & 0xFFFFFFFF v0 = (v0 - (((v1 << 4 ) + k0) ^ (v1 + sum ) ^ ((v1 >> 5 ) + k1))) & 0xFFFFFFFF return [v0, v1]def b2dle (byte_array ): return [int .from_bytes(byte_array[i:i+4 ], byteorder='little' , signed=False ) for i in range (0 , len (byte_array), 4 )]def d2ble (dword_array ): byte_list = [] for dword in dword_array: byte_list.extend(dword.to_bytes(4 , byteorder='little' )) return byte_list cipher_copy = cipher.copy() v = b2dle(cipher_copy)for i in range (32 , 7 , -8 ): for j in range (8 ): cipher_copy[j + i] ^= cipher_copy[j] v = tea_decrypt(v, key) dec_bytes = d2ble(v) for i in range (8 ): cipher_copy[i] = dec_bytes[i] v = tea_decrypt(v, key) dec_bytes = d2ble(v)for i in range (8 ): cipher_copy[i] = dec_bytes[i]print ('' .join(chr (b) for b in cipher_copy))
flag 为 flag{unitygame_I5S0ooFunny_Isnotit?????}