腾讯游戏安全大赛2023安卓初赛

dump libli2cpp.so

进来先随便看看,unity题先看看c#的东西,il2cpp被加密过,ida里解析不出来,考虑运行时dump
直接用frida读,注意到对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
/*
如何使用
设置一个倒计时后dump指定的so文件,timeout单位是ms,默认为3000ms
*/
function dump(soName: string, timeout: number = 3000) {
setTimeout(() => {
let libSo = Process.getModuleByName(soName);
let base = libSo.base;
let size = libSo.size;
let sectionRanges = libSo.enumerateRanges("");
for (let i = 0; i < sectionRanges.length; i++) {
console.log(sectionRanges[i].base.sub(base), sectionRanges[i].size, sectionRanges[i].base.add(sectionRanges[i].size).sub(base), sectionRanges[i].protection);
Memory.protect(sectionRanges[i].base, sectionRanges[i].size, 'rwx');
let buffer = sectionRanges[i].base.readByteArray(sectionRanges[i].size);
console.log(`write ${sectionRanges[i].size} bytes sections`);
send(["dumpso", soName], buffer);
if (i + 1 < sectionRanges.length && sectionRanges[i].base.add(sectionRanges[i].size).compare(sectionRanges[i + 1].base) !== 0) {
let gap = Memory.alloc(sectionRanges[i + 1].base.sub(sectionRanges[i].base.add(sectionRanges[i].size)).toUInt32());
let buffer = gap.readByteArray(sectionRanges[i + 1].base.sub(sectionRanges[i].base.add(sectionRanges[i].size)).toUInt32());
console.log(`write ${sectionRanges[i + 1].base.sub(sectionRanges[i].base.add(sectionRanges[i].size)).toUInt32()} bytes gap`);
send(["dumpso", soName], buffer);
}
}
console.log("base: ", base);
console.log("size: ", size);
console.log("base + size: ", base.add(size));
}, timeout);
}
dump("libil2cpp.so");
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
import frida
import sys
import time


def handleResigerNatives(message):
print("----------------------------------------")
print("Native Method in class: {}".format(message['payload'][1]))
print("Native Method name: {}".format(message['payload'][2]))
print("Native Method signature: {}".format(message['payload'][3]))
print("Native Method address: {}".format(message['payload'][4]))
print("Which file register It: {}".format(message['payload'][5]))


def handleDumpSo(message, data):
with open("dump_"+message['payload'][1], "ab") as f:
f.write(data)


def onMessage(message, data):
if message['type'] == 'send':
if message['payload'][0] == "registerNatives":
handleResigerNatives(message)
elif message['payload'][0] == "dumpso":
handleDumpSo(message, data)
else:
print("[!!] Message from target: ", message['payload'])
else:
print("Not a send type Message", message['stack'])


with open('dumpso.js', "r", encoding="utf-8") as f:
jscode = f.read()

targetProcessPackName = "com.com.sec2023.rocketmouse.mouse"
device = frida.get_device_manager().add_remote_device("127.0.0.1:12345")
try:
pid = device.spawn(targetProcessPackName)
session = device.attach(pid)
print(pid)
except frida.ProcessNotFoundError:
print("No such process")
sys.exit(0)

# device.resume(pid)
# input("continue")
script = session.create_script(jscode)
script.on('message', onMessage)

script.load()
device.resume(pid)

sys.stdin.read()


这样so基本都出来了,再结合ll2cppDumper和apk里找到的global-metadata修复一下c#符号信息

但是因为是dump的,所以偏移和节符号之类的多少有些损坏,这样直接扔进ida里是无法识别的,所以要修复elf,如果手动修的话就是把so扔进010里先跑一遍elf模板识别关键字段,然后把progame_table段里的物理地址值改成虚拟地址对应的值,把文件中的段大小改成内存中的段大小,具体原理就是因为dump下来的elf中数据的偏移都是还在内存中的偏移,同时因为内存中的elf会把一些像stack_segment之类只有在运行时才会占据实际空间的段拓展,所以有些段的位置会被挤歪,也就是物理地址和虚拟地址不符,而ida只识别物理地址,所以要把物理地址的值改成虚拟地址对应的值,还有把在文件中的大小改为在内存中的大小,这样才能定位真实的偏移

节表的部分在dump时都会损坏,要去未解密的so里把节表的部分复制过来
同理对节表(section_table)里的元素也做修正偏移的操作

注意到节表里有个储存各字段名称的部分由elf头里的string_table_index定位

这个值是节表的索引,对应的节表元素里储存了所有表头的名称,这个部分在dump的时候是会丢失的,手动修复就要去原来apk里的so中把对应的部分复制出来(因为表头名称是不加密的)

这些都修完后按理就修好了,修的过程中发现有些elf的部分没识别出来是正常情况,因为没修好010模板无法识别,边修边f5就会逐渐把符号表,节表之类的都显示出来

但是主播主播,你的操作太麻烦了,有没有一把梭的方法

有的有的,直接上soFixer,修之前记得先用ll2cpp dumper提取符号,然后在用soFixer修,不然修完后的ll2cpp dumper就不识别符号了,用soFixer只要输入dump时的基址就能一键修正偏移了

修完后就是用ll2cppdumper自带的ida脚本把符号信息导入ida,在ida的script file里选择ida_with_struct_py3.py这个脚本,然后按照提示依次选择script.json和il2cpp.h两个文件导入脚本,就可以在ida里恢复符号
记得ida里也要设定基址,在edit->segment->rebase一栏中填入dump时的基址
这样跑完后该有的符号就都有了,因为题目要求做注册机,token是用小键盘输入的,所以先搜下keyboard试试看能不能找到相关函数

分析注册机输入


可以看到还是找到了,拿frida hook一下看看调用

这里虽然做的时候软件一直崩溃,但还是拿到了hook数据

这里是按了一下1按了一下enter,可以看到最后都调用了SmallKeyboard__iI1Ii,看看这个函数什么情况

可以看到这里针对keyType的三种不同情况做不同处理,其中type=2的情况又调用了别的函数,所以重点hook SmallKeyboard__iI1Ii_469567457968看看这个v23是什么,怀疑是我们的输入

hook结果如下,这个0x7b就是我们输入的123,点进这个函数看,发现最终调用了一个导入函数,如果跳过去是jumpout就手动c一下把数据转成代码,可能是ida误判没有把代码反编译出来

那么这里很有可能是加密了,我们得跳到sec2023这个so的导出表里找这个函数,这里指的是导出表的第十个元素
上文的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
setTimeout(() => {
let lib = Process.getModuleByName("libil2cpp.so");
console.log(lib.base);
let offset = 0x6EF9C28000;
let addr = [0x0000006EFA08E300,
0x0000006EFA08E18C,
0x0000006EFA08D880,
0x0000006EFA08DAB0,
0x0000006EFA08DFDC,
0x0000006EFA08DE90,
0x0000006EFA08E184,
0x0000006EFA08E2F8,
0x0000006EFA08E3B0,
0x0000006EFA08E3A8
]
var name = ["SmallKeyboard___ctor(SmallKeyboard_o *this, const MethodInfo *method)",
"SmallKeyboard__Start(SmallKeyboard_o *this, const MethodInfo *method)",
"void SmallKeyboard__iI1Ii(SmallKeyboard_o *this, SmallKeyboard_iII1i_o *info, const MethodInfo *method)",
"SmallKeyboard__iI1Ii_469567457968(SmallKeyboard_o *this, uint64_t i1I, const MethodInfo *method)",
"void SmallKeyboard__iI1Ii_476641288156(SmallKeyboard_o *this, UnityEngine_GameObject_o *go, const MethodInfo *method",
"SmallKeyboard__oO0oOo0(SmallKeyboard_o *this, const MethodInfo *method)",
"SmallKeyboard__oO0oOoO(SmallKeyboard_o *this, const MethodInfo *method)",
"SmallKeyboard___c__DisplayClass14_0___ctor(SmallKeyboard___c__DisplayClass14_0_o *this, const MethodInfo *method)",
"SmallKeyboard___c__DisplayClass14_0___Start_b__0(SmallKeyboard___c__DisplayClass14_0_o *this, const MethodInfo *method)",
"SmallKeyboard_iII1i___ctor(SmallKeyboard_iII1i_o *this, const MethodInfo *method)"
]
for (let i = 0; i < addr.length; i++) {
console.log("hook " + name[i]);
Interceptor.attach(lib.base.add(addr[i]).sub(offset), {
onEnter: function () {
console.log("called " + name[i]);
}
})
}
Interceptor.attach(lib.base.add(0x0000006EFA4839C4).sub(offset), {
onLeave(retval) {
console.log(retval);
},
})
//这下面是在hook一些libsec2023里的部分,和上文无关
var libsec = Process.getModuleByName("libsec2023.so");
Interceptor.attach(libsec.base.add(0x31164), {
onEnter: function () {
console.log("enter 31164");
}
})
Interceptor.attach(libsec.base.add(0x311a0), {
onEnter: function () {
console.log(this.context.x2.sub(libsec.base));
}
})
}, 3000);



因为这里有br跳转干扰静态分析,所以换bn用脚本修跳转
先将csel/cset 修成mov x0 x1 mov x0 x2

然后分别执行两次获取结果