0.前言
今天公司服务器中招LockBit2.0勒索病毒,损失惨重,全体加班了一天基本解决了部分问题,首先是丢失的文件数据就没法恢复了,这一块没有理睬,主要恢复的是两个数据库,一个是16GB大小的SQLserver数据库,另一个是一个15GB大小的Mysql数据库,最后丢失了1%左右的内容顺利恢复了两个数据库,因为lockbit勒索病毒的分享给文章太少,这里就记录下自己恢复的过程。
1.恢复逻辑
lockbit2.0的锁定逻辑是针对有价值的文件,头部4KB给你加密,尾部4KB给你加密,然后中间隔一段随机(也可能是固定间隔,没有考证过)给你加密大约4KB,全局给你大约加密不到1%的数据,因此恢复的逻辑也就是舍弃这些被加密的数据,尽可能恢复数据库正常。
注意!!!,lockbit2.0不存在找人破解的可能性,找人弄也是按照上面的逻辑,懂程序原理的都知道lockbit的加密是无法被破解的,因此注意各位不要被骗!
另外我们能恢复数据库主要是我们的数据库有以下特点,数据库极大,表内条数极多,这样被损坏的数据才能最大概率着落在表内数据的,并且表内多是流水账数据,丢失部分的损失可控,因此修复的可能性最高,如果你的数据库不大,几百兆,内容不多表结构复杂的,没什么修复的价值。
2.sqlserver恢复
sqlserver市面是存在恢复软件的,我找的是达思科技的软件,1399元买了一天的使用权限,通过被锁的文件和1年前数据库文件(一半大小)一起合并进行了恢复,最后的回复结果是大致回复了99%的数据,部分表格少几条数据,有一个表格是40000多条数据,少了20条,另一个表格18000条数据,少了8条,少的数据就是被加密的数据,被舍掉了。因为sqlsever按照我查的资料,主要被加密的数据一个是头文件,也就是表结构,主要是通过过去的备份文件恢复的,中间的数据就是舍弃。这个恢复成功的思路对我后面回复mysql提供了较大的帮助。
最终sqlsever恢复,但是因为是财务软件的数据库,少的数据还是会导致很多模块计算有问题,这个只能后续慢慢反查和处理了。
注意,一天时间比较宝贵,请提前给自己的机子搭建好本地sql server环境,到恢复成功大约耗时要18个小时,需要通宵干。
3,mysql恢复
mysql恢复是最复杂的,首先mysql市面上是没有成品恢复软件的,不要被骗。因此只能动手恢复mysql的原始文件是分立的文件,每一个文件都被加密了,原始数据库是二进制文件,因此恢复原始数据库文件的难度最大,几乎是不可能被恢复。
因此我们恢复瞄准的是备份的.sql文件,.sql文件是一个类似文本文件,可读性高,恢复起来比较容易。
3.1 mysql文件的逻辑与查看
mysql文件的基本逻辑是如下
建立数据库(一句话)
{
建立表A
建立表A结构(列)
插入表A的数据 *N
。。。。
} *N个表循环
其中头部的4KB是建立表头和建立表结构,这一块只能手敲,具体方法是自己建立数据库,复原表结构后导出为sql文件,然后比对着sql文件自己手动恢复头部4KB文件。
第二部分是建立表结构,表结构不复杂的话大约是1KB左右的样子
第三部分是插入表数据,这个是数据库中最大的一块,我们的mysql会将一大堆数据写成一个insert插入,每一个insert的最长是2MB字节的大小,因此如果损坏的4KB落在这里的话,就要排除掉这2MB,后续再维修这2MB。
加密的逻辑是随机加密(待考),因此文件内部被锁的4KB落在insert中的概率最大。
对于这种单文件超大的sql文件,是没有查看工具的,因此首先用c#写一个分析工具,逐行分析这个sql文件是干什么的
使用下面的代码,可以将每一行都打印到log文件中,使用文本打开log文件可以查看sql语句的结构。
cs
static void Main(string[] args)
{
///* sql有效性验证算法+打印算法
int linenum = 0;
string filename = "I:\\mysql_backup_20231008__00_00.sql.lockbit";
string filename1 = "I:\\log.text";
string filename2 = "I:\\xiufuz_error.sql";
String line;
bool bResult;
int[] errorcode = new int[5];
try
{
StreamReader sr = new StreamReader(filename);
StreamWriter sw1 = new StreamWriter(filename1);
//StreamWriter sw2 = new StreamWriter(filename2);
StringBuilder sb = new StringBuilder(); //定义
sb.Clear();
int isbinary = 0;
int errortick = 0;
int isrightorerror = 0;
while (!sr.EndOfStream)
{
//lstchar2 = lstchar1;
//lstchar1 = currentChar;
string readlinest = sr.ReadLine();
byte[] charArray = Encoding.Default.GetBytes(readlinest);
sb.Append(readlinest);
if (charArray.Count() > 0)
{
if (charArray[charArray.Count() - 1] == 0x3b)
{
line = sb.ToString();
sb.Clear(); //清空
linenum++;
byte[] bytetemp = Encoding.Default.GetBytes(line);
isbinary = 0;
errortick = 0;
isrightorerror = 0; //正确
for (int i = 0; i < line.Length; i++)
{
if (i + 7 < line.Length)
{
if (line[i] == '_')
{
try
{
if (line[i+1]=='b'&&line[i+2]=='i'&&line[i+3]=='n'&&line[i+4]=='a'&&line[i+5]=='r')
{
isbinary = 1;
}
}
catch
{
}
}
}
if (isbinary == 0)
{
if (!IsChineseLetter(line, i))
{
errorcode[errortick] = Char.ConvertToUtf32(line, i);
errortick++;
if (errortick > 3)
{
if (Char.ConvertToUtf32(line, i) < 0xff00)
{
}
isrightorerror = 1; //错误
break;
}
}
}
else
{
}
}
if (isrightorerror == 1)
{
//错误
//sw2.WriteLine(line);
sw1.WriteLine("行:" + linenum.ToString("D5") + "错误,内容:" + line.Substring(0, 50) + "---" + errorcode[0].ToString("X4") + ',' + errorcode[1].ToString("X4") + ',' + errorcode[2].ToString("X4"));
//Console.WriteLine("错误字符,line:" + linenum.ToString() + ",长度:"+line.Length.ToString()+"---" + errorcode[0].ToString("X4") + ',' + errorcode[1].ToString("X4") + ',' + errorcode[2].ToString("X4"));
}
else
{
//正确
//sw1.WriteLine(line);
sw1.WriteLine("行:" + linenum.ToString("D5") + "正确,内容:" + line.Substring(0, line.Length > 50 ? 50 : line.Length));
}
if (linenum % 100 == 0)
Console.WriteLine("Line:" + linenum.ToString());
}
}
}
sr.Close();
sw1.Close();
//sw2.Close();
Console.WriteLine("Final Line:" + linenum.ToString());
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e.Message);
}
finally
{
Console.WriteLine("Executing finally block.");
}
while (true) ;
}
通过上述文件打印出来的log可以观察到非常多的信息:
如下图,这是一个正确的建立表格,插入数据的结构,由7行的建立表头,若干行的INSERT数据和1行的解锁组成,按照这个逻辑可以查看每一个表格的语句是否完成,如图我战士的after_sales这个表格就非常完整。
如下图所示,这是一个典型的中间有损失的表,可以看到表结构是正确的,插入语句在21393行出现了错误,这一条语句的数据就要被舍弃,后续再根据重要性选择恢复剩余的细节,但是这个表的绝大多数部分都会恢复并且是好的
3.2 使用程序辅助分离sql文件中的好的部分和坏的部分
这里我使用的C#写的处理程序,处理逻辑是,按照分号加换行符来确认不同的sql语句行,在每一行中逐个字节判断字节是不是有效的文本信息,如果本行文件中出现了三次以上的非Unicode字符的字节,我们就判定这一行被加密了,将这一行复制到错误的文件中等待修复,反之则复制到正确的文件中修复。
附C#代码
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace 勒索修复程序
{
class Program
{
//我的数据库中各个国家的文字都有,因此这里做了针对性的排除
static bool IsChineseLetter(string input, int index)
{
int code = 0;
int chfrom = Convert.ToInt32("4e00", 16); //范围(0x4e00~0x9fff)转换成int(chfrom~chend)
int chend = Convert.ToInt32("9fff", 16);
if (input != "")
{
code = Char.ConvertToUtf32(input, index); //获得字符串input中指定索引index处字符unicode编码
if (code < 0x80) //英文是正确的
return true;
if (code >= chfrom && code <= chend)
{
return true; //当code在中文范围内返回true
}
if (code >= 0xa0 && code <= 0xff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0xff00 && code <= 0xffee)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x3000 && code <= 0x30ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x3300 && code <= 0x36ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x13a0 && code <= 0x13ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x2000 && code <= 0x22ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x2400 && code <= 0x27ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x0100 && code <= 0x02ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x0370 && code <= 0x03ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x0400 && code <= 0x05ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x0600 && code <= 0x06ff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x0e00 && code <= 0x0fda)
{
return true; //当code在中文范围内返回true
}
if (code >= 0xac00 && code <= 0xd7a3) //韩文
{
return true; //当code在中文范围内返回true
}
if (code >= 0xaa80 && code <= 0xaac2)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x1d00 && code <= 0x1eff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x0900 && code <= 0x0bff)
{
return true; //当code在中文范围内返回true
}
if (code >= 0x3400 && code <= 0x4dbf) //繁体
{
return true; //当code在中文范围内返回true
}
else
{
return false; //当code不在中文范围内返回false
}
}
return false;
}
static void Main(string[] args)
{
///* sql有效性验证算法
int linenum = 0;
string filename = "I:\\mysql_backup_20231008__00_00.sql.lockbit"; //目标文件
string filename1 = "I:\\xiufuz_right.sql"; //正确的保存文件
string filename2 = "I:\\xiufuz_error.sql"; //错误的保存文件
String line;
bool bResult;
try
{
StreamReader sr = new StreamReader(filename);
StreamWriter sw1 = new StreamWriter(filename1);
StreamWriter sw2 = new StreamWriter(filename2);
StringBuilder sb = new StringBuilder(); //定义
sb.Clear();
int isbinary = 0;
int errortick = 0;
int isrightorerror = 0;
while (!sr.EndOfStream)
{
//lstchar2 = lstchar1;
//lstchar1 = currentChar;
string readlinest = sr.ReadLine();
byte[] charArray = Encoding.Default.GetBytes(readlinest);
sb.Append(readlinest);
if (charArray.Count() > 0)
{
if (charArray[charArray.Count() - 1] == 0x3b)
{
line = sb.ToString();
sb.Clear(); //清空
linenum++;
byte[] bytetemp = Encoding.Default.GetBytes(line);
isbinary = 0;
errortick = 0;
isrightorerror = 0; //正确
for (int i = 0; i < line.Length; i++)
{
if (i + 7 < line.Length)
{
if (line[i] == '_')
{
try
{
if (line[i+1]=='b'&&line[i+2]=='i'&&line[i+3]=='n'&&line[i+4]=='a'&&line[i+5]=='r')
{
isbinary = 1;
}
}
catch
{
}
}
}
if (isbinary == 0)
{
if (!IsChineseLetter(line, i))
{
errorcode[errortick] = Char.ConvertToUtf32(line, i);
errortick++;
if (errortick > 3)
{
Console.WriteLine("错误字符,line:" + linenum.ToString() + ",长度:"+line.Length.ToString()+"---" + errorcode[0].ToString("X4") + ',' + errorcode[1].ToString("X4") + ',' + errorcode[2].ToString("X4"));
if (Char.ConvertToUtf32(line, i) < 0xff00)
{
}
isrightorerror = 1; //错误
break;
}
}
}
else
{
}
}
if (isrightorerror == 1)
{
//错误
sw2.WriteLine(line);
}
else
{
//正确
sw1.WriteLine(line);
}
}
}
}
sr.Close();
sw1.Close();
sw2.Close();
Console.WriteLine("Final Line:" + linenum.ToString());
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e.Message);
}
finally
{
Console.WriteLine("Executing finally block.");
}
while (true) ;
// */
while(true);
}
}
}
执行效果如下图
可以看到,被攻击的段落中至少出现三个FFFD以上的,这个就是被攻击的段落的特点,将没被攻击的干净的sql文件导出后进行下一步处理。
3.3 mysql文件导入库
mysql文件导入mysql库的过程需要自己执行,导入过程中发现错误随时进行处理和补全表结构,这里因为表是你自己建的,别人也帮不了你,是一个苦功夫,但是我的数据库16GB大小,表结构占位不到1MB,因此表结构被加密的概率较低,实际恢复过程中我的数据库就一个表结构被损坏了,因此就补了一个表结构就完成了数据库的恢复。
3.4 其他被损坏的文件的处理
首先我通过C#将500MB左右的损坏文件拆成了10个50MB的文件,按照每个文件50行
附代码
cs
static void Main(string[] args)
{
///* //拆分算法
try
{
string line="";
int linenum=0;
byte lstchar1=0, lstchar2=0;
byte currentChar=0;
int block = 0;
string filename = "I:\\xiufuz_error.sql";
string filename1 = "I:\\error1.sql";
string filename2 = "I:\\error2.sql";
string filename3 = "I:\\error3.sql";
string filename4 = "I:\\error4.sql";
string filename5 = "I:\\error5.sql";
string filename6 = "I:\\error6.sql";
string filename7 = "I:\\error7.sql";
string filename8 = "I:\\error8.sql";
string filename9 = "I:\\error9.sql";
string filename10 = "I:\\error10.sql";
StreamReader sr = new StreamReader(filename);
StreamWriter sw1 = new StreamWriter(filename1);
StreamWriter sw2 = new StreamWriter(filename2);
StreamWriter sw3 = new StreamWriter(filename3);
StreamWriter sw4 = new StreamWriter(filename4);
StreamWriter sw5 = new StreamWriter(filename5);
StreamWriter sw6 = new StreamWriter(filename6);
StreamWriter sw7 = new StreamWriter(filename7);
StreamWriter sw8 = new StreamWriter(filename8);
StreamWriter sw9 = new StreamWriter(filename9);
StreamWriter sw10 = new StreamWriter(filename10);
//Read the first line of text
StringBuilder sb = new StringBuilder(); //定义
sb.Clear();
while (!sr.EndOfStream)
{
//lstchar2 = lstchar1;
//lstchar1 = currentChar;
string readlinest = sr.ReadLine();
byte[] charArray = Encoding.Default.GetBytes(readlinest);
sb.Append(readlinest);
if (charArray.Count() > 0)
{
if (charArray[charArray.Count() - 1] == 0x3b)
{
line = sb.ToString();
sb.Clear(); //清空
linenum++;
if (linenum % 100 == 0)
Console.WriteLine("Line:" + linenum.ToString());
//清空前处理
if (block == 0)
sw1.WriteLine(line);
else if (block == 1)
sw2.WriteLine(line);
else if (block == 2)
sw3.WriteLine(line);
else if (block == 3)
sw4.WriteLine(line);
else if (block == 4)
sw5.WriteLine(line);
else if (block == 5)
sw6.WriteLine(line);
else if (block == 6)
sw7.WriteLine(line);
else if (block == 7)
sw8.WriteLine(line);
else if (block == 8)
sw9.WriteLine(line);
else if (block == 9)
sw10.WriteLine(line);
if (linenum > 50)
{
if (block < 9)
{
linenum = 0;
block++;
}
}
}
}
}
sw1.Close();
sw2.Close();
sw3.Close();
sw4.Close();
sw5.Close();
sw6.Close();
sw7.Close();
sw8.Close();
sw9.Close();
sw10.Close();
Console.WriteLine("Final Line:" + linenum.ToString());
while(true);
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e.Message);
}
finally
{
Console.WriteLine("Executing finally block.");
}
// */
while(true);
}
拆成50MB左右的文件后就可以使用notepad打开了,打开后根据每一条的重要程度选择性恢复,这里的回复只能自己手动回复的,具体的办法是将里面损坏的部分删除后,手动对齐格式,把剩余能插入的数据再执行插入回数据库中。是一个手工的水磨细活。
4 总结
全部恢复耗时两个人24小时通宵解决了,尽可能的减少了勒索病毒对公司的影响。
勒索病毒是对公司的损失是重大的,本次勒索病毒我们自己总结如下经验
1,首先杀毒系统要装,有钱就买个卡巴斯基装上(淘宝有便宜的),没钱装一个360,一定要有杀软
2,内网别偷懒,该划分vlan就划分vlan,至少能做好数据隔离
3,核心数据库资产一定要做一个云端备份,可以用阿里云或者腾讯云的付费产品,不要迷信本地备份,我们就是本地备份顺着共享文件夹全部一锅端了,三个备份机制全部失效。
4,如果真的依靠本地备份,就在vm exsi中做镜像级别的备份,这个备份不会被干掉,反之所有的windows级别的备份都会被勒索病毒干掉。