首页
社区
课程
招聘
[原创]以蓝色星原:旅谣为例 —— HybridCLR 解密记录 V2
发表于: 2025-9-14 16:48 1126

[原创]以蓝色星原:旅谣为例 —— HybridCLR 解密记录 V2

2025-9-14 16:48
1126

以蓝色星原:旅谣为例 —— HybridCLR 解密记录 V2

在 BW 试玩过蓝色星原 (蓝色原神) 之后,一直想找包解,浑身刺挠(

包体流出后,发现 AB 包只是单纯的 UnityCN 特色解密(无聊

但是在解密 global-metadata.dat 时,发现被加密了,解密完后头 magic 是 CODEPHIL,一眼代码哲学出品

翻文件的时候,发现几个 CDPH 开头的文件,同时又有 .NET 的 Metadata 段 magic + HybridCLR 的存在,估计这玩意是新的热加载 DLL,故写了这篇文章来记录 ¯\_(ツ)_/¯

补: 这篇文章所涉及的加解密与代码哲学新推出与 HybridCLR 共同使用的 Obfuz 混淆器相关

1. 分析 global-metadata.dat 解密

在 IDA 中搜索 CODEPHIL 或者 global-metadata.dat,前者对应 DecryptMetaData,后者对应 il2cpp::vm::GlobalMetadata::Initialize

alt text

alt text

DecryptMetaData 核心流程

  • 清空输出指针:*a2 = 0。
  • 校验头魔术 0x1357FEDA(不匹配则不解密,函数最后仍返回 1,输出保持 0)。
  • 以 64 字节为块对 [a1+0x148, a1+0x148+total_size) 原地解密;最后一块按实际剩余长度。
  • 解密完成后检查 memcmp(a1+0x148, "CODEPHIL", 8):
    • 若不相等(memcmp != 0)返回 0。
    • 若相等:设置 *a2 = a1 + 0x150,然后返回 1。

分析完 DecryptMetaData,可以将 global-metadata.dat 构造成结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
struct globalMetadata {
    int encryptMagic; // 0x1357FEDA
    int fileLength;
    char keys[256];
    char opcodes[64];
    decryptedGlobalMetadata* data;
};
 
struct decryptedGlobalMetadata {
    char magic[8]; // CODEPHIL
    void* data;
}

对于 DecryptBlock 的分析,见后文内容 =w=

2. 分析 HybridCLR 对 HotPatch DLL 的加载

官方文档中,直接使用了 Assembly.Load 来加载 HotPatch 的 DLL

alt text

其自定义后的 IL2CPP 通过下面的调用顺序对原始二进制进行加载

1
2
3
4
5
6
7
8
9
10
int32_t RuntimeApi::LoadMetadataForAOTAssembly(Il2CppArray* dllBytes, int32_t mode)
                                    |
                                    
LoadImageErrorCode Assembly::LoadMetadataForAOTAssembly(const void* dllBytes, uint32_t dllSize, HomologousImageMode mode)
                                    |
                                    
LoadImageErrorCode AOTHomologousImage::Load(const byte* imageData, size_t length)
                                    |
                                    
LoadImageErrorCode RawImageBase::Load(const void* rawImageData, size_t length)

所以对自定义 HotPatch DLL 的加载和解密会在 RawImageBase::Load 里面

3. HotPatch DLL 的加载和解密分析

在 IDA 中定位到 RawImageBase::Load (AzurPromilia CBT1 GameAssembly.dll RVA 0x95F380)

alt text

3.1 LoadCDPHHeader

原版 HybridCLR 中, LoadCDPHHeader 的位置实际是 LoadCLIHeader,所以 LoadCDPHHeader 在读取自定义结构的时候,也会读取 CLI Header

alt text

LoadCDPHHeader 具体流程如下 (伪代码已经经过 LLM 处理)

  • 读取 magic、版本 和 格式编号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 头与版本
 
// * `this->_imageLength` 在 `this+0x10`,先判 `>= 0x100`。
// * 检查魔术字:`memcmp(this->_imageData, "CDPH", 4) == 0`。
// * 版本:`*(uint32*)(this->_imageData + 4) == 1`,否则返回 `9`。
 
// 2. “密钥块”与固定起始偏移
 
// * `_cdphKey = this->_imageData + 8` 保存到 `this+0x0B40`。
// * `*(uint32*)_cdphKey <= 1`,否则返回 `10`。
 
    if (self->_imageLength < 0x100 || memcmp(self->_imageData, "CDPH", 4) != 0) return 1;
    if (*(uint32_t*)(self->_imageData + 4) != 1) return 9;
 
    self->_cdphKey = self->_imageData + 8;
    if (*(uint32_t*)self->_cdphKey > 1) return 10;
  • 读取解密用的 8 段 opcodes
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
// * 数据读取起点固定为 `data_offset = 0x110`。
 
// 3. 读取 8 段数据 → `instructions[0..7]`
 
// * 对 8 个“向量”循环(对象内连续 8 个 `std::vector<uint8_t>`,见下文布局):
 
//   * `len = *(uint32*)(image + off)`,数据体起点 `body = off + 4`,终点 `end = body + len`。`end > imageLength` 则失败。
//   * 关键点:它把**目标向量的 `last`(+8)临时写成 `imageBase + len`**,然后用一个特殊拷贝函数完成复制:
 
//     * `sub_180A520C0(destBegin, srcBegin, endPtr)` 的**语义是拷贝 `endPtr - srcBegin` 个字节**到 `destBegin`。
//     * 于是当 `srcBegin = image + body`、`endPtr = image + len` 时,效果正好是拷贝 `len` 字节。
//   * 如果容量不足,会走两条“扩容”分支之一(`j_psub_1809CABB0 / 1809CAB90`),并把 `end`(+0x10)更新为某个“容量终点”,注意这个 `end` 在实现里还会被 `& 0x7FFF...` 屏蔽最高位(汇编里 `and rax, 7FFFFFFFFFFFFFFFh`)。
//   * 推进 `off = align4(end)`。
 
    size_t off = 0x110;
 
    // instructions[0..7]
    for (int k = 0; k < 8; ++k) {
        uint32_t len  = *(uint32_t*)(self->_imageData + off);
        size_t   body = off + 4;
        size_t   end  = body + len;
        if (end > self->_imageLength) return 1;
 
        auto& vec = self->instructions[k]; // MSVC vector: first,last,end
 
        // 设 last = imageBase + len(用于 CopySpan 的 end - src 公式)
        vec.last = self->_imageData + len;
 
        // 如需扩容(比较的是 vec.end 屏蔽符号位后的值)
        if (vec.last > (vec.end & ((void*)~(1ULL<<63)))) {
            if ((int64_t)vec.end < 0) {
                void* newFirst = j_psub_1809CAB90(vec.last, /*grow*/1);
                CopySpan(newFirst, vec.first, vec.last); // 复制旧内容:vec.last - vec.first
                vec.end   = vec.last;
                vec.first = newFirst;
            } else {
                vec.first = j_psub_1809CABB0(vec.first, vec.last, /*grow*/1);
                vec.end   = vec.last;
            }
        }
 
        // 从镜像拷贝:len = (imageBase + len) - (imageBase + body)
        CopySpan(vec.first, self->_imageData + body, self->_imageData + len);
 
        off = (end + 3) & ~3ULL;
    }
  • 构造特定 Table 以检验密钥合法性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 4. 密钥校验
 
// * 读当前 `off` 处 16 字节到 `output`。
// * 用两组常量 + 计数器生成 0x100 字节工作表(那组 SSE 指令)。
// * `DecryptBlock(buf=table, size=0x100, key=_cdphKey+8, inout=output, n16=0x10)`;
// * 要求 `output == "Hello, HybridCLR"`,否则返回 `1`。
 
    // 密钥校验
    uint8_t output[16];
    memcpy(output, self->_imageData + off, 16);
    uint8_t table[0x100];
    BuildTableSSE(table); // 汇编里那段 SSE
    DecryptBlock(/*buf=*/table, /*size=*/0x100,
                 /*key=*/self->_cdphKey + 8,
                 /*inout=*/output, /*n16=*/0x10);
    if (memcmp(output, "Hello, HybridCLR", 16)) return 1;
  • 读取 CLI Header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 5) 入口与元数据
*entrypointToken = *(uint32_t*)&self->_imageData[off + 0x10];
*metadataAddress = *(uint32_t*)&self->_imageData[off + 0x14];
*metadataSize    = *(uint32_t*)&self->_imageData[off + 0x18];
 
// 6) 节表
uint32_t nsec = *(uint32_t*)&self->_imageData[off + 0x1C];
const uint8_t* p = &self->_imageData[off + 0x28];    // 实际循环里从 +0x28 之前取 RVA
 
for (uint32_t i = 0; i < nsec; i++) {
    // 反编译行为:va_delta = *(p - 8) /*RVA*/ - *(p) /*fileOffset*/
    uint32_t rva       = *(uint32_t*)(p - 8);
    uint32_t fileOff   = *(uint32_t*)(p + 0);
    uint32_t size      = *(uint32_t*)(p + 4);
 
    section_entry se;
    se.start   = fileOff;
    se.end     = fileOff + size;
    se.va_delta= (int)rva - (int)fileOff;
 
    self->_sections.push_back(se);
    p += 16;
}

3.2 LoadStreamHeaders / LoadTables

参照 HybirdCLR 原版代码,这部分读取没有做自定义化处理

LoadImageErrorCode RawImageBase::LoadStreamHeaders(uint32_t metadataRva, uint32_t metadataSize)

LoadImageErrorCode RawImageBase::LoadTables()

3.3 PostLoadStreams (解密 #1)

PostLoadStreams 在 HybirdCLR 原版代码没有给出定义,在这里它负责将 .NET 元数据的 streams 进行解密操作

其中的 instructions_cdphKey 正是在 LoadCDPHHeader 读取的

注: 伪代码中的 _cdphKey + 8 是我定义结构体时写错了 无伤大雅 ovo

alt text

该函数内 instructions (Opcodes) 和 streams 的对应关系如下

Opcodes 序号 streams 说明
1 #Strings 标识/名字池
2 #Blob 签名等二进制数据池(方法签名、字段签名、属性签名…)
3 #US User String(C# 源里写的 @"..."/普通字符串常量)
5 #~ / #- 表流(Tables Stream) #~ 为优化格式,#- 多用于编辑/调试

3.4 PostLoadTables

这段 PostLoadTables 很“vector 内存管理味儿”,作用就是:
把一个按“表2行数(rowNum)”计的辅助数组(字节数组)扩到足够大,并把新扩出来的部分清零,最后把 size 设为 rowNum。

换句话说:在读取完元数据表后,给“表2(索引 2,对应 ECMA-335 的 TypeDef 表)”准备一块 rowNum 字节的工作区,用来存标记/状态之类的一维字节表。

3.5 #US 流解密 sub_1806FF780 (解密 #2)

sub_1806FF780 的唯一引用存在于 hybridclr::managed_cdpe_vtbl,为虚表第五个元素,如下

alt text

sub_1806FF780 (DecryptUS) 可以概括为:#US(用户字符串)流里按 ECMA-335 的“压缩无符号整数”规则读出一个长度前缀,取出后续那段加密的 UTF-16 字节串,用 instructions[4] + _cdphKey 解密,然后构造成托管字符串返回。长度小于 0x1000 用栈上缓冲区,反之堆上 malloc 一块。

伪代码(等价)

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
Il2CppString* ReadUserString(managed_cdpe* self, uint32_t offset) {
    const uint8_t* p = &self->_streamUS.data[offset];
 
    // decode ECMA-335 compressed uint
    uint32_t len, adv;
    if (p[0] < 0x80)        { len = p[0];                adv = 1; }
    else if (p[0] < 0xC0)   { len = ((p[0]&0x3F)<<8)|p[1]; adv = 2; }
    else if (p[0] < 0xE0)   { len = ((p[0]&0x1F)<<24)|(p[1]<<16)|(p[2]<<8)|p[3]; adv = 4; }
    else { Hybrid::Log("bad metadata data. ReadEncodeLength fail"); /* 继续走,len 未定义 */ }
 
    if (len == 0) return System_Security_Principal_WindowsIdentity__GetTokenName_0_0();
 
    const uint8_t* src = p + adv;
    uint8_t* buf;
    bool heap = (len >= 0x1000);
    if (heap) buf = (uint8_t*)malloc(len);
    else      buf = stack_buf_0x1000;
 
    memmove_opt(buf, src, len); // sub_180A520C0
 
    DecryptData(self->instructions[4].instructions,
                self->instructions[4].count,
                self->_cdphKey,
                buf, len, 0x10);
 
    Il2CppString* s = il2cpp_string_new_utf16_0((const char16_t*)buf, (len - 1) >> 1);
 
    if (heap) free(buf);
    return s;
}

3.6 解密 TypeDef 表 sub_1807082D0 (解密 #3)

sub_1806FF780 的唯一引用存在于 hybridclr::managed_cdpe_vtbl,为虚表第八个元素

结论一句话:sub_1807082D0 读取并(按需)解密 TypeDef 表(表索引 2)中第 a3 行,把该行的 6 个列值取出写到 a2[0..5] 并返回 a2
它用一个“已解密标记”字节数组避免重复解密;列宽(2/4 字节)和列内偏移从 a1->_tableRowMetas[2].a 的模式描述里取。

做了什么(按顺序)

  1. 一次性解密该行(惰性)

    • a3-1 为 0 基行号,在 a1->unknown[12] 指向的字节数组里查看该行是否已解密:

      1
      if (!flags[a3-1]) { flags[a3-1] = 1; DecryptBlock(..., row_ptr, row_size); }
    • row_ptr = &a1->_tables[2].data[row_size * (a3-1)]

    • 解密材料:instructions[6].instructions / .count + _cdphKey,长度为整行 rowMetaDataSize

  2. 按“列模式”读取 6 列

    • 列模式基址:meta = a1->_tableRowMetas[2].a

    • 每列的描述是成对字段[width32, offset16],在 meta 上按 8 字节一组排布:

      1
      2
      3
      4
      5
      6
      0列: *(u32*)(meta+0)  宽度=24;  *(u16*)(meta+4)  偏移
      1列: *(u32*)(meta+8)          ;  *(u16*)(meta+12)
      2列: *(u32*)(meta+16)         ;  *(u16*)(meta+20)
      3列: *(u32*)(meta+24)         ;  *(u16*)(meta+28)
      4列: *(u32*)(meta+32)         ;  *(u16*)(meta+36)
      5列: *(u32*)(meta+40)         ;  *(u16*)(meta+44)
    • 读取逻辑:如果该列 width == 2 就读 uint16,否则读 uint32;写入 a2[i](扩展为 32 位保存)。

  3. 返回 a2

表 2 正好有 6 列,和 ECMA-335 的 TypeDef 一致:
Flags, TypeName, TypeNamespace, Extends(TypeDefOrRef coded index), FieldList, MethodList
其中不同列宽(2/4)由堆大小/表行数决定(字符串堆大用 4 字节,索引/编码索引随目标表行数变化)。


等价伪代码

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
DWORD* ReadTypeDefRow(managed_cdpe* self, DWORD* out6, int row1based)
{
    const uint64_t flags_base = self->unknown[12];   // 每行1字节“已解密”标记
    const uint32_t rowSize    = self->_tables[2].rowMetaDataSize;
    uint8_t* rowPtr = &self->_tables[2].data[rowSize * (row1based - 1)];
 
    // 惰性解密
    uint8_t* decFlag = (uint8_t*)(flags_base + (uint32_t)(row1based - 1));
    if (!*decFlag) {
        *decFlag = 1;
        DecryptBlock(
            (char*)self->instructions[6].instructions,
            self->instructions[6].count,
            (char*)self->_cdphKey,
            (char*)rowPtr,
            rowSize);
    }
 
    uint64_t meta = self->_tableRowMetas[2].a;
 
    auto RD = [&](int col) -> uint32_t {
        uint32_t width  = *(uint32_t*)(meta + col*8 + 0);
        uint16_t offset = *(uint16_t*)(meta + col*8 + 4);
        return (width == 2)
             ? *(uint16_t*)(rowPtr + offset)
             : *(uint32_t*)(rowPtr + offset);
    };
 
    out6[0] = RD(0);
    out6[1] = RD(1);
    out6[2] = RD(2);
    out6[3] = RD(3);
    out6[4] = RD(4);
    out6[5] = RD(5);
    return out6;
}

关键信息与推断

  • unknown[12]:上一函数里我们已确定它被初始化为长度=TypeDef 行数的一维字节数组,作用就是**“该行是否已解密”**的标志位。
  • _tableRowMetas[2].a:是“表 2”每列的宽度与偏移描述,按照 6×{u32 width; u16 offset} 组成(每组步长 8 字节)。
  • 列宽 2/4 的含义:完全符合 ECMA-335 的可变宽索引规则(小堆/大堆,或编码索引跨表行数变化)。
  • 参数a31 基的行号;a2 必须至少可容纳 6×DWORD
  • 线程安全:无锁的惰性解密 + 标志位,多线程并发读取同一行有竞态(两方可能同时解密/写标记)。
  • 越界检查:本函数不检查 a3 是否 ≤ 行数,也不检查行内偏移/宽度是否合法,安全性依赖上游元数据与模式生成的正确性。

3.7 IL Code 解密 sub_1806FAF70 (解密 #4)

sub_1806FAF70 为虚表 hybridclr::managed_cdpe_vtbl 第六个元素,也是最后一个解密函数,其伪代码如下

alt text

  • sub_1806FAF70 在通过一串门闸校验(sub_18070C160/sub_18070B380 比对)后,直接对传入的 IL Code 做解密,本函数用 instructions[7],随后进入 sub_18070F190(...) 做进一步解析/分发,更像“把已解密的 IL 交给后续解释/加载”。
  • 状态推进:sub_180716F90(a1->unknown, 1) 像是“阶段标记 = 已解密”,符合方法体加载流程中的一步。

3.8 总结

通过上面的分析,我们可以得到在 CDPH Header 中读取的 Opcodes 与实际解密的关系如下表

Opcodes 序号 解密对象 解密块大小 说明
0 保留
1 #Strings 0x100 (256) 解密 Metadata 段的 #Strings 整体
2 #Blob 0x100 (256) 解密 Metadata 段的 #Blob 整体
3 #US 0x100 (256) 解密 Metadata 段的 #US 整体
4 #US 0x10 (16) 解密 Metadata 段的 #US 中的字符串
5 #~ 0x100 (256) 解密 Metadata 段的 表流(Tables Stream) 整体
6 TypeDef typeDefRowMetaDataSize 解密 表流(Tables Stream) 中的 TypeDef 表(表索引 2)
7 IL Codes 0x10 (16) 解密方法体的 IL Codes

4. 解密算法分析 (DecryptData/DecryptBlock)

总结一句话:这是个 “指令驱动的字节块变换器”
DecryptData 负责把整段数据按固定块长切片,然后对每个块调用一次 DecryptBlock
DecryptBlockopcode(一串字节程序)逐条解释执行到 output 这个块上,每条指令用到 key[0..255] 某个字节和一些常量,对块内某个索引的字节做 XOR / 加减 / 右旋 / 交换 / 组合更新


把真实语义写清楚:

  • DecryptData(opcodes, opcodes_len, key, data, data_len, block_len)

    • opcodes/opcodes_len:指令序列及长度(之前叫 buf/buf_size,容易误解)
    • key:256 字节左右的密钥/表(索引到 0..255)
    • data/data_len:要变换(解密/加密)的字节串
    • block_len:每次处理的块大小(见你代码里多为 0x10
  • DecryptBlock(opcodes, opcodes_len, key, block, n)

    • block[0..n-1] 就地被修改

DecryptData 的逻辑(块处理器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void DecryptData(const u8* opcodes, size_t opcodes_len,
                 const u8* key,
                 u8* data, size_t data_len,
                 size_t block_len)
{
    if (!opcodes_len) return;         // 没“程序”就什么都不做
    size_t off = 0;
    while (off < data_len) {
        size_t n = block_len;
        if (data_len - off < block_len) n = data_len - off;   // 最后一个不满块
        DecryptBlock(opcodes, opcodes_len, key, data + off, (u32)n);
        off += block_len;
    }
}

要点:无链路/无状态,每块独立按同一串 opcodes 变换(更像“每块应用同一个程序”,而非标准分组密码的 CBC 等模式)。


DecryptBlock 的“虚拟机”指令集(概览)

每条指令是一个 int8_t opcode = opcodes[i]n = output_size。函数里所有索引都写成 IDX = CONST % nIDX = (key[K] ± CONST) % n,保证落在块内。

指令类型大致分为 5 类(在你的代码里分别对应大量 case):

  1. XOR

    1
    out[ C1 % n ] ^= key[K] ^ C2;

    例:case 2, 11, 12, 25, 42, 45, 46, 47, 48, ...

  2. 加/减(模 256)

    1
    out[ C1 % n ] = out[ C1 % n ] - key[K] ± C2;  // 或纯 -= key[K] / += 常量

    例:case 1, 3, 4, 5, 8, 17, 21, 23, 28, 33(纯 -= key[65]) ...

  3. 按位循环右移(单字节 ROR)

    1
    out[ C1 % n ] = ROR8(out[ C1 % n ], (key[K] + delta) & 7);

    例:case 0, 7, 9, 10, 13, 20, 29, 31, 34, 35, 36, 39, ...

    __ROR1__ 即对 8 位数做循环右移;& 7 保证 0..7 位数。

  4. 交换两位置字节LABEL_261 路径)

    1
    2
    3
    i1 = C1 % n;
    i2 = (key[K] ± C2) % n;
    swap(out[i1], out[i2]);

    例:case 19, 22, 27, 43, 44, 55, 62, 63, 75, 77, 86, 90, 94, 99, 105, 112, 122, 126, -123, -121, -120, -110, -99, -94, -84, -76, -74, -71, -69, -67, -51, -50, -48, -34, -21, -18, -14, -10, -6 ...

  5. 组合更新:先减 1 再影响另一位LABEL_23LABEL_262 路径)

    1
    2
    3
    4
    5
    i1 = C1 % n;
    i2 = (key[K] ± C2) % n;
    = out[i2] - 1;
    out[i1] -= t;     // out[i1] = out[i1] - (out[i2] - 1)
    out[i2]  = t;     // 同时把 out[i2] 自减 1

    例:case 18, 24, 26, 30, 32, 40, 58, 64, 70, 79, 81, 87, 89, 93, 97, 103, 106, 107, 110, 113, 116, 117, 119, 121, 123, 124, 126, -126, -110, -106, -102, -98, -97, -93, -92, -91, -88, -86, -80, -72, -63, -62, -40, -39, -34, -28, -26, -24, -20, -19, -17, -5 ...

由于 opcode有符号 8 位,所以 128..255 的字节在 switch 中以 -128..-1 出现(你看到那些负数的 case)。

除此之外,每条指令的常量(如 0xB330B7AF 等)在运行时都会取模 n=output_size 成为块内索引。很多常量会在小块(例如 16 字节)时映射为同一个下标,从而多次叠加到同一字节上。


代表性几条指令(逐条翻译)

  • case 0
    out[ 0xB330B7AF % n ] = ROR8(out[idx], (key[182] + 1) & 7);

  • case 2
    out[ 0x8CC11BA1 % n ] ^= key[224] ^ 0x23;

  • case 19(交换)
    i1 = 0x02BF6A70 % n; i2 = (key[115] - 1335899030) % n; swap(out[i1], out[i2]);

  • case 18(组合更新)
    i1 = 0x8AAEBD71 % n; i2 = (key[112] + 259923827) % n; t = out[i2]-1; out[i1]-=t; out[i2]=t;

  • case 61
    out[0x3BDE5592 % n] = ROR8(out[idx], (key[13] - 4) & 7);

……(其余同理,都是以上 5 大模板的实例)


重要性质与安全性提示

  • 块独立、指令重放:每个块独立变换,且用同一套 opcodes无随机化/反馈
  • 操作线性/弱非线性XOR/加减/旋转/交换 这类操作对攻击者非常友好(尤其是短块、常量取模后重复击中同一索引时)。
  • 密钥索引固定:每条指令使用 key[固定位置];只要拿到 opcodeskey,就完全可逆。
  • 实现方便:按上面的 5 个模板就能写出可运行的参考实现/解密器。

5. 总结

首先感谢我的朋友 @66hh ( 52pojie - zbby | Github - 66hh ),在对解密逻辑的分析以及解密器的编写中都做出了重大贡献,如果没有他的协助,该文章是不可能出现的 ovo

其次,在文章开头说过,本文所分析的解密是由代码哲学所开发的 Obfuz 所生成的,根据该项目的 ReadMe,解密用的 VM,即上文所分析的 DecryptBlock,是随机生成的,故该文章无法保证该解密方法适用于所有使用该混淆器的游戏

最后,来一张 dnSpy 解析解密后的 HotPatch DLL 作为结尾吧 ovo

解密器不公开 别私信问我了 (

alt text


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 104
活跃值: (7285)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
2025-9-15 14:23
0
游客
登录 | 注册 方可回帖
返回