Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

Предистория

Случайно се оказа, че сървърът беше атакуван от вирус на шифровалка, който по "щастлива случайност" частично остави непокътнати файловете .ibd (файлове с оригинални данни на innodb таблици), но напълно шифрова файловете .fpm (файлове със структура). При това, .idb могат да бъдат разделени на:

  • възстановими чрез стандартни средства и ръководства. За такива случаи, има отличен член;
  • частично шифровани таблици. Преимствено това са големи таблици, при които (както разбрах), злоумышлениците не са имали достатъчно оперативна памет за пълно шифроване;
  • и напълно шифровани таблици, които не подлежат на възстановяване.

Определянето на към какъв вариант принадлежат таблиците стана просто като отворите файл в текстов редактор с нужната кодировка (в моя случай UTF8) и просто прегледате файла за текстови полета, например:

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

Също така, в началото на файла може да се наблюдава голямо количество нулеви байтове, а вирусите, които използват алгоритъм на блочно шифроване (най-разпространен), обикновено засягат и тях.
Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

В моя случай, злоумышлениците в края на всеки шифрован файл оставяха ред от 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)

Така успях да намеря файловете, принадлежащи на първия тип. Вторият предполага дългосрочна ръчна работа, но вече намереното беше достатъчно. Всичко би било добре, но необходимо е да знаем абсолютно точната структура и (разбира се) се случи такъв случай, че трябваше да работя с често променяща се таблица. Никой не помнеше, нито типа на полето е бил променен, нито е било добавено ново поле.

За съжаление, Debri City не можа да помогне с такъв случай, затова и написах тази статия.

По същество

Има структура на таблица от преди 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 INT (11);
  • id_user INT (11);
  • date_start DATETIME ;
  • date_finish DATETIME .

За възстановяване се използва побайтов анализ на .ibd файла, след което се преобразува в по-четим вид. Тъй като за намиране на необходимото е достатъчно да анализираме типове данни като int и datetime, в статията ще бъдат описани само те, но понякога ще се позовавам и на други типове данни, което може да помогне при подобни случаи.

Проблем 1: в полета с типове DATETIME и TEXT имаше NULL стойности, и в файла те просто се пропускат, поради което не успях да определя структурата за възстановяване в моя случай. В новите колони стойността по подразбиране беше null, а част от транзакцията можеше да бъде загубена поради настройката innodb_flush_log_at_trx_commit = 0, затова за определяне на структурата щеше да е необходимо да се отдели допълнително време.

Проблем 2: трябва да се има предвид, че редовете, изтрити чрез DELETE, все пак ще се намират в ibd файла, но при ALTER TABLE тяхната структура няма да се обнови. В крайна сметка, структурата на данните може да варира от началото на файла до неговия край. Ако често използвате OPTIMIZE TABLE, вероятно няма да се сблъскате с подобен проблем.

Обърнете внимание, версията на СУБД влияе на начина на съхранение на данни, и този пример може да не работи за други мажорни версии. В моя случай беше използвана Windows версия на mariadb 10.1.24. Освен това, въпреки че в mariadb работите с InnoDB таблици, те всъщност са XtraDB, което изключва приложимостта на метода с InnoDB mysql.

Анализ на файла

В python, типът данни bytes() показва данните в юникод, вместо обикновен набор от числа. Въпреки че файлът може да бъде разглеждан и по този начин, за удобство можете да преобразувате байтовете в числов вид, като преобразувате масив от байтове в обикновен масив (list(example_byte_array)). Във всеки случай, и двата метода ще бъдат полезни за анализа.

Преглеждайки няколко ibd файла, можете да срещнете следните:

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

Освен това, ако разделите файла по тези ключови думи, ще получите предимно равни блокове данни. Ще използваме infimum като делител.

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

Интересно наблюдение, за таблици с малко количество данни, между infimum и supremum има указател на броя редове в блока.

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла — тестова таблица с 1 ред

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла — тестова таблица с 2 реда

Масивът от низове table[0] може да бъде пропуснат. Когато го прегледах, така и не успях да открия суровите данни на таблиците. Вероятно този блок служи за съхранение на индекси и ключове.
Започвайки от table[1] и превеждайки я в числов масив, вече могат да се забележат някои закономерности, а именно:

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

Това са int стойности, съхранявани в низ. Първият байт указва дали числото е положително или отрицателно. В моя случай всички числа са положителни. От останалите 3 байта може да се определи числото, използвайки следната функция. Скрипт:

def find_int(val: str):  # пример '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.
В таблицата имаше първичен ключ с автоинкремент, и тук също може да бъде открит.

Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла

Сопоставяйки данните от тестовите таблици, беше установено, че обектът DATETIME се състои от 5 байта, започващи от 153 (вероятно указва годишни интервали). Понеже диапазонът на DATETIME е от '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 време, това недоразумение може да се коригира.
Следва функция, която връща обект datetime от низ. Скрипт:

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 Възстановяване на данни от XtraDB таблици без файл на структурата, използвайки байтов анализ на ibd файла, изглежда това е точно това, от което се нуждаем. Освен това, такава последователност не се повтаря два пъти в низа.

Използвайки регулярни изрази, намираме необходимите данни:

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 стойности в необходимите полета, но в моя случай това не е критично. След това в цикъл преглеждаме намереното. Скрипт:

резултат = []
за вал в глобяни:
    предварителен_резултат = []
    bd_int  = re.findall(r"128, d*, d*, d*", вал)
    bd_date= re.findall(r"(153, 1[6,5,4,3]d, d*, d*, d*)", вал)
    за it в bd_int:
        предварителен_резултат.append(find_int(bd_int[it]))
    за bd в bd_date:
        предварителен_резултат.append(find_data_time(bd))
    резултат.append(предварителен_резултат)

Същността е, че данните от масива резултат са именно необходимите ни данни. ###PS.###
Разбира се, такъв начин не би подхождал на всеки, но основната цел на статията е по-скоро да вдъхнови за действие, отколкото да реши всичките ви проблеми. Мисля, че най-правилното решение би било да започнете да изучавате изходния код на самия mariadb, но поради ограниченото време, текущият метод се оказа най-бързият.

В някои случаи, анализирайки файла, ще можете да определите приблизителната структура и да възстановите по един от стандартните методи от изброените по-горе. Това ще бъде много по-правилно и ще предизвика по-малко проблеми.

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster