Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

prehistoria

Tak się złożyło, że serwer został zaatakowany przez wirusa ransomware, który „szczęśliwym zbiegiem okoliczności” częściowo pozostawił pliki .ibd (surowe pliki danych tabel innodb) nietknięte, ale jednocześnie całkowicie zaszyfrował pliki .fpm ( pliki strukturalne). W tym przypadku .idb można podzielić na:

  • podlega renowacji przy użyciu standardowych narzędzi i prowadnic. W takich przypadkach jest to doskonałe rozwiązanie stać się;
  • częściowo zaszyfrowane tabele. Przeważnie są to duże tabele, dla których (o ile rozumiem) atakującym nie starczało pamięci RAM na pełne szyfrowanie;
  • Cóż, w pełni zaszyfrowane tabele, których nie można przywrócić.

Można było określić, do której opcji należą tabele, po prostu otwierając je w dowolnym edytorze tekstu z żądanym kodowaniem (w moim przypadku jest to UTF8) i po prostu przeglądając plik pod kątem obecności pól tekstowych, na przykład:

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

Ponadto na początku pliku można zaobserwować dużą liczbę bajtów 0, a wirusy korzystające z algorytmu szyfrowania blokowego (najczęściej) zwykle również na nie wpływają.
Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

W moim przypadku napastnicy pozostawili 4-bajtowy ciąg (1, 0, 0, 0) na końcu każdego zaszyfrowanego pliku, co uprościło zadanie. Do wyszukiwania niezainfekowanych plików wystarczył 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)

Okazało się zatem, że znaleziono pliki należące do pierwszego typu. Drugie wymaga dużo pracy ręcznej, ale to, co zostało znalezione, już wystarczyło. Wszystko byłoby dobrze, ale musisz wiedzieć absolutnie precyzyjna konstrukcja i (oczywiście) pojawił się przypadek, że musiałem pracować z często zmieniającym się stołem. Nikt nie pamiętał, czy zmieniono typ pola, czy dodano nową kolumnę.

Wilds City niestety nie mogło pomóc w takiej sprawie, dlatego powstaje ten artykuł.

Przejdź do rzeczy

Istnieje struktura tabeli sprzed 3 miesięcy, która nie pokrywa się z obecną (ewentualnie jedno pole, ewentualnie 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)
); 

w tym przypadku musisz wyodrębnić:

  • id_point int(11);
  • id_user int(11);
  • date_start DATAGODZINA;
  • date_finish DATAGODZINA.

Do odzyskiwania używana jest analiza bajt po bajcie pliku .ibd, a następnie konwertowanie ich do bardziej czytelnej postaci. Ponieważ aby znaleźć to, czego potrzebujemy, wystarczy przeanalizować typy danych takie jak int i datatime, w artykule zostaną opisane tylko te, ale czasami będziemy odnosić się także do innych typów danych, które mogą pomóc w innych podobnych zdarzeniach.

Problem 1: pola o typach DATETIME i TEXT miały wartości NULL i są po prostu pomijane w pliku, z tego powodu w moim przypadku nie było możliwe określenie struktury do przywrócenia. W nowych kolumnach wartość domyślna wynosiła null, a część transakcji mogła zostać utracona w wyniku ustawienia innodb_flush_log_at_trx_commit = 0, więc określenie struktury wymagałoby dodatkowego czasu.

Problem 2: należy wziąć pod uwagę, że wszystkie wiersze usunięte za pomocą DELETE będą w pliku ibd, ale przy pomocy 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 korzystasz z OPTIMIZE TABLE, prawdopodobnie nie spotkasz się z takim problemem.

Zauważyć, wersja DBMS wpływa na sposób przechowywania danych i ten przykład może nie działać w przypadku innych głównych wersji. W moim przypadku użyto wersji mariadb 10.1.24 dla systemu Windows. Ponadto, chociaż w mariadb pracujesz z tabelami InnoDB, w rzeczywistości tak jest XtraDB, co wyklucza możliwość zastosowania metody z InnoDB mysql.

Analiza plików

W Pythonie typ danych bajty() wyświetla dane Unicode zamiast zwykłego zestawu liczb. Chociaż możesz wyświetlić plik w tej formie, dla wygody możesz przekonwertować bajty na postać liczbową, konwertując tablicę bajtów na zwykłą tablicę (list(example_byte_array)). W każdym razie obie metody nadają się do analizy.

Po przejrzeniu kilku plików ibd możesz znaleźć następujące informacje:

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

Co więcej, jeśli podzielisz plik według tych słów kluczowych, otrzymasz w większości nawet bloki danych. Użyjemy infimum jako dzielnika.

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

Ciekawa obserwacja: dla tabel z małą ilością danych, pomiędzy dolną i górną częścią znajduje się wskaźnik ilości wierszy w bloku.

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd — stół testowy z 1. rzędem

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd - stół testowy z 2 rzędami

Tablicę tablicową wierszy [0] można pominąć. Po przejrzeniu nadal nie udało mi się znaleźć surowych danych w tabeli. Najprawdopodobniej ten blok służy do przechowywania indeksów i kluczy.
Zaczynając od tabeli[1] i tłumacząc ją na tablicę numeryczną, można już zauważyć pewne wzorce, a mianowicie:

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

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żesz określić liczbę za pomocą następującej funkcji. Scenariusz:

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

Naprzykład 128, 0, 0, 1 = 1Lub 128, 0, 75, 108 = 19308.
Tabela miała klucz podstawowy z autoinkrementacją i można go również znaleźć tutaj

Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd

Po porównaniu danych z tabel testowych okazało się, że obiekt DATETIME składa się z 5 bajtów i zaczyna się od 153 (najprawdopodobniej oznaczających przedziały roczne). Ponieważ zakres DATTIME to „1000-01-01” do „9999-12-31”, myślę, że liczba bajtów może się różnić, ale w moim przypadku dane przypadają na okres od 2016 do 2019 r., więc założymy wystarczy 5 bajtów.

Aby wyznaczyć czas bez sekund, napisano następujące funkcje. Scenariusz:

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 dało się napisać funkcji funkcjonalnej dla roku i miesiąca, więc musiałem to zhakować. Scenariusz:

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

Jestem pewien, że jeśli poświęcisz n czasu, to nieporozumienie można naprawić.
Następnie funkcja zwracająca obiekt typu datetime z ciągu znaków. Scenariusz:

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 Odzyskiwanie danych z tabel XtraDB bez pliku struktury za pomocą analizy bajt po bajcie pliku ibd, wygląda na to, że tego właśnie potrzebujesz. Co więcej, taka sekwencja nie powtarza się dwa razy w wierszu.

Za pomocą wyrażenia regularnego znajdujemy niezbędne 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)

Należy pamiętać, że podczas wyszukiwania za pomocą tego wyrażenia nie będzie możliwe określenie wartości NULL w wymaganych polach, ale w moim przypadku nie jest to krytyczne. Następnie przechodzimy przez to, co znaleźliśmy w pętli. Scenariusz:

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)

Właściwie to wszystko, dane z tablicy wynikowej to dane, których potrzebujemy. ###PS.###
Rozumiem, że ta metoda nie jest odpowiednia dla wszystkich, ale głównym celem tego artykułu jest szybkie podjęcie działań, a nie rozwiązanie wszystkich problemów. Myślę, że najwłaściwszym rozwiązaniem byłoby samodzielne rozpoczęcie studiowania kodu źródłowego mariadb, ale ze względu na ograniczony czas obecna metoda wydawała się najszybsza.

W niektórych przypadkach po przeanalizowaniu pliku będzie można określić przybliżoną strukturę i przywrócić ją jedną ze standardowych metod z powyższych linków. Będzie to znacznie bardziej poprawne i spowoduje mniej problemów.

Źródło: www.habr.com

Dodaj komentarz