Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

förhistoria

Det hände så att servern attackerades av ett ransomware-virus, som av en "lyckosam olycka" delvis lämnade .ibd-filerna (rådatafiler från innodb-tabeller) orörda, men samtidigt helt krypterade .fpm-filerna ( strukturfiler). I det här fallet kan .idb delas in i:

  • föremål för restaurering genom standardverktyg och guider. För sådana fall finns det en utmärkt bli;
  • delvis krypterade tabeller. Oftast är dessa stora tabeller, för vilka (som jag förstår) angriparna inte hade tillräckligt med RAM-minne för full kryptering;
  • Tja, helt krypterade tabeller som inte kan återställas.

Det var möjligt att bestämma vilket alternativ tabellerna tillhör genom att helt enkelt öppna det i valfri textredigerare under önskad kodning (i mitt fall är det UTF8) och helt enkelt titta på filen för att se om det finns textfält, till exempel:

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

I början av filen kan du också observera ett stort antal 0 byte, och virus som använder blockkrypteringsalgoritmen (den vanligaste) påverkar vanligtvis dem också.
Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

I mitt fall lämnade angriparna en sträng på 4 byte (1, 0, 0, 0) i slutet av varje krypterad fil, vilket förenklade uppgiften. För att söka efter oinfekterade filer räckte skriptet:

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)

Således visade det sig att hitta filer som tillhörde den första typen. Det andra innebär mycket manuellt arbete, men det som hittats räckte redan. Allt skulle vara bra, men du måste veta helt exakt struktur och (naturligtvis) uppstod ett fall att jag var tvungen att arbeta med ett ofta bytande bord. Ingen kom ihåg om fälttypen ändrades eller en ny kolumn lades till.

Wilds City kunde tyvärr inte hjälpa till med ett sådant fall, varför denna artikel skrivs.

Kom till saken

Det finns en struktur för en tabell från 3 månader sedan som inte sammanfaller med den nuvarande (möjligen ett fält, och möjligen fler). Tabellstruktur:

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

i det här fallet måste du extrahera:

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

För återställning används en byte-för-byte-analys av .ibd-filen, följt av att konvertera dem till en mer läsbar form. Eftersom för att hitta det vi behöver behöver vi bara analysera datatyper som int och datatime, artikeln kommer endast att beskriva dem, men ibland kommer vi även att referera till andra datatyper, vilket kan hjälpa till vid andra liknande incidenter.

Problem 1: fält med typerna DATETIME och TEXT hade NULL-värden, och de hoppades helt enkelt över i filen, på grund av detta var det inte möjligt att bestämma strukturen som skulle återställas i mitt fall. I de nya kolumnerna var standardvärdet null, och en del av transaktionen kunde gå förlorad på grund av inställningen innodb_flush_log_at_trx_commit = 0, så ytterligare tid skulle behöva läggas på att bestämma strukturen.

Problem 2: det bör beaktas att rader raderade via DELETE alla kommer att finnas i ibd-filen, men med ALTER TABLE kommer deras struktur inte att uppdateras. Som ett resultat kan datastrukturen variera från början av filen till slutet. Om du ofta använder OPTIMIZE TABLE, är det osannolikt att du stöter på ett sådant problem.

Observera, DBMS-versionen påverkar hur data lagras, och det här exemplet kanske inte fungerar för andra större versioner. I mitt fall användes Windows-versionen av mariadb 10.1.24. Även om du i mariadb arbetar med InnoDB-tabeller, är de det faktiskt XtraDB, vilket utesluter tillämpligheten av metoden med InnoDB mysql.

Filanalys

I python, datatyp bytes() visar Unicode-data istället för en vanlig uppsättning nummer. Även om du kan visa filen i det här formuläret, kan du för enkelhetens skull konvertera byte till numerisk form genom att konvertera byte-arrayen till en vanlig array (list(example_byte_array)). Båda metoderna lämpar sig i alla fall för analys.

Efter att ha tittat igenom flera ibd-filer kan du hitta följande:

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

Dessutom, om du delar filen med dessa nyckelord, får du mestadels jämna datablock. Vi kommer att använda infimum som en divisor.

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

En intressant observation: för tabeller med en liten mängd data, mellan infimum och supremum finns det en pekare till antalet rader i blocket.

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen — testtabell med 1:a raden

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen - testbord med 2 rader

Radmatristabellen[0] kan hoppas över. Efter att ha tittat igenom den kunde jag fortfarande inte hitta råtabelldata. Troligtvis används detta block för att lagra index och nycklar.
Om du börjar med tabell[1] och översätter den till en numerisk matris kan du redan lägga märke till några mönster, nämligen:

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

Dessa är int-värden lagrade i en sträng. Den första byten anger om talet är positivt eller negativt. I mitt fall är alla siffror positiva. Från de återstående 3 byten kan du bestämma numret med hjälp av följande funktion. Manus:

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

Till exempel, 128, 0, 0, 1 = 1, eller 128, 0, 75, 108 = 19308.
Tabellen hade en primärnyckel med auto-inkrement, och den kan också hittas här

Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen

Efter att ha jämfört data från testtabellerna, avslöjades det att DATETIME-objektet består av 5 byte och började med 153 (mest troligt indikerar årliga intervall). Eftersom DATTIME-intervallet är '1000-01-01' till '9999-12-31' tror jag att antalet byte kan variera, men i mitt fall faller uppgifterna under perioden 2016 till 2019, så vi kommer att anta det räcker med 5 byte.

För att bestämma tiden utan sekunder skrevs följande funktioner. Manus:

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}

Det gick inte att skriva en funktionell funktion för år och månad, så jag var tvungen att hacka den. Manus:

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

Jag är säker på att detta missförstånd kan rättas till om du lägger ner en mängd tid.
Därefter en funktion som returnerar ett datetime-objekt från en sträng. Manus:

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)

Lyckades upptäcka ofta upprepade värden från int, int, datetime, datetime Återställa data från XtraDB-tabeller utan en strukturfil med hjälp av byte-för-byte-analys av ibd-filen, det verkar som att det här är vad du behöver. Dessutom upprepas inte en sådan sekvens två gånger per rad.

Med hjälp av ett reguljärt uttryck hittar vi nödvändiga data:

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)

Observera att när du söker med detta uttryck kommer det inte att vara möjligt att bestämma NULL-värden i de obligatoriska fälten, men i mitt fall är detta inte kritiskt. Sedan går vi igenom det vi hittat i en slinga. Manus:

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)

Egentligen är det allt, data från resultatmatrisen är de data vi behöver. ###PS.###
Jag förstår att den här metoden inte är lämplig för alla, men huvudmålet med artikeln är att uppmana till åtgärder snarare än att lösa alla dina problem. Jag tror att den mest korrekta lösningen skulle vara att börja studera källkoden själv mariadb, men på grund av begränsad tid verkade den nuvarande metoden vara den snabbaste.

I vissa fall, efter att ha analyserat filen, kommer du att kunna bestämma den ungefärliga strukturen och återställa den med en av standardmetoderna från länkarna ovan. Detta kommer att vara mycket mer korrekt och orsaka färre problem.

Källa: will.com

Lägg en kommentar