自动化解密 .NET XORStringsNet 混淆器
字数 2624
更新时间 2026-03-12 13:22:13

自动化解密 .NET XORStringsNet 混淆器 教学文档

一、 研究背景与目标

XORStringsNet 是一个开源的 .NET 字符串混淆工具,因其易用性(已编译、一键混淆),被如 Agent Tesla 和 Redline Stealer 等知名 .NET 恶意软件家族广泛使用,以隐藏恶意字符串。

本教学文档旨在解析 XORStringsNet 混淆器的原理,并最终提供一个脱离人工干预、可自动化从二进制文件中提取并解密字符串的 Python 脚本的实现方法。这将有助于安全研究人员对大量使用此混淆器的样本进行批量分析。

二、 加密算法与结构分析

2.1 加密数据结构

混淆后的字符串在文件中的存储结构是核心,其格式为:
[加密数据长度(4字节,小端序)] [XOR解密密钥(4字节)] [加密数据(N字节)]
这个结构会在文件中连续重复出现,形成“字符串池”。

2.2 加密/解密算法

加密算法的核心逻辑(C#伪代码风格)如下:

  1. 将待加密字符串转换为字节数组 buffer
  2. 获取一个32位整数密钥 key,但注意历史版本的一个关键Bug:在解密时,程序只使用了 key 的最低有效字节(即 key & 0xFF),这极大地削弱了加密强度,使得暴力破解成为可能。
  3. 对字节数组进行“对称交换异或”操作:
    • 定义两个指针,i 从数组头部开始,j 从数组尾部开始。
    • 核心三步操作(对每个对称位置):
      buffer[i] ^= buffer[j];
      buffer[j] ^= (buffer[i] ^ (byte)key);
      buffer[i] ^= buffer[j];
    • 如果数组长度为奇数,则对中间的字节执行:buffer[mid] ^= (byte)key;

简化理解:算法本质是“带密钥的字节数组逆序异或混淆”。一个更简单的解密实现思路是:先执行上述复杂的逆操作,或者直接将解密后的字节数组进行反转(reverse),再对每个字节用密钥的低8位进行异或。

2.3 定位加密数据特征

  1. Yara规则特征:XORStringsNet作者提供了检测Yara规则,其中包含一个特征字节码:{ 06 1E 58 07 8E 69 FE17 }
  2. 代码特征:在反编译的代码中,解密字符串的函数通常会包含一个初始化的空结构体和一个字段。调用解密函数时,可能会看到 cpblk 指令用于内存块复制。

三、 手动与半自动解密方法

在编写全自动工具前,可通过以下方法手动分析,这对理解过程至关重要。

3.1 使用 dnSpy 和 de4dot 的半自动方法(作者原方法)

  1. 定位解密函数:在反编译工具中,找到被频繁调用的、参数为一个大整数(Token)、返回类型为 string 的函数。该函数内部包含上述交换异或逻辑。
  2. 获取函数Token:在dnSpy中查看该函数的元数据Token(格式如0x06000000)。
  3. 委托de4dot解密:使用de4dot的命令行工具,指定委托调用该Token对应的函数来解密字符串。
    de4dot.exe <样本文件> --strtyp delegate --strtok 0x06000000
    
    此方法需要实际运行程序(或在安全环境模拟运行),不适合在物理机上进行大规模静态自动化分析。

3.2 手动解密验证

  1. 定位数据:在反编译器中点击加密字符串的引用,找到其RVA(相对虚拟地址)或文件偏移。
  2. 解析结构:按照 长度(4字节) -> 密钥(4字节) -> 数据(N字节) 的结构拆分数据。
  3. 编写解密脚本:根据上述算法实现解密函数。可先用CyberChef等工具验证。
    Python解密函数示例(模拟原始C#逻辑)
    def decrypt_xorstringsnet(cipher_data: bytes, key: int) -> str:
        buf = bytearray(cipher_data)
        n = len(buf)
        key_byte = key & 0xFF  # 关键:只取密钥的第一个字节
    
        for i in range(n // 2):
            j = n - 1 - i
            buf[i] ^= buf[j]
            buf[j] ^= (buf[i] ^ key_byte)
            buf[i] ^= buf[j]
    
        if n % 2 != 0:
            buf[n // 2] ^= key_byte
    
        return bytes(buf).decode('utf-8', errors='ignore')
    
    简化版解密函数(反转+异或)
    def decrypt_simple(cipher_data: bytes, key: int) -> str:
        # 将字节数组反转,然后每个字节与密钥低8位异或
        decrypted = bytearray(reversed(cipher_data))
        key_byte = key & 0xFF
        for i in range(len(decrypted)):
            decrypted[i] ^= key_byte
        return decrypted.decode('utf-8', errors='ignore')
    

四、 全自动化解密方案设计与实现

手动方法的瓶颈在于需要人工定位加密字符串表在文件中的起始偏移量。本方案的核心在于自动化定位这个偏移量

4.1 方案选择:暴力搜索大体积结构体

经过评估,放弃“统计函数调用次数”和“寻找特征签名”两种方案,原因如下:

  • 方案一(统计调用)需模拟执行,不适用于静态分析。
  • 方案二(特征签名)面临困难:.NET 的类型定义(TypeDef)与数据在文件中的物理地址(RVA)是分离的,通过类型定义关联到实际数据地址需要复杂的跨表查询,实现复杂且不稳定。

选定方案三思路
在 .NET 程序中,硬编码的大型数据(如加密字符串表)通常会被编译器放置在一个具有显式布局 (tdExplicitLayout) 和固定大小 (ClassSize) 的类/结构体中。因此,文件中最庞大的、具有显式布局的结构体,极有可能就是我们要找的加密字符串表

4.2 实现步骤

使用 dnfile 库解析 .NET PE 文件。

  1. 定位最大结构体:遍历所有类型定义 (TypeDef),找到标志位包含 tdExplicitLayout 的类,并获取其 ClassLayout 中定义的 ClassSize。记录其中最大的 ClassSize 值。
  2. 搜索匹配的地址区间:遍历所有字段的初始数据地址(通过 FieldRVA 表)。在内存中,字段的地址是连续的。如果发现两个字段的 RVA 之间的差值,正好等于上一步找到的最大 ClassSize,那么起始的 RVA 就极有可能是加密字符串表的起始地址。
  3. 提取并解密:根据找到的起始地址(需转换为文件偏移),读取数据,并按照 [长度][密钥][数据] 的结构循环解析,应用解密函数。

4.3 核心Python代码实现

以下是自动化脚本的核心代码框架:

import struct
import dnfile

def xor_decrypt_simple(data: bytes, key: int) -> str:
    """简化版解密函数:反转后异或"""
    key_byte = key & 0xFF
    decrypted = bytearray(reversed(data))
    for i in range(len(decrypted)):
        decrypted[i] ^= key_byte
    return decrypted.decode('utf-8', errors='ignore')

def auto_decrypt_xorstringsnet(file_path: str):
    print(f"[*] 分析文件: {file_path}")
    pe = dnfile.dnPE(file_path)
    decrypted_strings = []

    # 步骤1: 找到最大的显式布局类大小
    max_class_size = 0
    if hasattr(pe.net, 'mdtables') and pe.net.mdtables.ClassLayout:
        for layout in pe.net.mdtables.ClassLayout.rows:
            # 获取对应的类型定义
            type_def = pe.net.mdtables.TypeDef.rows[layout.Parent.row_index - 1]
            if type_def.Flags.tdExplicitLayout:
                if layout.ClassSize > max_class_size:
                    max_class_size = layout.ClassSize

    if max_class_size == 0:
        print("[-] 未找到显式布局的类。")
        return decrypted_strings

    print(f"[+] 最大显式布局类大小: 0x{max_class_size:X}")

    # 步骤2: 在 FieldRVA 中寻找地址间隔等于该大小的连续区域
    target_start_rva = None
    field_rvas = []
    if hasattr(pe.net, 'mdtables') and pe.net.mdtables.FieldRVA:
        for field_rva in pe.net.mdtables.FieldRVA.rows:
            field_rvas.append(field_rva.RVA)
    
    field_rvas.sort()
    for i in range(1, len(field_rvas)):
        if field_rvas[i] - field_rvas[i-1] == max_class_size:
            target_start_rva = field_rvas[i-1]
            print(f"[+] 疑似字符串表起始RVA: 0x{target_start_rva:X}")
            break

    if target_start_rva is None:
        print("[-] 未找到符合大小特征的连续RVA区域。")
        # 备选方案:可以尝试在文件的.data节等常见数据区进行暴力搜索匹配`长度(4字节)`的结构
        return decrypted_strings

    # 步骤3: 将RVA转换为文件偏移
    target_offset = pe.get_offset_from_rva(target_start_rva)
    
    # 步骤4: 读取并解析字符串池
    with open(file_path, 'rb') as f:
        f.seek(target_offset)
        while True:
            # 读取长度
            len_bytes = f.read(4)
            if len(len_bytes) < 4:
                break
            str_len = struct.unpack('<I', len_bytes)[0]  # 小端序
            if str_len == 0 or str_len > 0x10000:  # 简单合法性检查
                break
            # 读取密钥
            key_bytes = f.read(4)
            if len(key_bytes) < 4:
                break
            key = struct.unpack('<I', key_bytes)[0]
            # 读取加密数据
            encrypted_data = f.read(str_len)
            if len(encrypted_data) < str_len:
                break
            # 解密
            decrypted_str = xor_decrypt_simple(encrypted_data, key)
            if decrypted_str and len(decrypted_str.strip()) > 0:
                decrypted_strings.append(decrypted_str)
                print(f"[+] 解密: {decrypted_str[:50]}...")  # 打印前50字符

    print(f"[*] 共解密 {len(decrypted_strings)} 个字符串。")
    return decrypted_strings

# 使用示例
if __name__ == "__main__":
    strings = auto_decrypt_xorstringsnet("malware_sample.exe")

五、 总结与注意事项

  1. 版本差异:务必注意密钥使用方式的Bug(只使用低8位)。本教学针对此常见情况。如果样本使用修复后的版本,需调整解密函数,使用完整的4字节密钥进行更复杂的异或操作。
  2. 健壮性:自动化脚本应增加更多的错误检查和边界条件处理,例如对str_len的合理性校验,以及当一种搜索方法失效时的备选方案(如在.data节中按结构特征暴力搜索)。
  3. 工具依赖:自动化脚本依赖于 dnfile 库来解析.NET元数据,请确保在Python环境中安装 (pip install dnfile)。
  4. 应用场景:此方法适用于静态批量分析,可在不运行样本的情况下提取解密后的字符串,极大地提升了分析效率。
相似文章
相似文章
 全屏