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

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

Передісторія

Так сталося, що сервер був атакований вірусом шифрувальником, який за "щасливою випадковістю", частково відставив не зворушеними файли .ibd (файли сирих даних innodb таблиць), але при цьому повністю зашифрував файли .fpm (файли структур). При цьому .idb можна було розділити на:

  • підлягають відновленню через стандартні засоби та гайди. Для таких випадків, є відмінна статися;
  • частково зашифровані таблиці. Переважно це великі таблиці, на які (як я зрозумів) зловмисниками не вистачило оперативної пам'яті на повне шифрування;
  • та й повністю зашифровані таблиці, що не підлягають відновленню.

Визначити до якого з варіанта відносяться таблиці вдалося банальним відкривши в будь-якому текстовому редакторі під потрібним кодуванням (у моєму випадку це UTF8) і просто переглянути файл на наявність текстових полів, наприклад:

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

Також, на початку файлу можна спостерігати велику кількість 0-х байт, а віруси, що використовують алгоритм блочного шифрування (найбільш поширене), зазвичай і їх зачіпають.
Відновлення даних з 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)

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

Дебрі сіті на жаль не змогли допомогти з таким випадком, тому й пишеться ця стаття.

Ближче до діла

Існує структура таблиці 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 і datatime, у статті будуть описані тільки вони, але іноді будуть посилатися і на інші типи даних, що може допомогти в інших подіях.

Проблема 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 тип даних байт () відображає дані в юнікод в місце звичайного набору чисел. Хоча розглядати файл можна й у такому вигляді, але для зручності можна перевести байти у числовий вид перевівши масив байт у звичайний масив (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):  # 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.
У таблиці був первинний ключ з автоінкрементом, і тут його також можна знайти

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

Зіставивши дані з тестових таблиць, було виявлено, що об'єкт DATETIME складається з 5 байт починався зі 153 (швидше за все вказує на річні проміжки). Так як діапазон DATTIME дорівнює '1000-01-01' to '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 у потрібних полях, але в моєму випадку це не критично. Після цього в циклі перебираємо знайдене. Скрипт:

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)

Власне, дані з масиву result, це і є необхідні нам дані. ###PS.###
Я розумію, що такий спосіб підійде далеко не всім, але основна мета статті швидше наштовхнути на дію, ніж вирішити всі ваші проблеми. Думаю найбільш правильне рішення було б почати вивчати вихідний код Маріадб, Але у зв'язку з обмеженим часом, поточний спосіб здався найшвидшим.

У деяких випадках, проаналізувавши файл, ви зможете визначити зразкову структуру і відновити одним із стандартних способів із посилань вище. Це буде набагато правильніше і викличе менше проблем.

Джерело: habr.com

Додати коментар або відгук