前情提要:LockBit 是自2019年以来最活跃的勒索软件组织之一,采用双重勒索策略并具备高度专业化运营能力。尽管在2024年遭遇国际打击并被揭露核心身份,该组织仍迅速恢复并于2025年推出LockBit 4.0版本。2025年5月,其运营数据库泄露暴露了大量内部细节。详情可见【独家揭秘】LockBit 4.0 解密器失效真相|首发谈判日志+万字深剖:发展脉络 · TTP 演进 · 2025最新 IOC。
1.背景
客户在遭遇勒索事件后因业务中断极为焦急,遂委托第三方公司代为与黑客进行谈判,并按照对方要求支付了巨额赎金,成功获取了解密器。然而在实际使用过程中发现,该解密器恢复效果极差:大量文件,尤其是体积较大的数据库文件未能成功解密,部分已“解密完成”的文件也严重损坏,无法正常打开或使用。第三方公司多次尝试再次联系黑客寻求技术支持,但对方已彻底失联,未再回应,导致数据恢复陷入困境。最终,第三方公司在无力解决问题的情况下,将客户引荐至我们团队,希望借助专业技术手段修复受损文件并挽回关键数据。
我们不建议受害者向黑客支付赎金购买所谓的恢复工具,因为在整个交易过程中主导权完全掌握在黑客手中,支付手段多为比特币等匿名加密货币,既难以追溯也无法保障交易安全,黑客可能在收款后拒绝提供解密工具,或者故意发送错误密钥,导致数据依然无法恢复,受害者也缺乏有效的申诉途径和法律保障,因此支付赎金不仅不能确保数据恢复,详情可参考文章【病毒分析】交了赎金也无法恢复--针对国内某知名NAS的LVT勒索病毒最新分析。
为协助客户最大程度挽回损失,我们在获取原始解密器后,第一时间对其进行了逆向分析,最终发现其在处理大文件时存在明显逻辑缺陷。通过修复该问题,我们成功恢复了大量此前无法解密的关键数据文件。本文将重点介绍此次解密器逆向分析的关键过程与修复技术细节。
1.1聊天记录展示
以下为该客户与 LockBit 黑客组织在赎金谈判期间的部分聊天记录截图。该谈判记录来源于2025年5月 LockBit 团伙被执法机构打击后泄露的大量运营数据,我们通过对其中的谈判日志进行梳理和还原,成功定位到该客户的完整交互过程。目前,该谈判页面已被黑客关闭,原始对话 ID 无法通过公开入口访问。我们已将相关内容导入至 SAR 勒索情报平台的“谈判分析”模块,并对其语义内容进行归类标注,平台也集成了“一键翻译”功能,帮助用户高效、直观地理解英文沟通中的关键威胁、推脱话术及赎金策略。
[图1] 客户支付赎金
[图2] 客户反馈使用黑客提供的解密器解密后显示很多文件无法修复,解密器存在问题
[图3] 客户发给黑客恢复异常文件,黑客表示从未遇到该情况,开始推脱问题
[图4] 黑客持续拖延
[图5]黑客提供了恢复文件,但客户验证后非所需文件
[图6]客户反馈问题,黑客长时间未回复
[图7]客户申请退款,黑客不予理会
在支付赎金后,由于解密效果严重不达预期,第三方公司曾多次尝试向黑客申请退款,但对方已不再回应,彻底失联。至此,客户初次委托的这家负责代谈和支付赎金的公司也已无计可施。最终,第三方公司将客户引荐至我们团队。我们在详细分析加密逻辑与解密器结构后,成功修复了解密逻辑中的关键问题,帮助客户完整恢复了数万个受影响的重要文件。
下文将对该解密器的技术机制进行深入分析,并在第四章我们会公开发布针对此次事件免费的恢复工具。如果您也遭遇了类似问题,欢迎联系我们协助处理——更重要的是,面对勒索攻击切勿盲目缴纳赎金,应在专业团队协助下开展分析研判与恢复评估。这次能够挽回,更多时候则可能再无机会。
1.2 谈判模式解析
角色层级 | 关键职责 | 典型细节 | 价值/收益 |
---|---|---|---|
核心团队(Boss) | - 开发/维护加密器 - 搭建 DLS 泄露站、谈判后台 - 统一托管密钥 & 赎金钱包 | - 777 USD BTC 门槛才能拿到后台账号 - 技术服务费≈赎金 20% | “品牌方”——控制生态规则、抽成 |
Affiliate(加盟攻击者) | - 渗透、横移、窃取数据 - 部署勒索程序 | - 自带 0day/钓鱼资源 - 享受全套自动化控制台 | “渠道商”——拿 80% 赎金 |
Negotiator(谈判员) | - 受害者沟通、催款 - 调整赎金策略 | - 常见口头禅:Let me ask my boss | “前台客服”——加速回款 |
辅助服务商 | - 洗钱、流量租赁、初级代理 | - 兑换所 / Mixer / Bulletproof VPS | “外包供应链”——降低风险 |
在对话中可以看到黑客回复“我去告诉老板”,许多读者可能会疑惑:“黑客还有老板?”事实上,这句话揭示了LockBit家族背后的运作模式——RaaS(勒索软件即服务)。在RaaS模式下,与受害者沟通的通常是谈判人员或附属成员,他们并不是核心操盘者,而是“加盟代理”。整个组织结构中,核心团队负责开发与维护勒索软件本体、构建数据泄露站点、谈判门户等基础设施,并提供技术支持;而附属成员负责前期渗透、横向移动、数据窃取及部署勒索程序。两者之间按照约定比例分成收益,比如LockBit的核心团队会抽取约20%的赎金作为“技术服务费”。
据泄露的聊天记录显示,想要成为LockBit的附属成员并获取其后台权限,需支付约777美元的比特币费用。因此,这类“黑客集团”并非传统意义上的单人作案,而是高度产业化、分工明确的黑色供应链体系,“老板”也确实存在,只不过藏在幕后的平台操控者而已,关于运营模式与策略可参考【独家揭秘】LockBit 4.0 解密器失效真相|首发谈判日志+万字深剖:发展脉络 · TTP 演进 · 2025最新 IOC。
1.3 关键时间线
时间(UTC+8) | 事件节点 | 说明 |
---|---|---|
2025/3/24 6:31 | 客户委托第三方与 LockBit 谈判支付赎金 | [图1] |
2025/3/25 14:55 | 解密后显示很多文件无法修复,解密器存在问题 | [图2] |
2025/03/24~03/30 | 黑客多次以「我去问老板」推脱,未给解决方案 | [图3] |
2025/03/27~04/02 | 双方持续沟通,黑客让客户持续等待 | [图4] |
2025/4/3 1:11 | 黑客提供100GB补偿数据链接,但非客户所需文件 | [图5] |
2025/4/9 4:28 | 客户反馈问题,黑客频繁失联 | [图6] |
2025/4/21 6:42 | 客户要求退款,黑客不予理会 | [图7] |
2025/4/23 | 第三方将客户转介至 Solar 团队 | |
2025/4/24 | Solar 团队收到样本完成逆向,定位逻辑缺陷 | |
2025/4/26 | 解决BUG问题,定制恢复工具 | |
2025/4/27 | 完成所有问题文件修复,客户检测文件无异常 | |
2025/6/27 | 公众号发布针对此次事件恢复工具 |
2.样本文件基础信息
2.1 样本文件基本信息
描述 | 说明 |
---|---|
名称 | lbg32_decryptor.exe |
大小 | 60,416 bytes |
加密后缀 | .JDXsjci2V |
操作系统 | windows |
架构 | 80386 |
类型 | exe |
字节序 | 小端 |
MD5 | 4ce976e0a424227a374d485a71924bda |
SHA1 | 29730aa767167746c67c137521fbec46af5ebbfe |
SHA256 | 0a582aae66ecbb7dac6437fb3090061df8bcc160e35e5c588258e3d79b41f59f |
2.2 勒索信
2.3 其他注意事项
加密器和解密器中的密钥不匹配,但是算法相同,替换密钥即可解密.32位为通用解密器(用32位和64位加密的文件都可以被该解密器解密),在客户主机上有部分文件无法解密.
3.解密流程图
3.1 主要行为流程图
3.2 解密算法流程图
4.逆向分析
脱壳方法十分简单,为简单压缩壳,找到oep下断即可.在此不再赘述.
4.1 遍历本地驱动器
先获取本地驱动器属性,存入数组,并挂载隐藏分区.
4.2 遍历共享驱动器
遍历共享驱动器,并加入队列.
4.3 本地驱动器队列
遍历本地驱动器,跳过cd-rom驱动器,测试共享驱动器是否可连通.
4.4 扫描arp缓存
arp缓存中的主机也会被扫描,并解密.
4.5 文件解密
读取本地驱动器队列.进入解密函数.阻塞解密每个驱动器中的文件.
使用windows api遍历目录,根据白名单决定是否写入目录双向链表.
不处理的目录名如下:
[".","..","Windows","$Recycle.Bin","AllUsers","Boot","chocolatey","Microsoft Visual Studio","System Volume Information"]
不处理的文件名包含字符串如下:
["exe","lnk","dll","cpl","sys","Restore-My-Files"]
创建文件句柄,读取文件,判断大小,决定是否处理该文件.解密校验用字符串"FBIsosite",进行第一次校验,并拷贝黑客私钥.
代码内联造成此处代码十分臃肿.此处是x25519密钥交换的主要算法,即标量乘法.
计算自定义hash.
加密器中的hash算法,与解密器中的完全相同,故引用注释.
黑客使用了openssl库中的poly1305算法,和源码对比可得.
此处同样有部分函数内联.
引用加密器分析的部分操作.
salt定义:
BYTE salt[] = { 32,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0 };
解密加密文件所用的密钥.此处密钥是正确的,但是使用poly1305校验不通过,原因是文件部分损坏,见实验部分.
后面为对于文件的分块解密,解密算法和加密算法完全相同(因为chacha20是异或流加密).
以下内容摘自加密器分析:
然后为共享驱动器创建生产者-消费者模式的遍历路径和解密文件两个线程,使用同样的方法解密每一个驱动器中的文件.
解密线程.
4.6 重启服务
最后根据hash比对得出被停止的服务(与加密器中停止的服务相同),并启动之.
4.7 关于解密器bug的实验
从客户提供的样本文件来看,部分数据在解密过程中仍无法成功恢复。同时,由于客户仅提供了解密器,尚未获取到对应的加密器样本,因此我们在尚未排查提取加密器的情况下,基于加解密算法互为逆过程的前提,对现有的脱壳修复版本(已去除动态基址)进行实验验证,以进一步排查解密失败的根本原因.
4.7.1 实验一
4.7.1.1 目的
判断加密器和解密器算法是否互逆.
4.7.1.2 第一步
使用python生成x25519的一对密钥对.
4.7.1.3 第二步
将公钥和私钥硬编码分别写入加密器和解密器的对应地址.
加密器. 公钥foa: 0x1c390
解密器. 私钥foa: 0x1e4a0
4.7.1.4 第三步
保存文件并运行加密器,等待加密完成.
4.7.1.5 第四步
运行解密器.成功扫描并解密文件.
4.7.1.6 结论
加解密器的算法没有问题.问题只能出在数据损坏上.
4.7.2 实验二
4.7.2.1 目的
获得被加密文件中数据损坏的位置.
4.7.2.2 第一步
解压样本包至虚拟机中勒索病毒可访问到的路径.选定一个文件.
img
选择此文件.
4.7.2.3 第二步
物理机打开解密器源码.
找到此函数,根据解密流程图给程序分段打点.
打点后的解密主函数如下:
DWORD dec_a_file(const wchar_t* from_path)
{
wchar_t dst_path[MAX_PATH+1] = { 0 };
wcscpy(dst_path, from_path);
*(PathFindExtension(dst_path)) = 0;
HANDLE fp = CreateFile(from_path,GENERIC_READ,NULL,NULL,OPEN_EXISTING,NULL,NULL);
if (INVALID_HANDLE_VALUE == fp)
{
return0;
}
LARGE_INTEGER tmp_fs;
GetFileSizeEx(fp, &tmp_fs);
ULONGLONG file_size = tmp_fs.QuadPart - 92;
tmp_fs.QuadPart = -92;
BYTE info[92] = { 0 };
DWORD read_bytes = 0;
SetFilePointerEx(fp, tmp_fs, NULL, FILE_END);
ReadFile(fp, info, 92, &read_bytes, NULL);
if (!is_file(info) || (read_bytes != 92))
{
CloseHandle(fp);
return0;
}
HANDLE fp2 = CreateFile(dst_path, GENERIC_WRITE,NULL, NULL, CREATE_ALWAYS, NULL, NULL);
if (INVALID_HANDLE_VALUE == fp2)
{
CloseHandle(fp);
return0;
}
BYTE* file_pub = info + 60;
BYTE shared_key[32] = { 0 };
curve25519_donna(shared_key, hacker_priv, file_pub);
//1.密钥交换输出点
printf("shared_key: ");
for (int i = 0; i < 32; i++)
{
printf("%02X", shared_key[i]);
}
printf("\n\n");
BYTE output[32] = { 0 };
BYTE output2[32] = { 0 };
BYTE ctx[216] = { 0 };
BYTE chacha20_ctx[32] = { 0 };
fake_sha512_init(ctx, 32);
fake_sha512_update(ctx, shared_key);
fake_sha512_update(ctx, file_pub);
fake_sha512_final(ctx, output);
memset(ctx, 0, 216);
fake_sha512_init(ctx, 24);
fake_sha512_update(ctx, output);
fake_sha512_final(ctx, output2);
//2.自定义hash输出点
printf("custom_hash: ");
for (int i = 0; i < 24; i++)
{
printf("%02X", output2[i]);
}
printf("\n\n");
init_chacha20_ctx(chacha20_ctx, output, output2);
//3.chacha20密钥拓展输出点
printf("chacha20_key_expanded: ");
for (int i = 0; i < 32; i++)
{
printf("%02X", chacha20_ctx[i]);
}
printf("\n\n");
BYTE* nonce = output2 + 16;
BYTE* enced_file_key = info + 12;
BYTE v175[32] = { 0 };
BYTE v176[24] = { 0 };
BYTE poly_1305[64] = { 0 };
trans_or_gen_key(0, poly_1305, 64, chacha20_ctx, nonce, 0);
//4.poly1305密钥生成输出点
printf("poly1305_key: ");
for (int i = 0; i < 64; i++)
{
printf("%02X", poly_1305[i]);
}
printf("\n\n");
POLY1305 plctx = { 0 };
BYTE poly_result[16] = { 0 };
BYTE salt[] = { 32,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0 };
Poly1305_Init(&plctx, poly_1305);
Poly1305_Update(&plctx, file_pub, 32);
Poly1305_Update(&plctx, enced_file_key, 32);
Poly1305_Update(&plctx, salt, 16);
Poly1305_Final(&plctx, poly_result);
//5.poly1305结果和文件校验值输出点
BYTE* check_cmp = info + 44;
printf("hash_of_file: ");
for (int i = 0; i < 16; i++)
{
printf("%02X", poly_result[i]);
}
printf("\n\n");
printf("check_code_in_file: ");
for (int i = 0; i < 16; i++)
{
printf("%02X", check_cmp[i]);
}
printf("\n\n");
trans_or_gen_key(enced_file_key, v175, 32, chacha20_ctx, nonce, 1);
for (DWORD j = 0; j < 24; j++)
{
v176[j] = v175[j] ^ v175[j + 8];
}
section_arr all_sections;
if (file_size > 0x100000)
{
ULONGLONG block_sz = 0, gap_sz = 0, block_start = 0;
block_sz = 9 * (ULONGLONG)(file_size / 100);
gap_sz = ((file_size - 27 * (ULONGLONG)(file_size / 100)) >> 1);
block_start = block_sz + gap_sz;
all_sections.push_back(section_uint64(0, block_sz, 1));
all_sections.push_back(section_uint64(block_start, block_sz, 1));
all_sections.push_back(section_uint64(block_start * 2, file_size - block_start * 2, 1));
all_sections.push_back(section_uint64(block_sz, gap_sz, 0));
all_sections.push_back(section_uint64(block_start + block_sz, gap_sz, 0));
all_sections.unwind();
all_sections.sort();
}
else
{
all_sections.push_back(section_uint64(0, file_size, 1));
}
for (DWORD i = 0; i < all_sections.secs.size(); i++)
{
section_uint64& current = all_sections.secs[i];
LARGE_INTEGER tmp_fs;
tmp_fs.QuadPart = current.start;
DWORD cursz = (DWORD)current.size;
SetFilePointerEx(fp, tmp_fs, NULL, FILE_BEGIN);
SetFilePointerEx(fp2, tmp_fs, NULL, FILE_BEGIN);
memset(tmp, 0, 0x100000);
ReadFile(fp, tmp, cursz, NULL, NULL);
if (current.is_enc)
{
file_block_trans(tmp, tmp, cursz, v175, v176);
}
DWORD written = 0;
WriteFile(fp2, tmp, cursz, &written, NULL);
}
CloseHandle(fp);
CloseHandle(fp2);
DWORD dwAttribute = GetFileAttributes(from_path);
if (dwAttribute & FILE_ATTRIBUTE_READONLY)
{
dwAttribute &= ~FILE_ATTRIBUTE_READONLY;
SetFileAttributes(from_path, dwAttribute);
DeleteFile(from_path);
dwAttribute = GetFileAttributes(dst_path);
dwAttribute |= FILE_ATTRIBUTE_READONLY;
SetFileAttributes(dst_path, dwAttribute);
}
else
{
DeleteFile(from_path);
}
return1;
}
4.7.2.4 第三步
解密器输入选定的文件,收集输出结果.
结果如下:
4.7.2.5 第四步
ida调试打断点,获取黑客解密器的运行数据.
断点1: 取edx.
断点2: 取esi.
断点3: 断在0x40e901,输出取高亮变量(ida可双击打开).
断点4: 断在0x40e946,取高亮变量(双击打开).
断点5: 取edx.
4.7.2.6 第五步
收集断点结果.
断点1:
断点2:
断点3:
断点4:
断点5:
可以发现,只有断点5(文件中校验值)与成功解密的输出不一样.
4.7.2.7 第六步
patch掉poly1305校验的两处判断关键跳转,然后f9执行:
第一处:
第二处:
得到结果:
文件得以成功解密.
4.7.2.8 结论
数据损坏的位置在被加密文件中的16bytes的poly1305校验码处.
5.分析结论
本次解密失败的根本原因在于:加密文件尾部的 Poly1305 校验码发生了损坏,导致解密器在验证文件完整性时计算出的校验值与实际存储值不一致,从而触发了解密流程中的校验失败判断,终止了解密操作。通过Patch跳过该校验逻辑后,文件得以成功恢复,进一步确认了解密算法本身无误,问题确实源于校验数据的损坏。