0%

gslab2021 决赛 - 安卓客户端安全

最终效果

使用frida实现功能破解,未完全绕过完整性检测,猜测解密so文件时有校验

app分析

文件结构

游戏引擎为il2cpplibil2cpp.so代码段加密,global-metadata.datfilelist被加密。

逆向分析过程

获取global-metadata.dat

使用frida,从内存中dump并修复libil2cpp.so。然后使用IDA打开,搜索字符串global-metadata.dat,定位到MetadataCache__Initialize(偏移值5580FC),

由此获取到s_GlobalMetadata偏移值为760E90,hook该函数,dump出解密后的global-metadata.dat(需修复文件头)
(之后没用上这个)

参考正向编程的逻辑

参考FlappyBirdStyleGame代码,得知碰撞检测函数为OnTriggerEnter2D(水管)和OnCollisionEnter2D(地面)

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
void OnTriggerEnter2D(Collider2D col)
{
if (GameStateManager.GameState == GameState.Playing)
{
if (col.gameObject.tag == "Pipeblank") //pipeblank is an empty gameobject with a collider between the two pipes
{
GetComponent<AudioSource>().PlayOneShot(ScoredAudioClip);
ScoreManagerScript.Score++;
}
else if (col.gameObject.tag == "Pipe")
{
FlappyDies();
}
}
}

void OnCollisionEnter2D(Collision2D col)
{
if (GameStateManager.GameState == GameState.Playing)
{
if (col.gameObject.tag == "Floor")
{
FlappyDies();
}
}
}

所以之后重点关注OnTriggerEnter2D函数

hook il2cpp_runtime_invoke

由于该app会检测frida默认端口,故需要修改监听端口(如 1234)启动frida服务端

1
adb shell su -c "/data/fs -l 0.0.0.0:1234 &"

转发端口并spawn模式启动:

1
2
3
4
adb devices
adb forward tcp:1234 tcp:1234
frida-ps -H 127.0.0.1:1234
frida -H 127.0.0.1:1234 -f com.personal.flappybird -l script.js --no-pause

使用frida hook libil2cpp.soil2cpp_runtime_invoke函数,打印被调用的函数地址及函数名。
由于该函数被魔改,所以需要对比il2cpp源码(位于..\Unity\Editor\Data\il2cpp\libil2cpp),同时使用Unity手动编译一份apk,反编译进行参考对比(笔者编译的是同类游戏FlappyBirdStyleGame

该函数参数为(const MethodInfo * method, void *obj, void **params, Il2CppException **exc)

其中method包含函数名及函数地址,所以只需要通过多级指针+偏移的方式,即可找到命名空间、类名、函数名及函数地址

直接hook,打印输出为空,可知偏移值被修改了。通过对比正常编译的 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
44
45
function dump_class(p) {
var off = 48
var namespaze = ptr(p).readPointer().readPointer().readCString()
var s = ptr(p).add(off).readPointer().readCString()
return namespaze + " " + (s)
}

function dump_method(p, soAddr) {
var off = 24
var name = ptr(p).add(off).readPointer().readCString()
var k = dump_class(ptr(p).add(off + 4).readPointer());
var method_ptr = ptr(p).add(40).readPointer().sub(soAddr)
return k + "." + name + " " + method_ptr;
}

function hook_il2cpp() {
var libbase = Module.findBaseAddress("libil2cpp.so");
console.log(libbase);

addr = Module.findExportByName("libil2cpp.so", "il2cpp_runtime_invoke");
var il2cpp_runtime_invoke = new NativeFunction(addr, 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
Interceptor.replace(addr, new NativeCallback(function (mtd, obj, params, exec) {

if (obj != 0x0 && obj != null) {
var method = dump_method(mtd, libbase);

if (method.indexOf("Assembly-CSharp") != -1 && method.indexOf("Update") == -1) {
console.warn("il2cpp_runtime_invoke() called!", method);
}

//不调用该函数
if (method.indexOf("PlayerController.OnTriggerEnter2D") != -1) {
var paramobj = params.readPointer();
var klass = params.readPointer().readPointer()
console.warn("il2cpp_runtime_invoke() called!", mtd, obj, params, paramobj, klass);
console.log(method, dump_class(klass));
// return 0
return ptr(0x0);
}
}

return il2cpp_runtime_invoke(mtd, obj, params, exec);
}, 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']));

}
打印被调用函数
1
2
il2cpp_runtime_invoke() called! Assembly-CSharp.dll PlayerController.OnTriggerEnter2D 0x540d04
il2cpp_runtime_invoke() called! Assembly-CSharp.dll ObstacleSpawner.OnTriggerEnter2D 0x5403b0

显然PlayerController.OnTriggerEnter2D就是我们要找的函数,由此可得到偏移值0x540d04

查看 libil2cpp.so

从内存中dump libil2cpp.so并修复,使用IDA打开。
由于之前已使用frida定位到函数地址0x540d04,跳转到此函数,按F5分析。

可找到判断障碍物的关键位置(找到之后发现可以通过getGameObject、getTag、GameObject_CompareTag等函数,查看交叉引用定位到此处):

等libil2cpp.so解密后,动态patch此处即可

使用frida验证,发现可以实现功能

1
2
3
4
5
6
7
8
function hook_il2cpp() {
var libbase = Module.findBaseAddress("libil2cpp.so");
console.log(libbase);
var addr = libbase.add(0x540e60);
var barr = [0xe2, 0x00, 0x00, 0xea];
Memory.protect(addr, 0x4, 'rwx');
Memory.writeByteArray(addr, barr);
}

去检测(未完成)

使用堆栈回溯法逐一去除检测(即每当app崩溃,就使用日志查看堆栈,修改libsec2021.so的相应位置)
结果还是未完全绕过,猜测解密so文件时有校验