Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

prazgodovina

Zgodilo se je, da je strežnik napadel izsiljevalski virus, ki je po »srečnem naključju« delno pustil nedotaknjene datoteke .ibd (datoteke s surovimi podatki tabel innodb), hkrati pa popolnoma šifriral datoteke .fpm ( strukturne datoteke). V tem primeru bi lahko .idb razdelili na:

  • predmet obnove s standardnimi orodji in vodniki. Za takšne primere obstaja odlična postati;
  • delno šifrirane tabele. Večinoma so to velike tabele, za katere (kot razumem) napadalci niso imeli dovolj RAM-a za popolno šifriranje;
  • No, popolnoma šifrirane tabele, ki jih ni mogoče obnoviti.

Kateri možnosti pripadajo tabele, je bilo mogoče ugotoviti tako, da jo preprosto odprete v katerem koli urejevalniku besedila pod želenim kodiranjem (v mojem primeru je to UTF8) in si preprosto ogledate datoteko za prisotnost besedilnih polj, na primer:

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

Prav tako lahko na začetku datoteke opazite veliko število 0 bajtov, virusi, ki uporabljajo algoritem za šifriranje blokov (najpogostejši), pa običajno vplivajo tudi nanje.
Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

V mojem primeru so napadalci na koncu vsake šifrirane datoteke pustili 4-bajtni niz (1, 0, 0, 0), kar je poenostavilo nalogo. Za iskanje neokuženih datotek je bil dovolj skript:

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)

Tako se je izkazalo, da najdemo datoteke, ki pripadajo prvi vrsti. Pri drugem je veliko ročnega dela, a je bilo že najdenega dovolj. Vse bi bilo v redu, vendar morate vedeti popolnoma natančna struktura in (seveda) se je pojavil primer, da sem moral delati s pogosto previjalno mizo. Nihče se ni spomnil, ali je bil spremenjen tip polja ali dodan nov stolpec.

Wilds City žal ni mogel pomagati v takšnem primeru, zato nastaja ta članek.

Pojdi na stvar

Obstaja struktura tabele izpred 3 mesecev, ki se ne ujema s trenutno (morda eno polje in morda več). Struktura tabele:

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)
); 

v tem primeru morate ekstrahirati:

  • id_point int(11);
  • id_user int(11);
  • date_start DATUM ČAS;
  • date_finish DATUM ČAS.

Za obnovitev se uporabi analiza datoteke .ibd po bajtih, ki ji sledi pretvorba v bolj berljivo obliko. Ker moramo, da bi našli tisto, kar potrebujemo, analizirati samo tipe podatkov, kot sta int in datatime, bo članek opisal le njiju, včasih pa se bomo sklicevali tudi na druge tipe podatkov, ki lahko pomagajo pri drugih podobnih incidentih.

Problem 1: polja s tipoma DATETIME in TEXT so imela vrednosti NULL in so v datoteki preprosto preskočena, zaradi tega v mojem primeru ni bilo mogoče določiti strukture za obnovitev. V novih stolpcih je bila privzeta vrednost ničelna in del transakcije bi se lahko izgubil zaradi nastavitve innodb_flush_log_at_trx_commit = 0, zato bi bilo treba za določitev strukture porabiti dodaten čas.

Problem 2: treba je upoštevati, da bodo vse vrstice, izbrisane z DELETE, v datoteki ibd, vendar z ALTER TABLE njihova struktura ne bo posodobljena. Zaradi tega se lahko struktura podatkov razlikuje od začetka do konca datoteke. Če pogosto uporabljate OPTIMIZE TABLE, je malo verjetno, da boste naleteli na takšno težavo.

Обратите внимание, različica DBMS vpliva na način shranjevanja podatkov in ta primer morda ne bo deloval za druge večje različice. V mojem primeru je bila uporabljena Windows različica mariadb 10.1.24. Tudi, čeprav v mariadb delate s tabelami InnoDB, v resnici so XtraDB, kar izključuje uporabnost metode z InnoDB mysql.

Analiza datoteke

V pythonu, vrsta podatkov bajti() prikaže podatke Unicode namesto običajnega niza številk. Čeprav si lahko ogledate datoteko v tej obliki, lahko zaradi priročnosti pretvorite bajte v številsko obliko tako, da pretvorite matriko bajtov v navadno matriko (seznam(example_byte_array)). V vsakem primeru sta obe metodi primerni za analizo.

Po ogledu več datotek ibd lahko najdete naslednje:

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

Poleg tega, če datoteko razdelite na te ključne besede, boste dobili večinoma enakomerne bloke podatkov. Kot delitelj bomo uporabili infimum.

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

Zanimiva ugotovitev: pri tabelah z majhno količino podatkov je med infimumom in supremumom kazalec na število vrstic v bloku.

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd — preskusna miza s 1. vrstico

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd - testna miza z 2 vrsticama

Tabelo niza vrstic[0] lahko preskočite. Ko sem jo pregledal, še vedno nisem mogel najti neobdelanih podatkov tabele. Najverjetneje se ta blok uporablja za shranjevanje indeksov in ključev.
Če začnete s tabelo [1] in jo prevedete v številsko matriko, lahko že opazite nekaj vzorcev, in sicer:

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

To so int vrednosti, shranjene v nizu. Prvi bajt označuje, ali je število pozitivno ali negativno. V mojem primeru so vse številke pozitivne. Iz preostalih 3 bajtov lahko določite številko z naslednjo funkcijo. Skript:

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

Na primer, 128, 0, 0, 1 = 1Ali 128, 0, 75, 108 = 19308.
Tabela je imela primarni ključ s samodejnim povečevanjem in ga najdete tudi tukaj

Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd

Po primerjavi podatkov iz testnih tabel je bilo ugotovljeno, da objekt DATETIME obsega 5 bajtov in se začne s 153 (najverjetneje označuje letne intervale). Ker je obseg DATTIME od '1000-01-01' do '9999-12-31', menim, da se lahko število bajtov razlikuje, vendar v mojem primeru podatki spadajo v obdobje od 2016 do 2019, zato bomo predpostavili da je 5 bajtov dovolj.

Za določitev časa brez sekund so bile napisane naslednje funkcije. Skript:

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}

Funkcionalne funkcije za leto in mesec ni bilo mogoče napisati, zato sem jo moral vdreti. Skript:

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

Prepričan sem, da lahko ta nesporazum popravite, če porabite n časa.
Nato funkcija, ki vrne predmet datuma in časa iz niza. Skript:

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)

Uspelo zaznati pogosto ponavljajoče se vrednosti iz int, int, datetime, datetime Obnovitev podatkov iz tabel XtraDB brez strukturne datoteke z analizo bajt za bajtom datoteke ibd, zdi se, da je to tisto, kar potrebujete. Poleg tega se takšno zaporedje ne ponovi dvakrat na vrstico.

Z uporabo regularnega izraza najdemo potrebne podatke:

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)

Upoštevajte, da pri iskanju s tem izrazom ne bo mogoče določiti vrednosti NULL v zahtevanih poljih, vendar v mojem primeru to ni kritično. Nato gremo skozi tisto, kar smo našli v zanki. Skript:

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)

Pravzaprav je to vse, podatki iz niza rezultatov so podatki, ki jih potrebujemo. ###PS.###
Razumem, da ta metoda ni primerna za vsakogar, vendar je glavni cilj članka spodbuditi ukrepanje in ne rešiti vseh vaših težav. Mislim, da bi bila najbolj pravilna rešitev, da sami začnete preučevati izvorno kodo mariadb, vendar se je zaradi omejenega časa trenutna metoda zdela najhitrejša.

V nekaterih primerih boste po analizi datoteke lahko določili približno strukturo in jo obnovili z eno od standardnih metod iz zgornjih povezav. To bo veliko bolj pravilno in povzročalo manj težav.

Vir: www.habr.com

Dodaj komentar