2023腾讯游戏安全安卓初赛复现

参考博客:

[原创] 腾讯游戏安全技术竞赛2023 安卓客户端初赛WriteUp-Android安全-看雪-安全社区|安全招聘|kanxue.com

[原创]2023腾讯游戏安全竞赛初赛题解(安卓)-Android安全-看雪-安全社区|安全招聘|kanxue.com

[原创]2023腾讯游戏安全大赛-安卓赛道初赛wp-Android安全-看雪-安全社区|安全招聘|kanxue.com

腾讯游戏安全大赛安卓2023初赛复现 | Blue.Blogs

2023腾讯游戏安全mobile端初赛wp - TLSN - 博客园

绕过调试检测

根据wp中的说法是检测了/data/local/tmp文件夹下的re.frida.server字符串,同时检测了默认端口。实际上检测的应该是tmp文件夹中的文件名,如果存在Android-server或者frida-server就会报错,同时检测了这两个服务器的默认端口。只要给他们改个名然后转发一下端口就可以正常运行游戏了。

先在adbshell中用指令:./fs -l 0.0.0.0:1234在1234端口上启动frida-server,之后另开cmdadb forward tcp:1234 tcp:1234转发端口,然后frida -H 127.0.0.1:1234 -f <package-name> -l Script.js即可。

解包il2cpp

解压apk后找到libil2cpp.so与metadata,尝试用il2cppDumper解包,失败。ida打开ilbil2cpp.so后发现被加密过,会在运行时解密,这里可以写一个frida脚本hook运行时的ilbil2cpp.so并dump出来得到解密后的文件。这里用的[fallw1nd]师傅的代码。

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
function WriteMemToFile(addr, size, file_path) {
Java.perform(function() {
var prefix = '/data/data/com.com.sec2023.rocketmouse.mouse/files/'

var mkdir = Module.findExportByName('libc.so', 'mkdir');
var chmod = Module.findExportByName('libc.so', 'chmod');
var fopen = Module.findExportByName('libc.so', 'fopen');
var fwrite = Module.findExportByName('libc.so', 'fwrite');
var fclose = Module.findExportByName('libc.so', 'fclose');

var call_mkdir = new NativeFunction(mkdir, 'int', ['pointer', 'int']);
var call_chmod = new NativeFunction(chmod, 'int', ['pointer', 'int']);
var call_fopen =
new NativeFunction(fopen, 'pointer', ['pointer', 'pointer']);
var call_fwrite =
new NativeFunction(fwrite, 'int', ['pointer', 'int', 'int', 'pointer']);
var call_fclose = new NativeFunction(fclose, 'int', ['pointer']);

call_mkdir(Memory.allocUtf8String(prefix), 0x1FF);
call_chmod(Memory.allocUtf8String(prefix), 0x1FF);
var fp = call_fopen(
Memory.allocUtf8String(prefix + file_path),
Memory.allocUtf8String('wb'));
if (call_fwrite(addr, 1, size, fp)) {
console.log('[+] Write file success, file path: ' + prefix + file_path);
} else {
console.log('[x] Write file failed');
}

call_fclose(fp);
});
}

function HookLibWithCallback(name, callback) {
var dlopen = Module.findExportByName('libdl.so', 'dlopen');
var detach_listener = Interceptor.attach(dlopen, {
onEnter: function(args) {
var cur = args[0].readCString();
console.log('[+] dlopen called, name: ' + cur);
if (cur.indexOf(name) != -1) {
this.hook = true;
}
},
onLeave: function() {
if (this.hook) {
console.log('[+] Hook Lib success, name:', name);
callback();
detach_listener.detach();
}
}
});
}

function DumpIL2CPP() {
var libil2cpp = Process.getModuleByName("libil2cpp.so");
WriteMemToFile(libil2cpp.base, libil2cpp.size, 'libil2cpp.so');
}

function main() {
HookLibWithCallback('libil2cpp.so', DumpIL2CPP);
}

main();

提示成功后pull出来,再使用il2cppDumper即可成功解包。

解包后的libil2cpp.so缺少了尾部的重定位表,直接将解包前的复制过来即可。

获得flag

找到相应函数

可以用il2cppDumper自带的脚本恢复libil2cpp.so的符号表。

然后在dump.cs中搜索coin,发现有个CollectCoin函数:

image-20250326113200916

在ida中跳转到这个函数的va:

image-20250326113245581

image-20250326113317701

可以看到这样一个函数,有一处和1000进行比较的地方,猜测此处就是判断是否得到flag的位置。

修改程序逻辑

只要将1000改成0,即将逻辑从1000分给出flag变成0分给出flag即可。

代码:

1
2
3
4
5
6
7
8
function PatchIncrease() {
var libil2cpp = Process.getModuleByName("libil2cpp.so");
var insn = libil2cpp.base.add(0x4653CC);/cmp的地址
Memory.protect(insn,4,"rwx");
insn.writeByteArray([0x1F,0x00,0x00,0x71]);/cmp w0 #0
console.log("hacked!")
}
PatchIncrease();

直接运行会如下报错:

image-20250326194923310

原因是app刚启动时还没有加载libil2cpp.so这个文件,所以找不到,使用%reload重新运行一下脚本即可。

image-20250326195045742

然后随便吃一个硬币就能看到flag了。

image-20250326194720526

实现注册机

分析 token生成逻辑

在dump.cs中可以看到有一个smallKeyboard类:

image-20250327215027973

在恢复了符号表的so中找到相关函数:

image-20250327215308998

写一个frida脚本