使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

史前

恰巧服务器遭到勒索病毒攻击,“侥幸”地部分保留了 .ibd 文件(innodb 表的原始数据文件),但同时完全加密了 .fpm 文件(结构文件)。 在这种情况下,.idb 可以分为:

  • 可通过标准工具和指南进行修复。 对于这种情况,有一个很好的 变得;
  • 部分加密的表。 大多数情况下,这些都是大表,(据我所知)攻击者没有足够的 RAM 来进行完全加密;
  • 好吧,完全加密的表无法恢复。

只需在任何文本编辑器中以所需编码(在我的例子中为 UTF8)打开表并简单地查看文件中是否存在文本字段,即可确定表属于哪个选项,例如:

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

另外,在文件的开头,您可以观察到大量的 0 字节,使用块加密算法(最常见)的病毒通常也会影响它们。
使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

在我的例子中,攻击者在每个加密文件的末尾留下了一个 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 表,但实际上它们是 数据库,这排除了该方法对 InnoDB mysql 的适用性。

文件分析

在Python中,数据类型 字节() 显示 Unicode 数据来代替一组常规数字。 虽然您可以以这种形式查看文件,但为了方便起见,您可以通过将字节数组转换为常规数组 (list(example_byte_array)) 将字节转换为数字形式。 无论如何,这两种方法都适合分析。

翻阅了几个ibd文件,可以发现以下内容:

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

此外,如果您按这些关键字划分文件,您将获得大部分偶数数据块。 我们将使用下确界作为除数。

table = table.split("infimum".encode())

一个有趣的观察:对于数据量较小的表,在下确界和上界之间有一个指向块中行数的指针。

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据 — 第一行的测试表

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据 - 2行测试表

可以跳过行数组table[0]。 查了一下,还是找不到原始表数据。 该块很可能用于存储索引和键。
从 table[1] 开始并将其转换为数值数组,您已经可以注意到一些模式,即:

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

这些是存储在字符串中的 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 = 1128, 0, 75, 108 = 19308.
该表有一个自增主键,也可以在这里找到

使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据

比较测试表中的数据后发现,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 中检测频繁重复的值 使用 ibd 文件的逐字节分析从没有结构文件的 XtraDB 表中恢复数据,看起来这就是您所需要的。 此外,这样的序列不会每行重复两次。

使用正则表达式,我们找到必要的数据:

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.###
我知道这种方法并不适合所有人,但本文的主要目标是促使您采取行动,而不是解决您的所有问题。 我认为最正确的解决方案是自己开始研究源代码 MariaDB的但由于时间有限,目前的方法似乎是最快的。

在某些情况下,分析文件后,您将能够确定大致结构并使用上面链接中的标准方法之一恢复它。 这将更加正确并且导致更少的问题。

来源: habr.com

添加评论