Фон
Так получилось, что сервер был атакован вирусом-вымогателем, который по «счастливой случайности» частично оставил файлы .ibd (файлы необработанных данных таблиц innodb) нетронутыми, но при этом полностью зашифровал файлы .
fpm ( структурные файлы).
В этом случае .
idb можно разделить на:
- подлежит восстановлению стандартными инструментами и направляющими.
Для таких случаев есть отличное статья ;
- частично зашифрованные таблицы.
В основном это большие таблицы, для которых (как я понимаю) у злоумышленников не хватило оперативной памяти для полноценного шифрования;
- Ну и полностью зашифрованные таблицы, которые невозможно восстановить.
Также в начале файла можно наблюдать большое количество 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:\\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)
Второй предполагает много ручной работы, но и того, что было найдено, уже достаточно.
Все бы хорошо, но нужно знать абсолютно точная структура и (конечно) возник случай, что мне пришлось работать с часто меняющимся столом.
Никто не помнил, менялся ли тип поля или добавлялся новый столбец.
Wilds 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 и datatime, в статье будут описаны только они, но иногда будут и ссылки на другие типы данных, что может помочь и в других подобных инцидентах.
Проблема 1 : поля с типами DATETIME и TEXT имели значения NULL, и они просто пропускаются в файле, из-за этого в моем случае не удалось определить структуру для восстановления.
В новых столбцах значение по умолчанию было нулевым, а некоторые транзакции могли быть потеряны из-за настройки innodb_flush_log_at_trx_commit=0, поэтому на определение структуры пришлось бы потратить дополнительное время.
Проблема 2 : следует учитывать, что строки, удаленные через DELETE, все будут находиться в ibd файле, но при ALTER TABLE их структура не обновится.
В результате структура данных может меняться от начала файла до его конца.
Если вы часто используете OPTIMIZE TABLE, то вряд ли столкнетесь с такой проблемой.
Примечание , версия СУБД влияет на способ хранения данных, и этот пример может не работать для других основных версий.
В моем случае использовалась версия mariadb для Windows 10.1.24. Кроме того, хотя в mariadb вы работаете с таблицами InnoDB, на самом деле они ЭкстраДБ , что исключает применимость метода с InnoDB mysql.
Анализ файлов
В Python тип данных байты() отображает данные в Юникоде вместо обычного набора чисел.Хотя просмотреть файл можно и в таком виде, для удобства можно преобразовать байты в числовую форму, преобразуя массив байтов в обычный массив (список(example_byte_array)).
В любом случае оба метода полезны для анализа.
Просмотрев несколько файлов ibd, вы можете найти следующее:
Более того, если разделить файл по этим ключевым словам, то в большинстве случаев вы получите ровные блоки данных.
В качестве делителя мы будем использовать минимальную шкалу.
table = table.split("infimum".
encode())
Интересное наблюдение: для таблиц с небольшим объемом данных между нижней и верхней границей находится указатель на количество строк в блоке.
— тестовая таблица с 1-й строкой
- тестовая таблица с 2 строками Таблицу массива строк[0] можно пропустить.
Просмотрев его, я все еще не смог найти необработанные данные таблицы.
Скорее всего, этот блок используется для хранения индексов и ключей.
Начиная с table[1] и переводя ее в числовой массив, уже можно заметить некоторые закономерности, а именно:
Это целочисленные значения, хранящиеся в строке.
Первый байт указывает, является ли число положительным или отрицательным.
В моем случае все числа положительные.
Из оставшихся 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 количества времени, это недоразумение можно исправить.
Далее функция, которая возвращает объект 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
, похоже, это то, что вам нужно.
При этом такая последовательность не повторяется дважды в строке.
С помощью регулярного выражения находим необходимые данные: 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.### Я понимаю, что этот метод подходит не всем, но основная цель статьи – подтолкнуть к действию, а не решить все ваши проблемы.
Думаю, самым правильным решением было бы начать изучать исходный код самостоятельно.
Мариадб , но из-за ограниченности времени текущий метод показался самым быстрым.
В некоторых случаях, проанализировав файл, вы сможете определить примерную структуру и восстановить его одним из стандартных методов по ссылкам выше.
Это будет намного правильнее и вызовет меньше проблем.
Теги: #python #Администрирование базы данных #python3 #Восстановление данных #mariadb #база данных #восстановление #bytes
-
Haiku Разделит Стенд С Reactos На Linuxworld
19 Oct, 24 -
Гламурный Шаттл: Xpc Barebone Sg33G6 Deluxe
19 Oct, 24