0%

gslab2021 初赛 - 安卓客户端安全

最终效果

成功实现RocketMouse直装破解版

app分析

文件结构

首先查看apk文件结构,发现是mono引擎的unity3d游戏,且Assembly-CSharp.dll已加密。
并且存在assets/filelist文件,测试发现是apk中各个文件的crc32校验值。

java层

使用frida的spawn模式启动app,弹出对话框,点击对话框会退出进程,于是使用jeb打开该apk文件。
发现com.tencent.games.sec2021.Sec2021MsgBoxonDismiss函数存在退出代码。

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函数的交叉引用,最终跟踪至Sec2021IPConNativeEngineResponse函数,推测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();
// console.log("[dlopen]", path);
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) {
// hook_sec2021();
}
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.so0x1D29C函数。

其中对文件头和文件名进行了判断,如果是MZ开头且路径不包含Assembly-CSharp.dll,则跳转至0x1CF88执行。否则初始化指针,并调用0x1CF4C中的解密算法,将sec2021.png0x410B至末尾的数据解密,解密结果即为真正加载的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

准备工作

使用lieflibmono.so添加frida-gadget依赖

1
2
3
4
import lief
bin = 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时(通过数据大小判断),修改MouseControllerHitByLaser函数

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) {
// console.log("crc check");
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.dllAssembly-CSharp.dll作为引用,编写一个注入dll,从而拦截HitByLaser方法(使用MonoHook,其原理为替换函数地址)

在native层hook dlopen函数,过掉libsec2021.so的检测,并获取libmono句柄,然后导出mono的api,在Assembly-CSharp.dll加载后,调用api加载注入dll(使用VitualAppCydia 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;
//反射获取函数的`MethodInfo`
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后,启动游戏即可。