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

Също така, в началото на файла може да се наблюдава голямо количество нулеви байтове, а вирусите, които използват алгоритъм на блочно шифроване (най-разпространен), обикновено засягат и тях.
![]()
В моя случай, злоумышлениците в края на всеки шифрован файл оставяха ред от 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_pointINT (11);id_userINT (11);date_startDATETIME ;date_finishDATETIME .
За възстановяване се използва побайтов анализ на .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 таблици, те всъщност са , което изключва приложимостта на метода с InnoDB mysql.
Анализ на файла
В python, типът данни показва данните в юникод, вместо обикновен набор от числа. Въпреки че файлът може да бъде разглеждан и по този начин, за удобство можете да преобразувате байтовете в числов вид, като преобразувате масив от байтове в обикновен масив (list(example_byte_array)). Във всеки случай, и двата метода ще бъдат полезни за анализа.
Преглеждайки няколко ibd файла, можете да срещнете следните:
![]()
Освен това, ако разделите файла по тези ключови думи, ще получите предимно равни блокове данни. Ще използваме infimum като делител.
table = table.split("infimum".encode())Интересно наблюдение, за таблици с малко количество данни, между infimum и supremum има указател на броя редове в блока.
— тестова таблица с 1 ред
— тестова таблица с 2 реда
Масивът от низове table[0] може да бъде пропуснат. Когато го прегледах, така и не успях да открия суровите данни на таблиците. Вероятно този блок служи за съхранение на индекси и ключове.
Започвайки от table[1] и превеждайки я в числов масив, вече могат да се забележат някои закономерности, а именно:

Това са 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.
В таблицата имаше първичен ключ с автоинкремент, и тук също може да бъде открит.

Сопоставяйки данните от тестовите таблици, беше установено, че обектът 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
, изглежда това е точно това, от което се нуждаем. Освен това, такава последователност не се повтаря два пъти в низа.
Използвайки регулярни изрази, намираме необходимите данни:
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.###
Разбира се, такъв начин не би подхождал на всеки, но основната цел на статията е по-скоро да вдъхнови за действие, отколкото да реши всичките ви проблеми. Мисля, че най-правилното решение би било да започнете да изучавате изходния код на самия , но поради ограниченото време, текущият метод се оказа най-бързият.
В някои случаи, анализирайки файла, ще можете да определите приблизителната структура и да възстановите по един от стандартните методи от изброените по-горе. Това ще бъде много по-правилно и ще предизвика по-малко проблеми.
Източник: habr.com
