解密LockCrypt勒索软件
字数 969 2025-08-20 18:17:42
LockCrypt勒索软件解密技术详解
1. LockCrypt勒索软件概述
LockCrypt(又称EncryptServer2018)是2017年中发现的勒索软件家族,至今仍然活跃。该勒索软件使用了一种定制的加密方法,而非标准的Windows API加密方式。
2. 加密机制分析
2.1 加密流程概述
LockCrypt的加密过程分为两个阶段:
- 第一阶段:使用长度为12500字节的循环密钥进行初步加密
- 第二阶段:使用长度为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 加密过程关键点
- 每个明文位与3个密钥位进行XOR运算
- 如果撤销第二阶段中的位移操作,两个阶段都可以描述为使用25000字节循环密钥的流加密
- 前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. 解密步骤总结
-
恢复明文数据:
- 获取至少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的文件。