逆向某讯的global-metadata.dat的解密算法

查看 29|回复 4
作者:headedit   
最近想研究一下解密 global-metadata.dat 文件,于是准备了一份加固过的apk和一份未加固的apk。
话不多说直接开搞
1、尝试静态调试:搜索“global-metadata.dat”发现加固后的libil2cpp.so中加载函数是加密过的
2、尝试动态调试:使用frida在加载libil2cpp.so后将其dump出来,然后使用soFixer修复,使用IDA查看
function dump_so(libName:string) {
    Java.perform(function() {        
        // // 1. 更可靠地获取模块信息
        var libso = Process.getModuleByName(libName);
        if (!libso) {
            console.error("[!] 错误:未找到模块 libil2cpp.so");
            return;
        }
        // 2. 验证基地址和大小是否合理
        console.log("[+] 模块基址: " + libso.base);
        console.log("[+] 模块大小: " + libso.size);
        if (libso.size  0x10000000) { // 大小合理性检查,例如大于256MB则可疑
            console.error("[!] 模块大小异常,可能获取失败");
            return;
        }
        // 3. 安全地设置内存权限并读取
        try {
            // 修改内存权限为可读
            // var originProtection =
            Memory.protect(libso.base, libso.size, 'r--');
            // 关键改进:分块读取内存,避免不连续映射区域导致崩溃
            var chunkSize = 0x1000; // 每次读取4KB
            var totalSize = libso.size;
            var file_path = "/sdcard/download/" + libso.name + "_" + libso.base + "_" + ptr(totalSize) + ".so";
            var file_handle = new File(file_path, "wb");
            if (file_handle) {
                for (var offset = 0; offset

3、搜索字符串global-metadata.dathook,定位到函数sub_48D10C。

结合il2cpp的源码


536db527-2cd5-4496-838f-603b8769010e.png (23.28 KB, 下载次数: 1)
下载附件
2025-10-22 16:28 上传



536db527-2cd5-4496-838f-603b8769010e.png (14.64 KB, 下载次数: 3)
下载附件
2025-10-22 16:28 上传

可以知道exportedTypeDefinitionsOffset和exportedTypeDefinitionsSize的偏移值,尝试在未加固的程序里hook一下该函数获取返回值。
var libil2cpp: Module | null = null;
var isCalled = false;
var metadata_size = 0;
function hookdlopen() {
  var dlopen = Process.getModuleByName("libc.so").getExportByName("dlopen");
  if (dlopen !== null) {
    Interceptor.attach(dlopen, {
      onEnter: function (args) {
        var path = args[0].readCString();
        if (path && path.indexOf('libil2cpp.so') !== -1) {
          this.path = path;
          console.log("[+] 检测到 libil2cpp.so 加载");
        }        
      },
      onLeave: function (retval) {
        if (this.path && this.path.indexOf('libil2cpp.so') !== -1 && !isCalled) {
          libil2cpp = Process.getModuleByName("libil2cpp.so");
          if (!libil2cpp) {
            console.error("[!] 错误:未找到模块 libil2cpp.so");
            return;
          }
          else{
            console.log("[+] 成功获取 libil2cpp.so 模块: " + libil2cpp.name + " 基址: " + libil2cpp.base + " 大小: " + libil2cpp.size);
          }         
          hook_loadMetadata();
        }
      }
    });
  } else {
    console.error("无法找到 dlopen 函数");
  }
}
function hook_loadMetadata() {
  const loadMetadata = libil2cpp?.base.add(0x48D10C);
  if (loadMetadata) {
    console.log("[+] 成功获取函数 loadMetadata 地址: " + loadMetadata);
    Interceptor.attach(loadMetadata, {
      onEnter: function (args) {
        console.log('loadMetadata called');
      },
      onLeave(retval) {
        console.log('loadMetadata returned');
        if (!isCalled) {
          // 捕获元数据指针
          globalMetadata.ptr = retval;
          console.log("[+] 元数据加载成功! 地址: " + globalMetadata.ptr);
          // 读取头部
          const tamperedSanity = globalMetadata.ptr.readU32();
          const tamperedVersion = globalMetadata.ptr.add(4).readU32();
          console.log("    魔数: 0x" + tamperedSanity.toString(16));
          console.log("    版本: " + tamperedVersion);
          // 计算完整元数据大小
          const exportedTypeOffset = globalMetadata.ptr.add(0xE0).readU32(); // 偏移0xE0是exportedTypeDefinitionsOffset
          const exportedTypeSize = globalMetadata.ptr.add(0xE4).readU32();   // 偏移0xE4是exportedTypeDefinitionsSize
          globalMetadata.size = exportedTypeOffset + exportedTypeSize;
          metadata_size = globalMetadata.size;
          console.log("    估算元数据大小: " + globalMetadata.size + " 字节");
          console_byte_array(globalMetadata.ptr, globalMetadata.size);
          isCalled = true;
        }
      },
    });
  }
  else {
    console.error("loadMetadata function not found");
  }
}
function console_byte_array(ptr: NativePointer, size: number) {
  if (!ptr || size === 0) {
    console.error("[-] 错误: 未捕获到元数据");
    return;
  }
  const buffer = ptr.readByteArray(size);
    // console.log("    读取内存成功,开始写入文件...");
    if (buffer) {
      console.log("    元数据前64字节:");
      console.log(hexdump(ptr, {
        offset: 0,
        length: 64,
        header: true,
        ansi: true
      }));
      return true;
    }     
    else {
      console.log("    [!] 无法读取内存");
      return false;
    }
}
可以获取到与原global-metadata.dat相同的数据
可以确定在执行sub_48D10C(loadmetadata)函数后会将global-metadata.dat加载到内存中

4、hook加固后的该函数,获取返回值,发现数据除了前四个字节其余部分是完全相同的。



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (28.04 KB, 下载次数: 3)
下载附件
2025-10-22 16:29 上传

看起来数据应该就是在sub_48D10C中进行的解密
5、至此已经可以dump出解密后的global-metadata.data数据
function dump_global_metadata(ptr: NativePointer, size: number) {
  Java.perform(function () {
    console.log("[+] 开始导出元数据...");
    console.log("    地址: " + ptr);
    console.log("    大小: " + size + " 字节");
    // 方法1:使用应用私有目录(推荐)
    try {
      if (console_byte_array(ptr, size)) {
        //分块读取内存,避免不连续映射区域导致崩溃
        var chunkSize = 0x1000; // 每次读取4KB
        var totalSize = size;
        var file_path = "/sdcard/download/global-metadata.dat";
        var file_handle = new File(file_path, "wb");
        if (file_handle) {
          for (var offset = 0; offset

6、不过目的不是要dump出global-metadata.data的数据而是获取其解密算法,所以进一步查看sub_48D10C的内容,分析如何解密,逐步分析两个函数的差异
7、继续深入sub_48D10C函数,和源码对比,发现sub_45A3F0函数对应的是mmap函数
unsigned __int64 __fastcall sub_48D10C(const char *a1)
{
  unsigned __int64 v2; // x8
  const char *v3; // x9
  __int64 v4; // x0
  char *v5; // x8
  unsigned __int64 v6; // x9
  __int64 v7; // x0
  const char *v8; // x1
  __int64 v9; // x20
  unsigned __int64 v10; // x19
  const char *v12; // [xsp+0h] [xbp-70h] BYREF
  __int64 v13; // [xsp+8h] [xbp-68h]
  _QWORD v14[2]; // [xsp+10h] [xbp-60h] BYREF
  const char *v15; // [xsp+20h] [xbp-50h]
  _QWORD v16[3]; // [xsp+28h] [xbp-48h] BYREF
  char *v17; // [xsp+40h] [xbp-30h] BYREF
  unsigned __int64 v18; // [xsp+48h] [xbp-28h]
  sub_44B644((__int64)v14);
  v12 = "Metadata";
  v13 = 8LL;
  v2 = (unsigned __int64)LOBYTE(v14[0]) >> 1;
  if ( (v14[0] & 1) != 0 )
    v3 = v15;
  else
    v3 = (char *)v14 + 1;
  if ( (v14[0] & 1) != 0 )
    v2 = v14[1];
  v17 = (char *)v3;
  v18 = v2;
  sub_44ADBC(&v17, &v12, v16);
  if ( (v14[0] & 1) != 0 )
    sub_8C66D0();
  v4 = sub_8C6670(a1);
  if ( (v16[0] & 1) != 0 )
    v5 = (char *)v16[2];
  else
    v5 = (char *)v16 + 1;
  if ( (v16[0] & 1) != 0 )
    v6 = v16[1];
  else
    v6 = (unsigned __int64)LOBYTE(v16[0]) >> 1;
  v12 = a1;
  v13 = v4;
  v17 = v5;
  v18 = v6;
  sub_44ADBC(&v17, &v12, v14);
  LODWORD(v17) = 0;
  v7 = sub_427A0C((__int64)v14, 3, 1u, 1u, 0, &v17);
  if ( (_DWORD)v17 )
  {
    if ( (v14[0] & 1) != 0 )
      v8 = v15;
    else
      v8 = (char *)v14 + 1;
    sub_45A264("ERROR: Could not open %s", v8);
  }
  else
  {
    v9 = v7;
    v10 = sub_45A3F0();
    sub_427C4C(v9, &v17);
    if ( !(_DWORD)v17 )
      goto LABEL_22;
    sub_45A400(v10);
  }
  v10 = 0LL;
LABEL_22:
  if ( (v14[0] & 1) != 0 )
    sub_8C66D0();
  if ( (v16[0] & 1) != 0 )
    sub_8C66D0();
  return v10;
}

8、继续深入分析sub_45A3F0函数,发现只要sub_4283B4和sub_428574函数和返回值有关



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (38.32 KB, 下载次数: 3)
下载附件
2025-10-22 16:29 上传

sub_4283B4函数看起来应该是和文件的加载有关和解密应该没有关系,那解密的逻辑应该就是藏在sub_428574里了
9、和返回值有关的函数只有sub_8C6750,与未加固的对比




74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (41.77 KB, 下载次数: 2)
下载附件
2025-10-22 16:30 上传




74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (22.25 KB, 下载次数: 3)
下载附件
2025-10-22 16:30 上传




74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (22.44 KB, 下载次数: 3)
下载附件
2025-10-22 16:30 上传




74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (2.11 KB, 下载次数: 3)
下载附件
2025-10-22 16:31 上传


可以看到这里原本是调用导入函数mmap函数



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (22.17 KB, 下载次数: 3)
下载附件
2025-10-22 16:31 上传




74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (3.89 KB, 下载次数: 2)
下载附件
2025-10-22 16:32 上传


现在则是变成了指向某个地址,使用frida获取其实际地址并算出偏移值
function find_ptr(name:string,ptr_addr:number) {
  var qwordAddr = libil2cpp?.base.add(ptr_addr); // 替换为实际偏移量
  if (!qwordAddr)
    return;
  var m_ptr = qwordAddr.readPointer();
  var ptr_module = Process.findModuleByAddress(m_ptr);
  var rva = m_ptr.sub(ptr_module ? ptr_module.base : ptr(0));
  console.log(name+" 地址: " + m_ptr + " 所属模块: " + (ptr_module ? ptr_module.name : "未知模块") + " 偏移: " + rva);
}
find_ptr("mmap",0x91F270)



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (5.49 KB, 下载次数: 3)
下载附件
2025-10-22 16:32 上传

未加固



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (4.46 KB, 下载次数: 3)
下载附件
2025-10-22 16:32 上传

加固
10、找到偏移值的位置



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (15.49 KB, 下载次数: 1)
下载附件
2025-10-22 16:33 上传

function find_mmap_ptr() {
  var qwordMmapAddr = libil2cpp?.base.add(0xB9F5E0); // 替换为实际偏移量
  if (!qwordMmapAddr)
    return;
  var mmapPtr = qwordMmapAddr.readPointer();
  var mmapModule = Process.findModuleByAddress(mmapPtr);
  var rva = mmapPtr.sub(mmapModule ? mmapModule.base : ptr(0));
  console.log("[+] mmap 地址: " + mmapPtr + " 所属模块: " + (mmapModule ? mmapModule.name : "未知模块") + " 偏移: " + rva);
}



74dc224e-bf9e-4fdf-a99a-9b7690074d69.png (3.94 KB, 下载次数: 3)
下载附件
2025-10-22 16:33 上传

计算出偏移,发现是libtprt.so库里的,根据汇编代码可知最终调用的地址是19f510+b0=1306c0,也就是函数1306c0,应该离结果很近了!



856eb487-5ca1-4a33-9335-2a3eb8467eeb.png (45.97 KB, 下载次数: 3)
下载附件
2025-10-22 16:33 上传

11、那么这个函数应该就是实际的mmap函数了,hook一下验证



856eb487-5ca1-4a33-9335-2a3eb8467eeb.png (12.31 KB, 下载次数: 2)
下载附件
2025-10-22 16:34 上传

let libtprt: Module | null = null;
function load_libtprt(){
  if(libtprt){
    return;
  }
  libtprt = Process.getModuleByName("libtprt.so");
  if(!libtprt){
    console.error("[-] 错误: 未找到 libtprt.so 模块");
    return;
  }
  console.log("[+] 成功获取 libtprt.so 模块: " + libtprt.name + " 基址: " + libtprt.base + " 大小: " + libtprt.size);
  var sub_1306C0 = libtprt.base.add(0x1306C0);
  if(!sub_1306C0){
    console.error("[-] 错误: 未找到 sub_1306C0 函数地址");
    return;
  }
  console.log("[+] 成功获取 sub_1306C0 函数地址: " + sub_1306C0);
  Interceptor.attach(sub_1306C0,{
    onEnter: function(args){
      console.log("[+] sub_1306C0 被调用");
    },
    onLeave: function(retval){
      console.log("[+] sub_1306C0 返回: " + retval);      
      globalMetadata.ptr = retval;
      console.log("[+] 元数据加载成功! 地址: " + globalMetadata.ptr);
      const tamperedSanity = globalMetadata.ptr.readU32();
      const tamperedVersion = globalMetadata.ptr.add(4).readU32();
      console.log("    魔数: 0x" + tamperedSanity.toString(16));
      console.log("    版本: " + tamperedVersion);
      const exportedTypeOffset = globalMetadata.ptr.add(0xE0).readU32();
      const exportedTypeSize = globalMetadata.ptr.add(0xE4).readU32();  
      globalMetadata.size = exportedTypeOffset + exportedTypeSize;
      metadata_size = globalMetadata.size;
      console.log("    估算元数据大小: " + globalMetadata.size + " 字节");
      console_byte_array(globalMetadata.ptr, globalMetadata.size);
    }
  });
}
输出



2151f4dd-cec3-4924-b7c8-c27b40e64abe.png (33.78 KB, 下载次数: 2)
下载附件
2025-10-22 16:34 上传

可以确认该函数就是mmap函数了,继续深入分析。
11、查看sub_1306c0函数,F5看一下伪C



d1756560-04de-4b63-963f-15bbc8fe96ed.png (21.84 KB, 下载次数: 3)
下载附件
2025-10-22 16:34 上传

可以看到有一个数字非常的显眼3647341504,转成16进制是12 72 43 94,这与加密后的global-metadata.dat的头一致,猜测sub_13ECC就是解密函数,跟进去看看
__int64 __fastcall sub_132ECC(__int64 a1, unsigned __int64 a2, __int64 a3, __int64 a4)
{
  char v6; // w9
  unsigned __int64 v7; // x8
  _BYTE *v8; // x10
  unsigned int v10; // w21
  __int64 i; // x8
  __int64 v12; // x10
  int8x16_t v13; // q0
  __int64 v14; // x14
  __int64 v15; // x11
  __int64 v16; // x13
  __int64 v17; // x12
  __int64 v18; // x9
  _BYTE *v19; // x8
  __int64 v20[2]; // [xsp+8h] [xbp-18h] BYREF
  char v21; // [xsp+18h] [xbp-8h]
  if ( !a1 )
    return 0xFFFFFFFFLL;
  if ( a2 >> 20 )
  {
    if ( BYTE2(a3) )
      v10 = WORD1(a3);
    else
      v10 = -121;
    for ( i = 8LL; i != 4096; ++i )
      *(_BYTE *)(a1 + i) ^= v10;
    v20[0] = 0LL;
    v20[1] = 0LL;
    v21 = 0;
    sub_133038(v20, a2, a3, a4, (unsigned int)a3, (unsigned int)a3);
    sub_11B790(a1 + 4096, 0x4000LL, 0LL, 2LL, v20, 16LL);
    v12 = 0LL;
    if ( a2 - 69632 >= 0x10000 )
    {
      v13 = vdupq_n_s8(v10);
      v14 = 1LL;
      v15 = a1 + 69632;
      do
      {
        v16 = 0LL;
        v17 = v14;
        do
        {
          *(int8x16_t *)(v15 + v16) = veorq_s8(*(int8x16_t *)(v15 + v16), v13);
          v16 += 16LL;
        }
        while ( v16 != 0x4000 );
        ++v14;
        v15 += 0x10000LL;
      }
      while ( v17 != ((a2 - 135168) >> 16) + 1 );
      v12 = v17
很明显这就是解密函数了,在a2小于1MB时对范围内的每个字节与密钥进行简单的逐字节异或,大于1MB时则需要进一步分析。查看一下该函数的调用树。



b47948d7-cc3d-42ab-8a26-c1efd04d0718.png (13.11 KB, 下载次数: 3)
下载附件
2025-10-22 16:35 上传

这里偷个懒直接把这些函数丢给ai分析一下
分析报告:sub_132ECC函数及其子函数
工作流程(大文件):
[ol]
  • 初始化异或:
    ○        对前 4096 字节的偏移 8–4095 进行异或(密钥来自 a3)。
  • AES 密钥生成:
    ○        调用 sub_133038基于 a2, a3, a4生成 16 字节密钥。
  • AES 解密核心:
    ○        对接下来 16384 字节(偏移 4096–20479)分块(2048 字节/块)解密:
    ▪        sub_11B790→ sub_11B6C4→ sub_11BCE0(密钥扩展) → sub_11C5A4(解密)。
  • 剩余数据异或:
    ○        对偏移 69632 之后的数据:
    ▪        向量化异或每 64KB 块的前 16KB(NEON 优化)。
    ▪        逐字节异或剩余部分。
  • 小文件简化:
    ○        仅对整个数据(偏移 8 开始)异或。
    [/ol]
    后面就是写脚本还原算法了,这部分就不写了
    最后感谢@无问且问 大佬的帖子,这篇帖子https://www.52pojie.cn/forum.php?mod=viewthread&tid=2010789&extra=page%3D1&page=1给了我很大的帮助。如有不够完善或者有错误的地方欢迎大佬们指点~

    函数, 下载次数

  • 414246704   

    好厉害,看得我一头雾水,好像看着很过瘾,不过我不会解密,不知道怎么操作。
    yyk81   

    我的天,这个厉害了,代码一堆一堆的,真功夫!某讯会不会挖你,O(∩_∩)O哈哈~
    aiyoniganma   

    丁又丁不懂,鞋又鞋不废
    woshicaodj   

    这个太牛了
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部