#随笔-Try to reverse ACE 2023

😭5ea1


这里没有前言


哎三星大虫子三星劫三星瑟庄妮真有实力吧,上了个执事挂机,hook完回来一看收了400层,商店里连刷6只大虫子直接3星了,要啥运营啊


apk的本地库里有libil2cpp.so,放ida看一下,发现是加密过的,看不了
1
因为在实际调用过程中,肯定不可能直接使用这个被加密的lib库,所以可以考虑使用fridahook把要的库dump下来
这里可以采用getModuleByName的方法,直接传进去lib_name,获取地址,然后用lib.base和lib.size来读取内容
一开始我写了如下代码:

function dump_so(so_name){
    Java.perform(function() {
        var libso = Process.getModuleByName(so_name);
        console.log("libso base address: " + libso.base.toString(16));
        console.log("libso size: " + libso.size.toString(16));
        console.log("libso path: " + libso.path);
        console.log("libso name: " + libso.name);

        Memory.protect(ptr(libso.base), libso.size, 'rwx');

        var data = Memory.readByteArray(libso.base, libso.size);
        var file = new File(/data/local/tmp/dump.so, "wb");
        file.write(data);
        file.flush();
        file.close();
        console.log("libso dumped to " + dump_path);
        console.log("libso dump done!");
    });
}

function main() {
    var so_name = "libil2cpp.so"; 
    dump_so(so_name);
}

setImmediate(main);

但是使用的时候发现两个错:

  1. 在读取的时候,会报错Error: access violation accessing 0x71191e1000
    这个报错的原因可能是因为在lib中有某种防止读取的内存防御措施,这个措施在0x71191e1000这个地址处,所以当我们修改内存权限的时候,要绕过它修改
  2. frida对于应用沙箱外的地址没有操作权限,所以产生的dump.so要在应用沙箱内完成创建

基于上面的两个错误,我们可以得到下面的代码:

function dump_so(so_name){
    Java.perform(function() {
        
        var context = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();
        var dir = context.getFilesDir().getAbsolutePath();
        
        var libso = Process.getModuleByName(so_name);
        console.log("libso base address: " + libso.base.toString(16));
        console.log("libso size: " + libso.size.toString(16));
        console.log("libso path: " + libso.path);
        console.log("libso name: " + libso.name);

        Memory.protect(ptr(libso.base), libso.size, 'rwx');
        Memory.protect(ptr(libso.base).add(0x13B7000), libso.size - 0x13B7000, 'rwx');

        var data = Memory.readByteArray(libso.base, libso.size);
        var dump_path = dir + "/" + libso.name;
        var file = new File(dump_path, "wb");
        file.write(data);
        file.flush();
        file.close();
        console.log("libso dumped to " + dump_path);
        console.log("libso dump done!");
    });
}

function main() {
    var so_name = "libil2cpp.so"; 
    dump_so(so_name);
}

setImmediate(main);

运行后得到:

libso base address: 7119420000
libso size: 13cc000
libso path: /data/app/~~c4CHbJNFMW6K-83Dl7_psQ==/com.com.sec2023.rocketmouse.mouse-Wo6nAo9UMfyLXh-IViOx-g==/lib/arm64/libil2cpp.so
libso name: libil2cpp.so
libso dumped to /data/user/0/com.com.sec2023.rocketmouse.mouse/files/libil2cpp.so
libso dump done!

由于这个位置需要su权限才能读取,不能直接pull,所以要先复制到共享文件夹,再pull拉取:

adb shell
su
cp /data/user/0/com.com.sec2023.rocketmouse.mouse/files/libil2cpp.so /sdcard/
exit
exit
adb pull /sdcard/libil2cpp.so  

哦,这个位置还要记得保存一下基址,后面要用


拿完解密后的lib后,需要使用Il2CppDumper来获取符号表
这里要用到一个global-metadata.dat,这个dat直接能在apk中解压缩找到:
2

条件完备以后可以使用Il2CppDumper来操作
将这两个文件放入Il2CppDumper的input中后,双击Il2CppDumper.exe进行分析,地址直接填写前面获取的基址即可
3
运行后在output中获得结果


修复dump下来的so

由于dump下来的文件是在运行时的时候dump的,所以文件中的地址偏移也是在虚拟环境中的偏移,而ida这一类的静态分析工具,一般使用文件真实偏移来作为分析标准,所以这往往会导致ida的分析失败
所以我们需要把so文件中的真实偏移替换为虚拟偏移,让ida分析虚拟环境下的文件
此处替换需要考虑两个部分: segment(段)section(节),分别由program header tablesecion header table中的成员指出
使用010Editor,用本地库中的libil2cpp.solibil2cpp.sp_0x7122e17000_0x13cc000.so对照修复

010Editor中,可以使用Templates中的模板辅助分析,然后在Templates Result中,使用Ctrl Shift CCtrl Shift V 可以直接对元素进行复制

修复segment

segment段program_header_table中的成员定义,定义内容如下:

成员名称 含义
p_offset_FROM_FILE_BEGIN 在实际文件中的偏移
p_vaddr_VIRTUAL_ADDRESS 在虚拟空间中的偏移
p_filesz_SEGMENT_FILE_LENGTH 在实际文件中的大小
p_memsz_SEGMENT_RAM_LENGTH 在虚拟空间中的大小
那么按照上面的定义,我们只需要将p_vaddr_VIRTUAL_ADDRESS中的值转移到p_offset_FROM_FILE_BEGIN中,并将p_memsz_SEGMENT_RAM_LENGTH中的值转移到p_filesz_SEGMENT_FILE_LENGTH中即可
4

修正节头表(secion header table)的偏移

节头表 (secion header table) 的位置在最后一个段 (segment) 之后,我们可以从 ELF 文件的 Execution View 直观看出
5
由这张图可见,节头表是整个elf中的最后一个element,所以就对应了program_header_table中的最后一个element,即program_table_element[10]
6
其中可以看到这个段的起始地址是13BC000h,段的长度为63352,经过简单计算可得:
虚拟地址起点=13BC000h+63352d=13BC000h+F778h=13CB778h

而在Template Results中,节头表的起始地址由elf_header->e_shoff_SECTION_HEADER_OFFSET_IN_FILE决定
7
所以只要直接把这个值修改为13CB778h即可

修正section内容

libil2cpp.so 点击 struct section_header_table 并按下 Ctrl + Shift + C ,,然后回到 libil2cpp.so_0x7122e17000_0x13cc000.so 中,选中 section_header_table 然后按下 Ctrl + Shift + V , 按下 F5 重新运行模板 ELF.bt

恢复节 (section) 的名称

ELF 文件中的每个 section 都是有名字的,比如 .data 、 .text 、 .rodata ,每个名字都是一个字符串,既然是字符串就需要一个字符串池来保存,而这个字符串池也是一个 section ,或者说准备一个 section 用来维护一个字符串池,这个字符串池保存了其他 section 以及它自己的名字。这个特殊的 section 叫做 .shstrtab ,所有 section 的头部是连续存放在一起的,类似一个数组, e_shstrndx 变量是 .shstrtab 在这个数组中的下标

此时的每个section的名字还是乱码,而elf文件中会维护一个字符常量池来作为这些section的名称;
通过观察发现,在 libil2cpp.so_0x7122e17000_0x13cc000.so 中,找到 elf_header->e_shtrndx_STRING_TABLE_INDEX , 这个的值为 26 (0x1A), 说明了 section_header_table->section_table_element[26] 存储了所有 section 的名称

8

section_header_table->section_table_element[26]s_offset 的值决定了 section 的名称将从 1199370h 去索引

9

跳转到对应地址发现,存放的字符串都是乱码;而libil2cpp.so中的是正常字符串

10
11

所以只需要把正常的字符串常量池修正进去即可,即将 section 的符号名称从原来的 so 复制到 dump 下来的 so 里面,位置就是我们之前分析出来的 section_table_element[26] 中 s_offset 所指向的物理内存地址,即选中 libil2cpp.so 从 0x1199370h 到 0x1199470h 按下 Ctrl + Shift + C , 然后将光标移动到 libil2cpp.so_0x712a997000_0x13cc000.so 的 119A370h 处,按下 Ctrl + Shift + V

修正 节 (section) 的偏移

节 (section) 的位置和大小由节头表 (secion_header_table) 中这两个成员决定

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

修正 节 (section) 的偏移有两条规则

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