ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

Bakgrund

Det hÀnde sig att servern blev attackerad av ett ransomware-virus, som av "lycklig slump" delvis lÀmnade .ibd-filer (filer med rÄdata frÄn innodb-tabeller) orörda, men samtidigt helt krypterade .fpm-filer (strukturfiler). DÀrför kunde .idb delas in i:

  • filer som kan Ă„terstĂ€llas via standardmetoder och guider. För sĂ„dana fall finns det en utmĂ€rkt artikel;
  • delvis krypterade tabeller. Det handlar huvudsakligen om stora tabeller, som (som jag förstod det) angriparna inte hade tillrĂ€ckligt med RAM för att kryptera helt;
  • och helt krypterade tabeller, som inte gĂ„r att Ă„terstĂ€lla.

Det gick att bestÀmma vilken av alternativen tabellerna tillhörde genom att enkelt öppna dem i vilken textredigerare som helst med rÀtt kodning (i mitt fall UTF8) och bara kolla filen för textfÀlt, till exempel:

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

Dessutom kan man i början av filen se en stor mÀngd nollor, och virus som anvÀnder blockkrypteringsalgoritmer (de mest förekommande) brukar vanligtvis pÄverka dem ocksÄ.
ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

I mitt fall lÀmnade angriparna en rad av fyra bytes (1, 0, 0, 0) i slutet av varje krypterad fil, vilket gjorde uppgiften enklare. För att hitta de osmittade filerna rÀckte det med ett 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)

SÄledes lyckades jag hitta filer som tillhörde den första typen. Den andra typen innebÀr lÄngvarigt manuellt arbete, men den hittade mÀngden var redan tillrÀcklig. Allt skulle vara bra, men det Àr nödvÀndigt att veta den helt exakta strukturen och (naturligtvis) intrÀffade en sÄdan situation, att jag tvingades arbeta med en ofta förÀndrad tabell. Ingen kom ihÄg om fÀltets typ hade Àndrats eller om en ny kolumn lagts till.

TyvÀrr kunde Debris City inte hjÀlpa med ett sÄdant fall, sÄ denna artikel skrivs.

Till saken

Det finns en tabellstruktur frÄn tre mÄnader sedan som inte stÀmmer överens med nuvarande (kanske pÄ ett fÀlt, eller kanske fler). Tabellstrukturen Àr:

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

Samtidigt mÄste följande extraheras:

  • id_point INT (11);
  • id_user INT (11);
  • date_start DATETIME ;
  • date_finish DATETIME .

För ÄterstÀllning anvÀnds byte-för-byte analys av .ibd-filen, följt av översÀttning av dessa till en mer lÀsbar form. Eftersom det för att söka det vi behöver rÀcker att analysera sÄdana datatyper som int och datetime kommer artikeln endast att beskriva dem, men ibland kommer jag att hÀnvisa till andra datatyper som kan vara till hjÀlp i liknande situationer.

Problem 1: i fÀlten med DATETIME och TEXT typer fanns det NULL-vÀrden, och i filen hoppar de helt enkelt över, vilket gör att det inte gick att bestÀmma strukturen för ÄterstÀllning i mitt fall. I de nya kolumnerna var standardvÀrdet null, och en del av transaktionerna kan ha gÄtt förlorade pÄ grund av instÀllningen innodb_flush_log_at_trx_commit = 0, sÄ för att bestÀmma strukturen skulle man ha behövt lÀgga mer tid.

Problem 2: det bör beaktas att rader som har tagits bort via DELETE fortfarande finns i ibd-filen, men vid ALTER TABLE kommer deras struktur inte att uppdateras. I slutÀndan kan databasstrukturen variera frÄn början av filen till dess slut. Om du ofta anvÀnder OPTIMIZE TABLE kommer du sÀllan att stöta pÄ denna typ av problem.

Observera, versionsnummer av databashanteraren pÄverkar hur data lagras, och detta exempel kanske inte fungerar för andra majorversioner. I mitt fall anvÀndes windows-versionen av mariadb 10.1.24. Dessutom, Àven om du arbetar med InnoDB-tabeller i mariadb, sÄ Àr de faktiska XtraDB, vilket utesluter tillÀmplighet av metoden med InnoDB mysql.

Analys av filen

I python representerar datatypen bytes() visar data i unicode istĂ€llet för en vanlig uppsĂ€ttning siffror. Även om filen kan ses pĂ„ detta sĂ€tt, för bekvĂ€mlighetens skull kan man konvertera bytes till numerisk form genom att omvandla byte-arrayen till en vanlig array (list(example_byte_array)). I vilket fall som helst, för analys kommer bĂ„da metoder att vara till nytta.

Genom att granska flera ibd-filer kan man stöta pÄ följande:

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

Och om man delar filen efter dessa nyckelord fÄr man i huvudsak jÀmna block av data. Vi kommer att anvÀnda infimum som avdelare.

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

Intressant observation, för tabeller med en liten mÀngd data finns det ett pekare mellan infimum och supremum som anger antalet rader i blocket.

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen. — testtabell med 1 rad

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen. — testtabell med 2 rader

Array of strings table[0] can be skipped. Upon reviewing it, I still couldn't find the raw data of the tables. Most likely, this block is used to store indexes and keys.
Starting from table[1] and converting it into a numeric array, certain patterns can already be observed, namely:

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

These are int values stored in a string. The first byte indicates whether the number is positive or negative. In my case, all numbers are positive. From the remaining 3 bytes, the number can be determined using the following function. Script:

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.
The table had a primary key with auto-increment, and it can also be found here.

ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen.

By matching data from the test tables, it was found that the DATETIME object consists of 5 bytes, starting with 153 (most likely indicating annual intervals). Since the DATETIME range is ‘1000-01-01’ to ‘9999-12-31’, I think the number of bytes may vary, but in my case, the data falls within the range from 2016 to 2019, so we will consider that 5 bytes are sufficient.

To determine the time without seconds, the following functions were written. Script:

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}

For the year and month, I couldn't write a functional working function, so I had to hardcode it. Script:

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 om man lÀgger ner n antal timmar kan detta missförstÄnd ocksÄ ÄtgÀrdas.
HÀrnÀst, en funktion som returnerar ett datetime-objekt frÄn en strÀng. 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)

Det har lyckats upptĂ€cka ofta förekommande vĂ€rden frĂ„n int, int, datetime, datetime ÅterstĂ€llning av data frĂ„n XtraDB-tabeller utan strukturfil, med hjĂ€lp av byte-för-byte analys av ibd-filen., verkar som detta Ă€r vad som behövs. Dessutom upprepas en sĂ„dan sekvens inte tvĂ„ gĂ„nger i strĂ€ngen.

Genom att anvÀnda ett reguljÀrt uttryck letar vi efter de nödvÀndiga uppgifterna:

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 vid sökning med detta uttryck gÄr det inte att identifiera NULL-vÀrden i de nödvÀndiga fÀlten, men i mitt fall Àr detta inte kritiskt. EfterÄt itererar vi genom de funna resultaten. 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)

Det Àr faktiskt allt, data frÄn arrayen result Àr de nödvÀndiga uppgifterna för oss. ###PS.###
Jag förstÄr att denna metod inte passar alla, men huvudsyftet med artikeln Àr mer att inspirera till handling Àn att lösa alla era problem. Jag tror att det mest korrekta tillvÀgagÄngssÀttet skulle vara att börja studera kÀllkoden sjÀlv mariadb, men pÄ grund av begrÀnsad tid verkade den aktuella metoden mest snabb.

I vissa fall, efter att ha analyserat filen, kan du avgöra den ungefÀrliga strukturen och ÄterstÀlla den med en av de standardmetoder som nÀmns ovan. Det kommer att vara mycket mer korrekt och orsaka fÀrre problem.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster