逆向unity游戏myyx3,使用dump.cs还原ida方法名

声明

仅用于学习记录,关键信息隐藏。

背景

游戏是unity+windows平台,使用il2cpp。

分析

首先,先尝试使用Il2CppDumper反编译

1
Il2CppDumper GameAssembly.dll global-metadata.dat out

但是报错

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Il2CppDumper.Metadata..ctor(Stream stream) in C:\projects\il2cppdumper\Il2CppDumper\Il2Cpp\Metadata.cs:line 41

应该是做了某种加密,查看了一下global-metadata.dat文件头,是正确的 AF 1B B1 FA。

那就从加载metadata的代码看起。打开IDA,Shift+F12打开Strings列表,查找global-metadata.dat,发现字符串有。 双击字符串,按下x找关联的代码,找到一个方法

  v4 = sub_xxxxx("global-metadata.dat");

那这个方法应该是il2cpp::vm::MetadataLoader::LoadMetadataFile 尝试把这个方法发给AI分析,写成python代码,但是解密没成功,还是报错。

换一个思路,发现使用frida-il2cpp-bridge是可以dump cs的。

import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
    Il2Cpp.dump("dump.cs", "./");
}

dump完之后,可以看一下里面的方法,里面的偏移地址看起来都没什么问题。那能不能利用dump.cs修改IDA的方法名呢? 把想法发给AI,让它生成一个python代码提取dump.cs的名字和偏移地址。AI生成了一个代码,实际地址是:ImageBase + RVA。

 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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import idautils
import idaapi
import idc
import ida_funcs 
import re
import os

# --- 用户配置 ---
# 文件路径
DUMP_FILE_PATH = r"E:/dump.cs" 
# ----------------

def auto_rename_from_dump(file_path):
    """
    解析 dump.cs 文件,自动在 IDA 中重命名函数。
    脚本会动态识别类名,并用 '类名::函数名' 的格式进行重命名。
    """
    if not os.path.exists(file_path):
        print(f"❌ 错误:文件未找到,请检查路径:{file_path}")
        return

    # 1. 获取 IDA 数据库的基地址 (ImageBase)
    image_base = idaapi.get_imagebase()
    if image_base == idaapi.BADADDR:
        print("❌ 错误:无法获取 ImageBase。请确保已加载有效的 IDA 数据库。")
        return

    print(f"🌟 当前 IDA 数据库 ImageBase: 0x{image_base:X}")
    print(f"🔬 正在解析文件: {file_path}")

    functions_to_rename = {}
    current_class_name = "" # 用于跟踪当前解析到的类名
    
    # 正则表达式
    # 匹配类定义行: class Name: Inherit...
    class_pattern = re.compile(r'^\s*class\s+([\w\.]+)\s*:')
    # 匹配函数定义行: 返回类型 函数名(参数); // 0xRVA
    func_pattern = re.compile(r'\s*System\.[\w\.<>\[\]]*\s+([\w\.]+)\(.*?\);\s*//\s*(0x[0-9a-fA-F]+)')

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:
                # 尝试匹配类定义
                class_match = class_pattern.search(line)
                if class_match:
                    # 找到新的类定义,更新当前类名
                    current_class_name = class_match.group(1).strip()
                    print(f"-> 发现类: {current_class_name}")
                    continue
                
                # 尝试匹配函数定义
                func_match = func_pattern.search(line)
                if func_match and current_class_name:
                    func_name = func_match.group(1).strip()
                    rva_str = func_match.group(2).strip()
                    
                    # 格式化新函数名: 类名::函数名
                    full_name = f"{current_class_name}::{func_name}"
                    
                    # 转换 RVA 为 VA
                    try:
                        rva = int(rva_str, 16)
                        va = image_base + rva
                        functions_to_rename[va] = full_name
                    except ValueError:
                        print(f"⚠️ 警告:跳过无效 RVA 行: {line.strip()}")
                
    except Exception as e:
        print(f"❌ 读取或解析文件时发生错误: {e}")
        return

    # 2. 在 IDA 中重命名
    total_found = len(functions_to_rename)
    print(f"🔍 成功从 dump 文件中解析到 {total_found} 个待重命名地址。")
    renamed_count = 0

    for va, name in functions_to_rename.items():
        # 检查地址是否在可访问的段内
        if idc.get_full_flags(va) == 0:
            continue
            
        # 尝试将地址定义为函数(如果还没有)
        if not ida_funcs.get_func(va):
            ida_funcs.add_func(va)
            
        # 执行重命名操作
        if idc.set_name(va, name, idc.SN_NOWARN | idc.SN_NOCHECK):
            renamed_count += 1

    print("--- 重命名报告 ---")
    print(f"🎉 任务完成!")
    print(f"总计解析函数数: {total_found}")
    print(f"成功重命名函数数: {renamed_count}")
    print("------------------")

# 执行主函数
# 注意:现在调用时不需要传入 CLASS_NAME_PREFIX 参数了
auto_rename_from_dump(DUMP_FILE_PATH)

在IDA执行脚本之后,大部分方法名就改过来了。

使用Reqable抓包发现,有部分请求是加密的。从代码浏览,SteamWindowsAgent看起来和登录有关系

1
2
3
4
5
6
7
import "frida-il2cpp-bridge";


Il2Cpp.perform(() => {
  const CSharp = Il2Cpp.domain.assembly("Assembly-CSharp").image;
  Il2Cpp.trace(false).classes(CSharp.class("Script.***.SdkAgent.SteamWindowsAgent")).and().attach();
}

尝试trace代码,这里看起来是上层调用,具体底层调用了哪些方法,没有trace到。 想着换一种思路,Hook网络请求,然后再从backtrace看调用链路。一般游戏都通过UnityEngine.Networking.UnityWebRequest发送请求,但是发现根本没有。 既然上面的SteamWindowsAgent是上层调用,进入IDA,找到对应的方法,按F5切换到伪代码查看,找到了一些蛛丝马迹。 它和KarmaSDK有关,KarmaSDK有多个类,把多个类都加入trace里面来。 为了提取密钥,尝试trace改成Il2Cpp.trace(true),发现可以获取参数(很多时候会报错)。那这里就可以拿到加解密了。

IDA进入加解密的方法,我找到的伪代码发给AI分析,让它帮我们实现解密方法。 发现这个游戏用两种加密方式 第一种是AES(AES-128-CBC),请求参数加密:KarmaSDK.ex::Encrypt trace这个方法获取key和IV。 第二种是RSA(PKCS1_v1_5),返回内容加密:KarmaSDK.fb::Decrypt,trace获取RSA key。

里面很多子方法调用,可以先把主方法发给AI,然后问它还需要提供哪些子方法,继续提供信息给AI就行。提供的信息越多,实现越准确。不过GROK有点奇怪,老会吹牛逼,分析DLL的时候让我上传文件给我解密,说它解密好了,给我一个下载链接,发现根本不存在这个链接。

IDA动态分析

有一些qword_7FFF0AFECAE8,静态分析是查不到值的,需要attach进程,实时查看内存。

这里是一个MD5的伪代码,sub_7FFF09EFF080是一个字符串拼接的方法

  v3 = **(_QWORD **)(qword_7FFF0AFEEA28 + 184);
  if ( !v3 )
    sub_7FFF091B94D0();
  v4 = sub_7FFF09EFF080(a2, qword_7FFF0AFECAE8, *(_QWORD *)(v3 + 56));
  v16 = (__int64 *)System_Security_Cryptography_MD5::Create(0);
  v14 = 0;
  v15 = &v16;
  try
  {
    v5 = v16;
    UTF8 = System_Text_Encoding::get_UTF8(0);
    if ( !UTF8 )
      sub_7FFF091B94D0();
    v7 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)UTF8 + 600LL))(
           UTF8,
           v4,
           *(_QWORD *)(*(_QWORD *)UTF8 + 608LL));
    if ( !v5 )
      sub_7FFF091B94D0();
    v8 = System_Security_Cryptography_HashAlgorithm::ComputeHash(v5, v7, 0);
    v9 = sub_7FFF0A011DE0(v8, 0);
  }

假如我想获得qword_7FFF0AFECAE8的内容,在v16这行加上一个断点,双击qword_7FFF0AFECAE8,后面有一个地址

qword_7FFED50DCAE8 dq 14D112E5840h

再双击后面的地址,这是一个unity的字符串,前面会有一些header,实际这个是“_”。可以让AI解析。

debug1506:0000014D112E5840 db 0D0h
debug1506:0000014D112E5841 db 0A6h
debug1506:0000014D112E5842 db  25h ; %
debug1506:0000014D112E5843 db 0A0h
debug1506:0000014D112E5844 db  4Bh ; K
debug1506:0000014D112E5845 db    1
debug1506:0000014D112E5846 db    0
debug1506:0000014D112E5847 db    0
debug1506:0000014D112E5848 db    0
debug1506:0000014D112E5849 db    0
debug1506:0000014D112E584A db    0
debug1506:0000014D112E584B db    0
debug1506:0000014D112E584C db    0
debug1506:0000014D112E584D db    0
debug1506:0000014D112E584E db    0
debug1506:0000014D112E584F db    0
debug1506:0000014D112E5850 db    1
debug1506:0000014D112E5851 db    0
debug1506:0000014D112E5852 db    0
debug1506:0000014D112E5853 db    0
debug1506:0000014D112E5854 db  5Fh ; _
debug1506:0000014D112E5855 db    0
debug1506:0000014D112E5856 db    0
debug1506:0000014D112E5857 db    0
debug1506:0000014D112E5858 db    0
debug1506:0000014D112E5859 db    0
debug1506:0000014D112E585A db    0

再看V4变量,可以直接双击,直接跳转到内存值。可以把光标移动到文字开始的地址,在Edit>Strings>Unicode,可以转成字符串。

debug1553:0000014D9D91CF44 text "UTF-16LE", '1765897011173_CJui24lxxxxxxxxxxxxxxxxxx'
debug1553:0000014D9D91CFAA text "UTF-16LE", 'WLy22Nr',0

多抓取几次,发现后面这个字符串是固定的。最终获得了这个md5是如何hash的。

另外这次也尝试了使用ida-pro-mcp,使用claude-sonet-4.5,它帮我分析了一下代码,但是看起来前面有些分析是错的。后面在read_qword的时候地址是错的,不知道是不是mcp错误的问题。不过帮它纠正一下还能继续分析下去。但是读取上面的String时也有问题,没给我读取完整,还得自己去分析。

结尾

到这里基本就结束了,使用解密方法尝试解密抓包的内容,发现都成功了。

自从有了AI之后,分析代码的活都给AI干了,不用一行行的盯着伪代码看,试错也简单,如果AI有验证能力的话更好一些,有时候就是乱说一通。

updatedupdated2025-12-282025-12-28