Хабрахабр

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

image

Предыстория

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

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

Определить к какому из варианта относятся таблицы удалось банальным открыв в любом текстовом редакторе под нужной кодировкой (в моём случае это UTF8) и просто просмотреть просмотреть файл на наличие текстовых полей, например:

image

Также, в начале файла можно наблюдать большое количество 0-вых байт, а вирусы использующие алгоритм блочного шифрования (наиболее распространено), обычно и их затрагивают.
image

Для поиска не заражённых файлов хватило и скрипта: В моём случае, злоумышленники в конце каждого зашифрованного файла оставляли строку из 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:\\some\\path"): 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 .

Так как для поиска требуемого, нам достаточно проанализировать такие типы данных как int и datatime, в статье будут описаны только они, но иногда будуn ссылаться и на другие типы данных, что может помочь в иных подобных происшествиях. Для восстановление используется побайтовый анализ .ibd файла, с последующим переводом их в более читаемый вид.

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

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

В моём случае использовалась windows версия mariadb 10. Обратите внимание, версия СУБД влияет на способ хранения данных, и данный пример может не сработать для других мажорных версий. 24. 1. Также, хоть и в mariadb вы работаете с InnoDB таблицами, но по факту они являются XtraDB, что исключает применяемость метода с InnoDB mysql.

Анализ файла

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

Просмотрев несколько ibd файлов, можно встретить следующие:

image

Будем использовать infimum как делитель. При чём, если делить файл по этим ключевым словам, получатся преимущественно ровные блоки данных.

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

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

image — тестовая таблица с 1-ой строкой

image — тестовая таблица с 2-мя строками

Просмотрев его, мне так и не удалось обнаружить сырые данные таблиц. Массив строк table[0] можно пропустить. Скорей всего, данный блок служит для хранения индексов и ключей.
Начиная с table[1] и переведя её в числовой массив, уже можно заметить некоторые закономерности, а именно:

image

Первый байт указывает является ли число положительным, или отрицательным. Это 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.
В таблице имелся первичный ключ с автоинкрементом, и здесь его также можно обнаружить

image

Так как диапазон DATTIME равен '1000-01-01' to '9999-12-31', думаю число байт может может разнится, но в моём случае, данные подпадают в промежуток от 2016 по 2019 года, поэтому будем считать, что 5 байт достаточно. Сопоставив данные из тестовых таблиц, было выявлено, что объект DATETIME состоит из 5 байт начинался с 153 (скорей всего указывает на годовые промежутки).

Скрипт: Для определения времени без секунд, были написанные следующие функции.

day_ = lambda x: x % 64 // 2 # 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 image, похоже это то что нужно.

Используя регулярное выражение, находим необходимые данные:

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.###
Я понимаю что такой способ подойдёт далеко не всем, но основная цель статьи скорей натолкнуть на действие, чем решить все ваши проблемы. Собственно всё, данные из массива result, это и есть необходимые нам данные. Думаю наиболее правильное решение было бы начать изучать исходный код самой mariadb, но в связи с ограниченным временем, текущий способ показался наиболее быстрый.

Это будет гораздо правильней и вызовет меньше проблем. В некоторых случаях, проанализировав файл, вы сможете определить примерную структуру и восстановить одним из стандартных способов из ссылок выше.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть