最终效果 成功实现RocketMouse直装破解版
app分析 文件结构 首先查看apk文件结构,发现是mono
引擎的unity3d游戏,且Assembly-CSharp.dll
已加密。 并且存在assets/filelist
文件,测试发现是apk中各个文件的crc32校验值。
java层 使用frida的spawn模式启动app,弹出对话框,点击对话框会退出进程,于是使用jeb打开该apk文件。 发现com.tencent.games.sec2021.Sec2021MsgBox
的onDismiss
函数存在退出代码。
1 2 3 4 5 6 7 public void onDismiss (DialogInterface arg4) { if (!this .m_str.contains("。" )) { System.exit(0 ); } Sec2021MsgBox.m_showed = false ; }
可使用frida hook该退出函数:
1 2 3 4 5 6 7 8 9 if (Java.available) { Java.perform(function ( ) { Java.use("java.lang.System" ).exit .overload('int' ) .implementation = function (code ) { return ; }; }); }
查看show
函数的交叉引用,最终跟踪至Sec2021IPC
的onNativeEngineResponse
函数,推测native层会通过JNI来调用该函数,从而弹出对话框。
hook native层退出函数 使用frida或ida附加后,除了java层弹出对话框,native层也会调用函数使程序退出。所以想要好好调试的话,得先找到这些函数。 使用IDA打开libsec2021.so
,查看kill
函数的交叉引用,发现有3个函数会调用它,如下图: 分别查看对应函数的交叉引用,发现55EC
没有被调用,1F120
被多次调用,而1F788
仅被一个函数(1F6F0
)调用,向上寻找调用链找到1FAA8
,该函数最终只被调用了5次,将其命名为my_kill
。 可以看到在kill之前调用了sleep函数,根据经验,1F6F0
是退出函数的可能性更大一些。 使用frida hookkill
函数,并打印堆栈查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function print_c_stack (context, str_tag ) { console .log("=============================" + str_tag + " Stack strat=======================" ); console .log(Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n' )); console .log("=============================" + str_tag + " Stack end =======================" ); } function hook_kill ( ) { var addr = Module.findExportByName("libc.so" , "kill" ); Interceptor.replace(addr, new NativeCallback(function (pid, sig ) { console .log("fake_kill() called!" ); print_c_stack(this .context); return -1 ; }, 'int' , ['int' , 'int' ])); }
结果为:
1 2 3 4 =============================undefined Stack strat======================= 0xb3624a38 libsec2021.so!0x1fa38 0xb3624a38 libsec2021.so!0x1fa38 =============================undefined Stack end =======================
IDA定位到该偏移值,发现位于1F788
函数中,且程序崩溃,使用Android Studio
查看日志:
1 2 3 4 5 6 backtrace: #00 pc 00013ec8 /data/app/com.personal.rocketmouse-1/lib/arm/libsec2021.so #01 pc 0001fa50 /data/app/com.personal.rocketmouse-1/lib/arm/libsec2021.so #02 pc 00020920 /data/app/com.personal.rocketmouse-1/lib/arm/libsec2021.so #03 pc 000060e0 /data/app/com.personal.rocketmouse-1/lib/arm/libsec2021.so #04 pc 000224c7 /system/lib/libc.so (offset 0x1d000)
相关代码:
查看代码发现13EC4
是一个内存拷贝函数,而它的目标地址是0x0,所以导致程序崩溃。由此可以确定1F788
是退出函数,同时我们发现1F120
被调用作为参数传入13EC4
。使用frida hook看看13EC4
的第二个参数是什么,具体思路为: hook dlopen,然后在libsec2021.so
加载后立即hook该函数,发现程序卡住,猜测有crc32校验,于是在libmono
加载后再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 function hook_dlopen ( ) { var dlopenAddr = Module.findExportByName(null , "dlopen" ); if (dlopenAddr == null ) { dlopenAddr = Module.findExportByName(null , "android_dlopen_ext" ); } Interceptor.attach(dlopenAddr, { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr !== undefined && pathptr != null ) { var path = ptr(pathptr).readCString(); this .can_hook_lib = false ; this .can_hook_libmono = false ; if (path.indexOf("libsec2021.so" ) >= 0 ) { this .can_hook_lib = true ; } if (path.indexOf("libmono.so" ) >= 0 ) { this .can_hook_libmono = true ; } } }, onLeave : function (retval ) { if (this .can_hook_lib) { } if (this .can_hook_libmono) { hook_sec2021(); } } }) } setImmediate(hook_dlopen); function hook_sec2021 ( ) { var libbase = Module.findBaseAddress("libsec2021.so" ); var addr = libbase.add(0x13EC4 ); var memcpy_ori = new NativeFunction(addr, 'pointer' , ['int' , 'pointer' ]); Interceptor.replace(addr, new NativeCallback(function (dst, src ) { if (dst == 0 ) { var zero_replace_ptr = Memory.alloc(10 ); dst = zero_replace_ptr; console .log(Memory.readByteArray(src, 0x10 )); return dst; } return memcpy_ori(dst, src); }, 'pointer' , ['int' , 'pointer' ])); }
输出diediedie
,由此可以得知1F120
是一个获取字符串的函数,将其命名为getstring
,hook看看字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var string_maps = {};var key = 0 ;function hook_getstring ( ) { var libso_address = Module.findBaseAddress("libsec2021.so" ) console .log(libso_address); var addr = libso_address.add(0x1F120 ); Interceptor.attach(addr, { onEnter : function (args ) { key = args[0 ]; }, onLeave : function (retval ) { var str = Memory.readUtf8String(ptr(retval)); if (string_maps[key] == null ) { string_maps[key] = str; console .log("getstring: " , key, str); } } }); }
输出一系列字符串,比如tcp端口检测,过签名校验类的检测,动态链接库的检测,apk签名、资源文件等。配合堆栈打印,可快速定位相关函数。当然,不需要一个一个分析,笔者是将退出函数nop掉从而实现破解。
对话框 如0xa44 no heart beat
,打印堆栈可定位到1574C
函数: 查看14FE0
,分析得知是在调用onNativeEngineResponse
函数显示对话框:
dll加载 日志0x338 Assembly-CSharp.dll
,可定位到0x1D29C
函数(具体分析见下文)
native层 使用IDA打开libmono.so
,发现关键函数mono_image_open_with_name
被加密。程序运行起来后,使用frida对其进行内存dump,发现该函数已被解密。修复so文件结构后,使用IDA查看: 与正常的libmono.dll
函数对比,发现区别在于第一条B指令,于是使用IDA下断点进行动态调试,执行到了libsec2021.so
的0x1D29C
函数。 其中对文件头和文件名进行了判断,如果是MZ
开头且路径不包含Assembly-CSharp.dll
,则跳转至0x1CF88
执行。否则初始化指针,并调用0x1CF4C
中的解密算法,将sec2021.png
的0x410B
至末尾的数据解密,解密结果即为真正加载的Assembly-CSharp.dll
。解密函数为0x1D2A0
(查看调用发现也是解密so的函数)
0x1CF88
位置: 这里的1FAA8
是之前分析的my_kill
函数,2048即状态码,用来区分检测点。18B00
函数初始化了一个数组,其中存有dll文件的crc32校验码,18CEC
函数获取dll索引,并对其进行crc32校验(F3B4
函数),正常则返回0。(18CEC
函数部分内容如下,修改该函数返回值即可通过该检测点) 查看crc32校验函数(F3B4
)的交叉引用,可定位到159A0
函数,修改其返回值为0即可绕过相关检测点。
由于 mono_image_open_from_data_with_name
函数的第一条指令会完成解密操作,所以可以hook下一条指令(此时已完成解密),当读取到真正的Assembly-CSharp.dll
时(通过大小判断),将其dump出来。
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 dump_memory (base,size ) { Java.perform(function ( ) { var currentApplication = Java.use("android.app.ActivityThread" ).currentApplication(); var dir = currentApplication.getApplicationContext().getFilesDir().getPath(); var file_path = dir + "/dumpmemory.bin" ; var file_handle = new File(file_path, "wb" ); if (file_handle && file_handle != null ) { Memory.protect(ptr(base),size, 'rwx' ); var libso_buffer = ptr(base).readByteArray(size); file_handle.write(libso_buffer); file_handle.flush(); file_handle.close(); console .log("[dump]:" , file_path); } }); } function hook_mono ( ) { var libbase = Module.findBaseAddress("libmono.so" ); console .log("libbase" , libbase); var addr = Module.findExportByName("libmono.so" , "mono_image_open_from_data_with_name" ); console .log("mono_image_open_from_data_with_name" , addr); Interceptor.attach(Module.findExportByName("libmono.so" , "mono_image_open_from_data_with_name" ).add(4 ), { onEnter : function (args ) { var data = args[0 ]; var data_len = args[1 ]; if (data_len == 0x2800 ) { dump_memory(data, data_len); } console .log("mono_image_open_from_data_with_name_ori() called!" , data, data_len); }, onLeave : function (retval ) { } }); }
分析 dll 使用DnSpy打开该dll,注意到MouseController
类的碰撞检测函数OnTriggerEnter2D
中对碰撞物体进行了判断,如果不是金币则调用HitByLaser
函数 该函数将dead属性以及对话框赋值为true,所以只需要将赋值改为false即可实现无敌
即将il语句ldc.i4.1
改为ldc.i4.0
(对应16进制的17
改为16
),如下图:
静态patch即可得到一个破解版的Assembly-CSharp.dll
修改方法 最终采用的是方法1(不需要额外文件)
法1 - 直装破解 替换Assembly-CSharp.dll
文件为破解版,patch掉libsec2021.so
的检测,使mono_image_open_from_data_with_name
函数走正常流程,不对Assembly-CSharp.dll
进行替换。
修改方法: 修改libsec2021.so
,将1D0BC
偏移处BEQ loc_1CF88
指令替换为B loc_1CF88
,使得非MZ
开头的dll才去执行解密函数。而因为我们替换了dll为破解版,所以是正常的文件头,从而不会执行解密函数。
修改crc32检测函数返回值: 查找my_kill
函数(1FAA8
)的交叉引用,并将其nop (测试发现nop以下两处即可)
法2 - frida-gadget 准备工作 使用lief
为libmono.so
添加frida-gadget
依赖
1 2 3 4 import liefbin = lief.parse("libmono.so" )bin .add_library("libgadget.so" )bin .write("libmono-patch.so" )
配置文件
1 2 3 4 5 6 7 { "interaction" : { "type" : "script" , "path" : "/data/data/com.personal.rocketmouse/files/script.js" , "on_change" : "reload" } }
释放脚本文件到 /data/data/com.personal.rocketmouse/files/script.js
。(无root环境需要自己编写一个so文件,并将其编辑为libgadget.so
的依赖,释放脚本文件)
hook mono_image_open_from_data_with_name
函数的下一条指令(此时已完成解密),当读取到真正的Assembly-CSharp.dll
时(通过数据大小判断),修改MouseController
的HitByLaser
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hook_mono ( ) { var libbase = Module.findBaseAddress("libmono.so" ); console .log("libbase" , libbase); var addr = Module.findExportByName("libmono.so" , "mono_image_open_from_data_with_name" ); console .log("mono_image_open_from_data_with_name" , addr); Interceptor.attach(Module.findExportByName("libmono.so" , "mono_image_open_from_data_with_name" ).add(4 ), { onEnter : function (args ) { var data = args[0 ]; var data_len = args[1 ]; if (data_len == 0x2800 ) { var arr = [0x16 ]; Memory.writeByteArray(data.add(0x9e4 ), arr); Memory.writeByteArray(data.add(0x9f5 ), arr); Memory.writeByteArray(data.add(0xa01 ), arr); } console .log("mono_image_open_from_data_with_name_ori() called!" , data, data_len); }, onLeave : function (retval ) { } }); }
去检测(libsec2021.so
加载后立即hook):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook_sec2021 ( ) { var libbase = Module.findBaseAddress("libsec2021.so" ); var addr = libbase.add(0x1F6F0 ); Interceptor.replace(addr, new NativeCallback(function ( ) { console .log("my_kill called" ); }, 'void' , [])); addr = libbase.add(0x159A0 ); Interceptor.replace(addr, new NativeCallback(function (src ) { return 0 ; }, 'int' , ['pointer' ])); addr = libbase.add(0x14FE0 ); Interceptor.replace(addr, new NativeCallback(function (jni, mode, cstr ) { console .log("show msg:" , Memory.readCString(cstr)); return 0 ; }, 'void' , ['pointer' , 'int' , 'pointer' ])); }
法3 - dll注入 笔者还尝试了通过native hook的方式,使用mono api来进行dll注入。步骤如下: 将UnityEngine.dll
和Assembly-CSharp.dll
作为引用,编写一个注入dll,从而拦截HitByLaser
方法(使用MonoHook ,其原理为替换函数地址)
在native层hook dlopen
函数,过掉libsec2021.so
的检测,并获取libmono
句柄,然后导出mono的api,在Assembly-CSharp.dll
加载后,调用api加载注入dll(使用VitualApp
及 Cydia Substrate
框架)
关键代码(native层) 1 2 3 4 5 6 void *image = orig_mono_image_open_with_name(r.c_str(), file_size(path), need_copy, status, refonly, name);void *assembly = mono_assembly_load_from_full(image, "UNUSED" , status, refonly);image = mono_assembly_get_image(assembly); void *pClass = mono_class_from_name(image, "UnityHack" , "HackLoad" );hookload_method = mono_class_get_method_from_name(pClass, "HookLoad" , 0 ); mono_runtime_invoke(hookload_method, NULL , NULL , NULL );
根据路径打开注入dll文件,并将其加载,之后根据类名和函数名获取相应函数,进行调用。
关键代码(C#层) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void HookLoad ( ) { Type tTarget = typeof (MouseController); Type tProxy = MethodBase.GetCurrentMethod().DeclaringType; string methodName = "HitByLaser" ; MethodInfo miTarget = tTarget.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); MethodInfo miReplace = tProxy.GetMethod(methodName + "Replace" ); MethodInfo miProxy = tProxy.GetMethod(methodName + "Proxy" ); new MethodHook(miTarget, miReplace, miProxy).Install(); } public void HitByLaserReplace (Collider2D laserCollider ){ UnityEngine.Debug.Log("HitByLaserReplace" ); } public void HitByLaserProxy (Collider2D laserCollider ){ UnityEngine.Debug.Log("HitByLaserProxy" ); }
破解效果 安装apk后,启动游戏即可。