史前
恰巧服务器遭到勒索病毒攻击,“侥幸”地部分保留了 .ibd 文件(innodb 表的原始数据文件),但同时完全加密了 .fpm 文件(结构文件)。 在这种情况下,.idb 可以分为:
- 可通过标准工具和指南进行修复。 对于这种情况,有一个很好的
变得 ; - 部分加密的表。 大多数情况下,这些都是大表,(据我所知)攻击者没有足够的 RAM 来进行完全加密;
- 好吧,完全加密的表无法恢复。
只需在任何文本编辑器中以所需编码(在我的例子中为 UTF8)打开表并简单地查看文件中是否存在文本字段,即可确定表属于哪个选项,例如:
另外,在文件的开头,您可以观察到大量的 0 字节,使用块加密算法(最常见)的病毒通常也会影响它们。
在我的例子中,攻击者在每个加密文件的末尾留下了一个 4 字节字符串 (1, 0, 0, 0),这简化了任务。 要搜索未受感染的文件,脚本就足够了:
def opened(path):
files = os.listdir(path)
for f in files:
if os.path.isfile(path + f):
yield path + f
for full_path in opened("C:somepath"):
file = open(full_path, "rb")
last_string = ""
for line in file:
last_string = line
file.close()
if (last_string[len(last_string) -4:len(last_string)]) != (1, 0, 0, 0):
print(full_path)
因此,结果发现属于第一种类型的文件。 第二个涉及大量的手动工作,但找到的内容已经足够了。 一切都会好起来的,但你需要知道 绝对精确的结构 (当然)出现了一个情况,我必须使用经常更换的桌子。 没有人记得字段类型是否已更改或是否添加了新列。
不幸的是,荒野城无法帮助解决这种情况,这就是撰写本文的原因。
切入正题
3 个月前的表结构与当前的表结构不一致(可能有一个字段,也可能有多个字段)。 表结构:
CREATE TABLE `table_1` (
`id` INT (11),
`date` DATETIME ,
`description` TEXT ,
`id_point` INT (11),
`id_user` INT (11),
`date_start` DATETIME ,
`date_finish` DATETIME ,
`photo` INT (1),
`id_client` INT (11),
`status` INT (1),
`lead__time` TIME ,
`sendstatus` TINYINT (4)
);
在这种情况下,您需要提取:
id_point
整数(11);id_user
整数(11);date_start
约会时间;date_finish
约会时间。
为了恢复,对 .ibd 文件进行逐字节分析,然后将它们转换为更易读的形式。 由于要找到我们需要的内容,我们只需要分析 int 和 datatime 等数据类型,本文将仅描述它们,但有时我们也会引用其他数据类型,这可以在其他类似事件中提供帮助。
1问题:类型为 DATETIME 和 TEXT 的字段具有 NULL 值,并且它们在文件中被简单地跳过,因此,在我的情况下无法确定要恢复的结构。 在新列中,默认值为 null,并且由于设置 innodb_flush_log_at_trx_commit = 0,部分事务可能会丢失,因此需要花费额外的时间来确定结构。
2问题:应该考虑到,通过 DELETE 删除的行都将位于 ibd 文件中,但使用 ALTER TABLE 时,它们的结构不会更新。 因此,数据结构从文件的开头到结尾可能会有所不同。 如果你经常使用OPTIMIZE TABLE,那么你不太可能遇到这样的问题。
注意,DBMS 版本会影响数据的存储方式,并且此示例可能不适用于其他主要版本。 就我而言,使用的是 Windows 版本的 mariadb 10.1.24。 另外,虽然在 mariadb 中您使用 InnoDB 表,但实际上它们是
文件分析
在Python中,数据类型
翻阅了几个ibd文件,可以发现以下内容:
此外,如果您按这些关键字划分文件,您将获得大部分偶数数据块。 我们将使用下确界作为除数。
table = table.split("infimum".encode())
一个有趣的观察:对于数据量较小的表,在下确界和上界之间有一个指向块中行数的指针。
— 第一行的测试表
- 2行测试表
可以跳过行数组table[0]。 查了一下,还是找不到原始表数据。 该块很可能用于存储索引和键。
从 table[1] 开始并将其转换为数值数组,您已经可以注意到一些模式,即:
这些是存储在字符串中的 int 值。 第一个字节指示数字是正数还是负数。 就我而言,所有数字都是正数。 您可以使用以下函数从剩余的 3 个字节中确定数字。 脚本:
def find_int(val: str): # example '128, 1, 2, 3'
val = [int(v) for v in val.split(", ")]
result_int = val[1]*256**2 + val[2]*256*1 + val[3]
return result_int
Например, 128, 0, 0, 1 = 1或 128, 0, 75, 108 = 19308.
该表有一个自增主键,也可以在这里找到
比较测试表中的数据后发现,DATETIME 对象由 5 个字节组成,以 153 开头(很可能表示每年间隔)。 由于DATTIME范围是“1000-01-01”到“9999-12-31”,我认为字节数可能会有所不同,但就我而言,数据落在2016年至2019年期间,所以我们假设那5个字节就够了。
为了确定没有秒的时间,编写了以下函数。 脚本:
day_ = lambda x: x % 64 // 2 # {x,x,X,x,x }
def hour_(x1, x2): # {x,x,X1,X2,x}
if x1 % 2 == 0:
return x2 // 16
elif x1 % 2 == 1:
return x2 // 16 + 16
else:
raise ValueError
min_ = lambda x1, x2: (x1 % 16) * 4 + (x2 // 64) # {x,x,x,X1,X2}
不可能编写年和月的函数,所以我不得不破解它。 脚本:
ym_list = {'2016, 1': '153, 152, 64', '2016, 2': '153, 152, 128',
'2016, 3': '153, 152, 192', '2016, 4': '153, 153, 0',
'2016, 5': '153, 153, 64', '2016, 6': '153, 153, 128',
'2016, 7': '153, 153, 192', '2016, 8': '153, 154, 0',
'2016, 9': '153, 154, 64', '2016, 10': '153, 154, 128',
'2016, 11': '153, 154, 192', '2016, 12': '153, 155, 0',
'2017, 1': '153, 155, 128', '2017, 2': '153, 155, 192',
'2017, 3': '153, 156, 0', '2017, 4': '153, 156, 64',
'2017, 5': '153, 156, 128', '2017, 6': '153, 156, 192',
'2017, 7': '153, 157, 0', '2017, 8': '153, 157, 64',
'2017, 9': '153, 157, 128', '2017, 10': '153, 157, 192',
'2017, 11': '153, 158, 0', '2017, 12': '153, 158, 64',
'2018, 1': '153, 158, 192', '2018, 2': '153, 159, 0',
'2018, 3': '153, 159, 64', '2018, 4': '153, 159, 128',
'2018, 5': '153, 159, 192', '2018, 6': '153, 160, 0',
'2018, 7': '153, 160, 64', '2018, 8': '153, 160, 128',
'2018, 9': '153, 160, 192', '2018, 10': '153, 161, 0',
'2018, 11': '153, 161, 64', '2018, 12': '153, 161, 128',
'2019, 1': '153, 162, 0', '2019, 2': '153, 162, 64',
'2019, 3': '153, 162, 128', '2019, 4': '153, 162, 192',
'2019, 5': '153, 163, 0', '2019, 6': '153, 163, 64',
'2019, 7': '153, 163, 128', '2019, 8': '153, 163, 192',
'2019, 9': '153, 164, 0', '2019, 10': '153, 164, 64',
'2019, 11': '153, 164, 128', '2019, 12': '153, 164, 192',
'2020, 1': '153, 165, 64', '2020, 2': '153, 165, 128',
'2020, 3': '153, 165, 192','2020, 4': '153, 166, 0',
'2020, 5': '153, 166, 64', '2020, 6': '153, 1, 128',
'2020, 7': '153, 166, 192', '2020, 8': '153, 167, 0',
'2020, 9': '153, 167, 64','2020, 10': '153, 167, 128',
'2020, 11': '153, 167, 192', '2020, 12': '153, 168, 0'}
def year_month(x1, x2): # {x,X,X,x,x }
for key, value in ym_list.items():
key = [int(k) for k in key.replace("'", "").split(", ")]
value = [int(v) for v in value.split(", ")]
if x1 == value[1] and x2 // 64 == value[2] // 64:
return key
return 0, 0
我相信,如果你花n多时间,这个误解是可以纠正的。
接下来是一个从字符串返回日期时间对象的函数。 脚本:
def find_data_time(val:str):
val = [int(v) for v in val.split(", ")]
day = day_(val[2])
hour = hour_(val[2], val[3])
minutes = min_(val[3], val[4])
year, month = year_month(val[1], val[2])
return datetime(year, month, day, hour, minutes)
设法从 int、int、datetime、datetime 中检测频繁重复的值 ,看起来这就是您所需要的。 此外,这样的序列不会每行重复两次。
使用正则表达式,我们找到必要的数据:
fined = re.findall(r'128, d*, d*, d*, 128, d*, d*, d*, 153, 1[6,5,4,3]d, d*, d*, d*, 153, 1[6,5,4,3]d, d*, d*, d*', int_array)
请注意,使用此表达式进行搜索时,将无法确定所需字段中的 NULL 值,但在我的情况下,这并不重要。 然后我们循环遍历我们发现的内容。 脚本:
result = []
for val in fined:
pre_result = []
bd_int = re.findall(r"128, d*, d*, d*", val)
bd_date= re.findall(r"(153, 1[6,5,4,3]d, d*, d*, d*)", val)
for it in bd_int:
pre_result.append(find_int(bd_int[it]))
for bd in bd_date:
pre_result.append(find_data_time(bd))
result.append(pre_result)
其实就是这样,结果数组中的数据就是我们需要的数据了。 ###PS.###
我知道这种方法并不适合所有人,但本文的主要目标是促使您采取行动,而不是解决您的所有问题。 我认为最正确的解决方案是自己开始研究源代码
在某些情况下,分析文件后,您将能够确定大致结构并使用上面链接中的标准方法之一恢复它。 这将更加正确并且导致更少的问题。
来源: habr.com