腾讯游戏安全2023

初赛

参考资料
https://bbs.kanxue.com/thread-278648.htm
https://bbs.kanxue.com/homepage-888558.htm
https://bbs.kanxue.com/homepage-thread-831526-1.htm
https://bbs.kanxue.com/homepage-thread-887676-1.htm
腾讯游戏安全竞赛官网下载 下载链接

分析题目

apk

通过jadx反编译来获取apk信息,查看AndroidManifest.xml获取包名:com.com.sec2023.rocketmouse.mouse

解压apk,在lib目录发现libil2cpp.so文件

使用Il2CppDumper获取符号信息

Il2CppDumper
直接运行Il2CppDumper.exe并依次选择il2cpp的可执行文件和global-metadata.dat文件,然后根据提示输入相应信息。

程序运行完成后将在当前运行目录下生成输出文件

1
Il2CppDumper.exe <executable-file> <global-metadata> <output-directory>

被加密了

IDA分析so

这个so加密了

利用frida进行sodump

so在运行时是解密状态的,利用frida进行sodump来得到解密状态的so。
先用frida获取so地址测试有没有frida检测

1
2
3
4
setImmediate(function (){
soAddr = Module.getBaseAddress("libil2cpp.so");
console.log(soAddr);
})

被检测了

修改frida特征

  • 修改frida-server的名字
  • 修改frida-server的默认端口

注入成功了!!

dumpso

接下来就开是编写sodump的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dump_so(soName){
var currentapplication = Java.use("android.app.ActivityThread").currentApplication();
var dir = currentapplication.getApplicationContext().getFilesDir().getPath();
var libso = Process.getModuleByName(soName);
console.log("[Name]:",libso.name);
console.log("[base]:",libso.base);
console.log("[size]:",ptr(libso.size));
console.log("[path]:",libso.path);
var file_path = dir+"/"+libso.name+"_"+libso.base+"_"+ptr(libso.size)+".so";
var file_handle = new File(file_path,"wb");
if (file_handle && file_handle !=null){
Memory.protect(ptr(libso.base),libso.size,'rwx');
var libso_buffer = ptr(libso.base).readByteArray(libso.size);
file_handle.write(libso_buffer);
file_handle.flush();
file_handle.close();
console.log("[dump]:",file_path);
}
}

dump_so("libil2cpp.so");

然后把dump下来的so给pull出来

移动位置并修改权限

pull

再次使用Il2CppDumper获取符号信息

生成如下文件

修复dump下来的so

  对于dump下来的so文件,所有的段(segment)和节(section)的偏移都是在虚拟空间中的偏移(即映射到进程空间的虚拟地址偏移),但静态分析工具分析so时所使用的偏移仍然为在实际文件中的偏移(即相对于文件开头的字节偏移量),错误的偏移导致静态分析工具如IDA等无法分析dump下来的so
  所以我们需要将segmentsection在实际文件中的偏移替换为在虚拟空间中的偏移,这些偏移由program header tablesecion header table内的成员指出
  使用010 Editor将libil2cpp.solibil2cpp.so_0x78dec12000_0x13cc000.so打开进行对比修复

修正段偏移

段的位置和大小由程序头中的字段决定

libil2cpp.so_0x78dec12000_0x13cc000.so中将program header table中的每一个element的p_vaddr复制到p_offset,p_memsz复制到p_filesz

修正节表的偏移

节表的位置在最后一个段之后
libil2cpp.so_0x78dec12000_0x13cc000.so中,section_header_table的文件偏移为0x11AB778,需要将其改为在虚拟空间的偏移


找到程序头表的最后一个段,节表的位置就是内存中的偏移(p_vaddr)加上内存中的长度(p_memsz

节表在虚拟空间的偏移为:0x13CB778 = 0x13BC000 + 0xF778
节表偏移的字段是文件头中e_shoff
修正后,按下F5刷新,重新运行模板,结果如下

补充section的内容

修正节表的偏移后,发现节表的区域没有内容,需要从libil2cpp.so中复制节表的内容到libil2cpp.so_0x78dec12000_0x13cc000.so中,重新运行模板

恢复节的名称

修复了节的内容之后,节的名称依旧是乱码
每个节的名字在.shstrtab节中保存着,首先我们要到文件头中的e_shtrndx字段找到.shstrtab的索引,其字段值是0x1A,也就是说,.shstrtab的索引是26

.shstrtab节头中的s_offset字段是section名称的偏移,其值为0x1199370

右键0x1199370可以跳转到这个地址处,发现其内容都是乱码


回到libil2cpp.so中,发现同样的地方是完整的字符串

libil2cpp.so中的section的符号名称复制到libil2cpp.so_0x78dec12000_0x13cc000.so中,F5刷新,重新运行模板

修正节的偏移

节的位置由两个字段决定,s_addrs_offset

字段 含义
s_addr 如果此 section 需要映射到进程空间,此成员指定映射的起始地址;如不需映射,此值为 0
s_offset 此 section 相对于文件开头的字节偏移量.如果 section 类型为 SHT_NOBITS,表明该 section 在文件中不占空间,这时 sh_offset 没什么用

修正节的偏移的规则:

  • 如果s_addr为0,无需修改s_offset
  • 如果s_addr不为0,则将s_addr的值复制给s_offset

修改完成好,按下F5,发现有了dynamic_symbol_table,至此,修复完成

IDA恢复符号

  将libil2cpp.so_0x78dec12000_0x13cc000.so拖入IDA中进行分析,待分析完成后,点击编辑->Segments->Rebase program,重新定位基址为0x78dec12000,这样可以分析出更多的符号

  分析完成后,点击File->Script file...运行il2cppdumper中的ida_with_struct_py3.py,需要注意的这个脚本需要运行两次,第一次选择script.json,il2cpp.h第二次选择stringliteral.json,il2cpp.h(分析时间可能很久)

获取flag

flag获取条件:当金币数量超过1000,会出现flag
有一个思路,我们知道金币的英文是coin,到前面使用Il2CppDumper得到的文件dump.cs文件里搜索coin,发现coin

跟进到IDA中,有发现,这里有一个1000,猜想到金币数量1000

切换为汇编语言,找到发现cmp指令以及值1000,这里利用frida把1000改成0,那么在游戏中随便捡一个金币就可以获取flag了

利用frida进行patch

1
2
3
4
5
6
7
8
9
10
11
12
function flag(){
var soAddr = Module.findBaseAddress("libil2cpp.so");
console.log(soAddr);
var mybase = 0x78dec12000;
var cmp = soAddr.add(0x78DF0773CC-mybase);
Memory.protect(cmp,4,"rwx");
var cmpPatch = new Arm64Writer(cmp);
cmpPatch.putBytes([0x1F,0x00,0x00,0x71]);
console.log("patch sueess")
}

flag();

运行游戏,frida注入

寻找OK按钮调用的函数


可以使用frida-il2cppDumper,用法就是直接frida注入_agent.js

1
frida -H 192.168.0.106:8000 -f com.com.sec2023.rocketmouse.mouse -l _agent.js

在IDA中分析调用的函数SmallKeyboard__oO0oOo0,明显看出是生成TOKEN的地方

SmallKeyboard类被调用了很多次,在dump.cs里面搜索发现,此处定义了KeyType不同的值对应的含义,那么这个EnterKey就是OK按钮了

回到IDA,再去搜索SmallKeyboard,找到SmallKeyboard__iI1Ii(SmallKeyboard_o *this, SmallKeyboard_iII1i_o *info, const MethodInfo *method)这个函数,这是与KeyType有关的函数,显而易见,我们需要重点分析的是KeyType == 2的情况

使用frida来hook System_Convert__ToUInt64_519142037956的返回值来验证猜想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_input(){
var soAddr = Module.findBaseAddress("libil2cpp.so");
console.log("soAddr:",soAddr);
var mysobase = 0x78dec12000;
var myfuncbase = 0x78DF46D9C4;
var funaddr = soAddr.add(myfuncbase-mysobase);
Interceptor.attach(funaddr,{
onEnter:function (args) {
console.log("hook success!")
console.log(args[0]);
console.log(args[1]);
},
onLeave:function (retval){
console.log("retval is:",retval.toInt32());

}
})
}
hook_input()

输入123456,按下OK按钮,frida的hook结果如下,猜测正确。