
Tło
Zdarzyło się, że serwer został zaatakowany przez wirusa szyfrującego, który „szczęśliwie” częściowo zostawił nienaruszone pliki .ibd (pliki surowych danych tabel InnoDB), ale całkowicie zaszyfrował pliki .fpm (pliki strukturalne). Pliki .idb można podzielić na:
- te, które można przywrócić za pomocą standardowych narzędzi i przewodników. W takich przypadkach istnieje doskonały ;
- częściowo zaszyfrowane tabele. Przede wszystkim są to duże tabele, które (jak zrozumiałem), hakerzy nie zdążyli całkowicie zaszyfrować z powodu braku pamięci operacyjnej;
- oraz całkowicie zaszyfrowane tabele, które nie podlegają przywróceniu.
Określenie, do którego wariantu należą tabele, udało się uzyskać, otwierając je w dowolnym edytorze tekstu z odpowiednim kodowaniem (w moim przypadku kodowanie to UTF8) i po prostu przeglądając plik w poszukiwaniu pól tekstowych, na przykład:

Również na początku pliku można zauważyć dużą ilość zer, a wirusy używające algorytmu szyfrowania blokowego (najczęściej spotykanego) zazwyczaj również je dotyczą.
![]()
W moim przypadku hakerzy na końcu każdego zaszyfrowanego pliku pozostawiali ciąg złożony z 4 bajtów (1, 0, 0, 0), co ułatwiło zadanie. Do znalezienia niezainfekowanych plików wystarczyła także skrypt:
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)W ten sposób udało się znaleźć pliki należące do pierwszego typu. Drugi wymagał długiego ręcznego przywracania, ale znalezionych plików już wystarczyło. Wszystko byłoby w porządku, ale trzeba znać absolutnie dokładną strukturę i (oczywiście) zdarzył się taki przypadek, że trzeba było pracować z często zmieniającą się tabelą. Nikt już nie pamiętał, czy typ pola się zmienił, czy dodano nową kolumnę.
Niestety, wsparcie z Debry City nie mogło pomóc w takim przypadku, dlatego pisana jest ta artykuł.
Wracając do tematu
Istnieje struktura tabeli sprzed 3 miesięcy, która nie zgadza się z obecną (może na podstawie jednego pola, a może więcej). Struktura tabeli:
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)
); należy wyodrębnić:
id_pointINT (11);id_userINT (11);date_startDATETIME ;date_finishDATETIME .
Do odzyskiwania używa się analizy bajtowej pliku .ibd, a następnie konwersji ich do bardziej czytelnej formy. Ponieważ do wyszukiwania wystarczą nam analizy typów danych takich jak int i datetime, w artykule zostaną opisane tylko one, chociaż czasami będę odnosił się również do innych typów danych, co może pomóc w podobnych przypadkach.
Problem 1: w polach o typach DATETIME i TEXT występowały wartości NULL, które po prostu były pomijane w pliku, przez co określenie struktury dla odzyskiwania w moim przypadku było niemożliwe. W nowych kolumnach wartość domyślna była null, a część transakcji mogła być utracona z powodu ustawienia innodb_flush_log_at_trx_commit = 0, więc należałoby poświęcić dodatkowy czas na określenie struktury.
Problem 2: należy pamiętać, że wiersze usunięte przez DELETE wciąż będą znajdować się w pliku ibd, ale przy ALTER TABLE ich struktura nie zostanie zaktualizowana. W rezultacie struktura danych może różnić się od początku pliku do jego końca. Jeśli często używasz OPTIMIZE TABLE, to rzadko napotkasz podobny problem.
Zwróć uwagę, wersja DBMS wpływa na sposób przechowywania danych, a ten przykład może nie działać dla innych wersji głównych. W moim przypadku używana była wersja mariadb 10.1.24 na windows. Ponadto, chociaż w mariadb pracujesz z tabelami InnoDB, to w rzeczywistości są one , co wyklucza zastosowanie metody z InnoDB mysql.
Analiza pliku
W pythonie typ danych wyświetla dane w Unicode zamiast zwykłego zestawu liczb. Chociaż plik można rozpatrywać w ten sposób, dla wygody można przekształcić bajty w formę liczbową, przekształcając tablicę bajtów w zwykłą tablicę (list(example_byte_array)). W każdym razie oba sposoby będą przydatne do analizy.
Przeglądając kilka plików ibd, można napotkać następujące:
![]()
Zresztą, jeśli podzielisz plik według tych słów kluczowych, otrzymasz przeważnie równe bloki danych. Użyjemy infimum jako dzielnika.
table = table.split("infimum".encode())Interesujące spostrzeżenie: dla tabel z niewielką ilością danych między infimum a supremum znajduje się wskaźnik liczby wierszy w bloku.
— tabela testowa z 1 wierszem
— tabela testowa z 2 wierszami
Tablica stringów table[0] może być pominięta. Po jej przeglądzie nie udało mi się znaleźć surowych danych tabel. Najprawdopodobniej ten blok służy do przechowywania indeksów i kluczy.
Zaczynając od table[1] i przekształcając ją w tablicę numeryczną, można już dostrzec pewne wzorce, a mianowicie:

Są to wartości int przechowywane w stringu. Pierwszy bajt wskazuje, czy liczba jest dodatnia, czy ujemna. W moim przypadku wszystkie liczby są dodatnie. Z pozostałych 3 bajtów można obliczyć liczbę, stosując następującą funkcję. Skrypt:
def find_int(val: str): # przykład '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_intNa przykład, 128, 0, 0, 1 = 1, lub 128, 0, 75, 108 = 19308.
W tabeli znajdował się klucz podstawowy z autoinkrementacją, i tutaj również można go zauważyć.

Porównując dane z testowych tabel, odkryto, że obiekt DATETIME składa się z 5 bajtów zaczynających się od 153 (najprawdopodobniej oznacza to zakresy roczne). Ponieważ zakres DATETIME wynosi ‘1000-01-01’ do ‘9999-12-31’, sądzę, że liczba bajtów może się różnić, ale w moim przypadku, dane mieszczą się w przedziale od 2016 do 2019 roku, więc uznajmy, że 5 bajtów jest wystarczające.
Aby określić czas bez sekund, napisano następujące funkcje. Skrypt:
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}Nie udało się napisać działającej funkcji dla roku i miesiąca, więc musiałem użyć hardcodowania. Skrypt:
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, 0Jestem pewny, że poświęcając n czas, można naprawić to nieporozumienie.
Dalej funkcja zwracająca obiekt datetime z ciągu. Skrypt:
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)Udało się wykryć często powtarzające się wartości z int, int, datetime, datetime
, wydaje się, że to właśnie to. Co więcej, taka sekwencja nie powtarza się dwa razy w ciągu.
Używając wyrażenia regularnego, znajdź potrzebne dane:
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)Zauważ, że podczas wyszukiwania przy użyciu tego wyrażenia niemożliwe będzie określenie wartości NULL w wymaganych polach, ale w moim przypadku nie jest to krytyczne. Następnie w pętli przetwarzamy znalezione elementy. Skrypt:
wynik = []
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))
wynik.append(pre_result)Właściwie to wszystko, dane z tablicy wynik to są dane, których potrzebujemy. ###PS.###
Rozumiem, że taka metoda nie będzie odpowiednia dla wszystkich, ale głównym celem artykułu jest raczej zainspirowanie do działania, a nie rozwiązanie wszystkich Twoich problemów. Myślę, że najlepszym rozwiązaniem byłoby rozpoczęcie nauki o kodzie źródłowym samego , ale z powodu ograniczonego czasu, bieżąca metoda wydaje się najszybsza.
W niektórych przypadkach, analizując plik, będziesz mógł określić przybliżoną strukturę i odzyskać w jeden z standardowych sposobów z powyższych linków. To będzie znacznie lepsze i spowoduje mniej problemów.
Źródło: habr.com
