解密LockCrypt勒索软件
字数 969 2025-08-20 18:17:42

LockCrypt勒索软件解密技术详解

1. LockCrypt勒索软件概述

LockCrypt(又称EncryptServer2018)是2017年中发现的勒索软件家族,至今仍然活跃。该勒索软件使用了一种定制的加密方法,而非标准的Windows API加密方式。

2. 加密机制分析

2.1 加密流程概述

LockCrypt的加密过程分为两个阶段:

  1. 第一阶段:使用长度为12500字节的循环密钥进行初步加密
  2. 第二阶段:使用长度为25000字节的循环密钥进行进一步加密

2.2 加密函数等效Python代码

def grouper(iterable, n, fillvalue=None):
    """将可迭代对象分组为大小为n的组"""
    args = [iter(iterable)] * n
    return itertools.izip_longest(fillvalue=fillvalue, *args)

def rol(val, n, width):
    """模拟x86 rol指令"""
    return (val << n) & ((1 << width) - 1) | \
           ((val & ((1 << width) - 1)) >> (width - n))

def encrypt(key, plain):
    size = len(plain) - 2
    enc = io.BytesIO(plain)
    
    # 第一阶段
    key_cyclic = grouper(itertools.cycle(key), 4)
    for _ in xrange(0, size & (~0x3), 2):
        # 获取下一个密钥dword
        k = "".join(key_cyclic.next())
        k = struct.unpack("<I", k)[0]
        
        # 获取下一个数据dword
        d = enc.read(4)
        d = struct.unpack("<I", d)[0]
        
        # 执行XOR并写回数据
        e = k ^ d
        e = struct.pack("<I", e)
        enc.seek(-4, os.SEEK_CUR)
        enc.write(e)
        enc.seek(-2, os.SEEK_CUR)
    
    # 第二阶段
    enc.seek(0, os.SEEK_SET)
    key_cyclic = grouper(itertools.cycle(key), 4)
    for i in xrange(0, size & (~0x3), 4):
        # 获取下一个密钥dword
        k = "".join(key_cyclic.next())
        k = struct.unpack("<I", k)[0]
        
        # 获取下一个数据dword
        d = enc.read(4)
        d = struct.unpack("<I", d)[0]
        
        # 循环左移5位
        e = rol(d, 5, 32)
        # XOR操作
        e = e ^ k
        # 交换字节顺序
        e = struct.pack(">I", e)
        # 写回数据
        enc.seek(-4, os.SEEK_CUR)
        enc.write(e)
    
    return enc.getvalue()

def encrypt_file(key, file):
    # 密钥长度为25000字节
    # 跳过前4个字节
    file.seek(4, os.SEEK_SET)
    # 加密文件的前1MB内容(减去4字节)
    plain_data = file.read(0x100000 - 4)
    enc_data = encrypt(key, plain_data)
    file.seek(4, os.SEEK_SET)
    file.write(enc_data)
    # 文件其余部分保持不变

2.3 加密过程关键点

  1. 每个明文位与3个密钥位进行XOR运算
  2. 如果撤销第二阶段中的位移操作,两个阶段都可以描述为使用25000字节循环密钥的流加密
  3. 前4个字节保持不加密
  4. 只加密文件的前1MB内容,其余部分保持不变

3. 解密方法

3.1 恢复流密钥(Stream Key)

要解密文件,首先需要恢复流密钥:

key_len = 25000

def ror(val, n, width):
    """模拟x86 ror指令"""
    return ((val & ((1 << width) - 1)) >> n) | \
           (val << (width - n) & ((1 << width) - 1))

def recover_stream_key(plain, enc, idx):
    assert len(plain) == len(enc)
    assert len(plain) >= 4 + idx + key_len
    
    plain = io.BytesIO(plain)
    enc = io.BytesIO(enc)
    assert plain.read(4) == enc.read(4)
    
    plain.seek(idx & (~3), os.SEEK_CUR)
    enc.seek(idx & (~3), os.SEEK_CUR)
    
    stream_key = io.BytesIO()
    for i in xrange(0, key_len + (idx % 4), 4):
        # 读取下一个明文dword
        p = plain.read(4)
        p = struct.unpack("<I", p)[0]
        
        # 读取下一个加密dword并撤销位移操作
        e = enc.read(4)
        e = struct.unpack(">I", e)[0]
        e = ror(e, 5, 32)
        
        # XOR明文和规范化后的加密dword
        k = p ^ e
        k = struct.pack("<I", k)
        
        # 将流密钥dword写入恢复的流密钥
        if i == 0:
            stream_key.write(k[idx % 4:])
        elif i < key_len:
            stream_key.write(k)
        else:
            stream_key.write(k[:-idx % 4])
    
    stream_key = stream_key.getvalue()
    assert len(stream_key) == key_len
    return stream_key

3.2 使用流密钥解密文件

def decrypt(stream_key, enc):
    size = len(enc) - 2
    plain = io.BytesIO(enc)
    stream_key_cyclic = grouper(itertools.cycle(stream_key), 4)
    
    for i in xrange(0, size & (~3), 4):
        # 读取下一个流密钥dword
        sk = "".join(stream_key_cyclic.next())
        sk = struct.unpack("<I", sk)[0]
        
        # 读取下一个加密dword并撤销位移操作
        e = plain.read(4)
        e = struct.unpack(">I", e)[0]
        e = ror(e, 5, 32)
        
        # XOR规范化后的加密dword与流密钥dword以恢复明文dword
        p = e ^ sk
        p = struct.pack("<I", p)
        plain.seek(-4, os.SEEK_CUR)
        plain.write(p)
    
    return plain.getvalue()

def decrypt_file(stream_key, file):
    assert len(stream_key) == key_len
    # 跳过前4个字节
    file.seek(4, os.SEEK_SET)
    # 解密文件的前1MB内容(减去4字节)
    enc_data = file.read(0x100000 - 4)
    plain_data = decrypt(stream_key, enc_data)
    file.seek(4, os.SEEK_SET)
    file.write(plain_data)
    # 文件其余部分保持不变

3.3 恢复原始密钥(Original Key)

要完全解密文件,需要恢复原始密钥:

def k_for_i(i):
    """返回与流密钥位i相关的原始密钥位索引"""
    i_dword = i >> 5  # dword索引
    i_offset = i % 32  # dword中的位索引
    i = i + key_bitlen
    
    k = []
    k.append((i_dword << 6) + i_offset)
    
    if i_offset < 16:
        k.append(k[0] - 16)
    else:
        k.append(k[0] + 16)
    
    k.append((i_dword << 5) + ((i_offset + 5) % 32))
    
    return [x % key_bitlen for x in k if x >= 0]

def gen_equations(idx):
    """生成原始密钥和流密钥之间的转换方程"""
    A_i_j_s = []
    for i in xrange(idx << 3, (idx << 3) + key_bitlen):
        A_i_j_s.append(k_for_i(i))
    return A_i_j_s

使用SageMath解决线性方程组:

# 设置参数
stream_key_idx = 25000  # 恢复的流密钥索引
stream_key_path = "key-stream-idx-25000.bin"  # 流密钥文件路径
original_key_path = "key.bin"  # 原始密钥输出路径

import itertools
import struct

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return itertools.izip_longest(fillvalue=fillvalue, *args)

def str2bits(s):
    """将字符串转换为位列表"""
    bits = []
    for dword in grouper(s, 4):
        dword = "".join(dword)
        dword = struct.unpack("<I", dword)[0]
        bits += [(dword >> i) & 1 for i in xrange(32)]
    return bits

def bits2str(bits):
    """将位列表转换为字符串"""
    s = []
    for dword_bits in grouper(bits, 32):
        dword = 0
        for i, bit in enumerate(dword_bits):
            dword = dword | (bit << i)
        s.append(struct.pack("<I", dword))
    return "".join(s)

# 读取流密钥
with open(stream_key_path) as f:
    stream_key = f.read()
assert len(stream_key) == 25000

# 将流密钥转换为位向量
stream_key_bits = str2bits(stream_key)
Y = vector(GF(2), 200000, stream_key_bits)

# 创建方程矩阵
A_i_j_s = gen_equations(stream_key_idx)
A = matrix(GF(2), 200000, sparse=True)
for i, A_i_j in enumerate(A_i_j_s):
    for j in A_i_j:
        A[i, j] = 1

# 解方程恢复原始密钥
X = A.solve_right(Y)
key_bits = [int(x) for x in X.list()]
key = bits2str(key_bits)

# 保存原始密钥
with open(original_key_path, 'wb') as f:
    f.write(key)

4. 解密步骤总结

  1. 恢复明文数据

    • 获取至少25010字节的未加密文件或明文文件
    • 建议在另一台机器上安装相同版本的勒索软件,比较加密DLL文件和原始文件
  2. 恢复流密钥

    • 安装Python 2.7
    • 使用recover_stream_key.py脚本恢复流密钥(建议使用idx=25000)
  3. 恢复原始密钥

    • 安装SageMath
    • 使用SageMath Jupyter Notebook执行上述代码(耗时20分钟到几小时)
  4. 解密文件

    • 使用decryptor.py脚本解密加密的文件

5. 注意事项

  1. 前2个加密字节(原始文件的字节4:6)只与2个密钥位进行XOR运算,因此:

    • 解密剩余字节需要使用idx≥2的流密钥
    • 解密前2个字节需要使用idx=0的流密钥
  2. 如果原始文件长度≠2 (mod 4),在encrypt()函数中明文的长度len(plain)≠0 (mod 4),因此在第一阶段最后一个循环中只对文件结尾的1-2个字节进行解密。这些字节的明文位只与1个密钥位进行XOR运算,因此无法直接解密。

  3. 如果已知文件长度n≥m,可以解密长度为m的文件。

LockCrypt勒索软件解密技术详解 1. LockCrypt勒索软件概述 LockCrypt(又称EncryptServer2018)是2017年中发现的勒索软件家族,至今仍然活跃。该勒索软件使用了一种定制的加密方法,而非标准的Windows API加密方式。 2. 加密机制分析 2.1 加密流程概述 LockCrypt的加密过程分为两个阶段: 第一阶段 :使用长度为12500字节的循环密钥进行初步加密 第二阶段 :使用长度为25000字节的循环密钥进行进一步加密 2.2 加密函数等效Python代码 2.3 加密过程关键点 每个明文位与3个密钥位进行XOR运算 如果撤销第二阶段中的位移操作,两个阶段都可以描述为使用25000字节循环密钥的流加密 前4个字节保持不加密 只加密文件的前1MB内容,其余部分保持不变 3. 解密方法 3.1 恢复流密钥(Stream Key) 要解密文件,首先需要恢复流密钥: 3.2 使用流密钥解密文件 3.3 恢复原始密钥(Original Key) 要完全解密文件,需要恢复原始密钥: 使用SageMath解决线性方程组: 4. 解密步骤总结 恢复明文数据 : 获取至少25010字节的未加密文件或明文文件 建议在另一台机器上安装相同版本的勒索软件,比较加密DLL文件和原始文件 恢复流密钥 : 安装Python 2.7 使用 recover_stream_key.py 脚本恢复流密钥(建议使用idx=25000) 恢复原始密钥 : 安装SageMath 使用SageMath Jupyter Notebook执行上述代码(耗时20分钟到几小时) 解密文件 : 使用 decryptor.py 脚本解密加密的文件 5. 注意事项 前2个加密字节(原始文件的字节4:6)只与2个密钥位进行XOR运算,因此: 解密剩余字节需要使用idx≥2的流密钥 解密前2个字节需要使用idx=0的流密钥 如果原始文件长度≠2 (mod 4),在 encrypt() 函数中明文的长度len(plain)≠0 (mod 4),因此在第一阶段最后一个循环中只对文件结尾的1-2个字节进行解密。这些字节的明文位只与1个密钥位进行XOR运算,因此无法直接解密。 如果已知文件长度n≥m,可以解密长度为m的文件。