Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Antefatti

È successo che il server è stato attaccato da un virus ransomware, che per "fortuna", ha parzialmente lasciato intatti i file .ibd (file dei dati grezzi delle tabelle innodb), ma ha completamente crittografato i file .fpm (file di struttura). A questo punto, il file .idb poteva essere suddiviso in:

  • recuperabili tramite mezzi e guide standard. Per questi casi, c'è un'ottima storia;
  • tabelle parzialmente crittografate. Principalmente si tratta di tabelle grandi, su cui (per quanto ho capito), gli aggressori non avevano abbastanza memoria RAM per una crittografia completa;
  • e tabelle completamente crittografate, non recuperabili.

Determinare a quale delle due categorie appartengono le tabelle è riuscito banalmente aprendo il file in qualsiasi editor di testo con la codifica adeguata (nel mio caso è UTF8) e semplicemente esaminando il file per la presenza di campi testuali, ad esempio:

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Inoltre, all'inizio del file si può osservare una grande quantità di byte zero, e i virus che utilizzano algoritmi di crittografia a blocchi (che sono i più comuni) di solito li toccano.
Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Nel mio caso, gli aggressori lasciavano alla fine di ciascun file crittografato una stringa di 4 byte (1, 0, 0, 0), il che ha semplificato il compito. Per cercare file non infettati è bastato uno script:

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)

Così sono riuscito a trovare i file appartenenti al primo tipo. Il secondo implica un lungo lavoro manuale, ma ciò che era già stato trovato era sufficiente. Tutto bene, ma era necessario conoscere la struttura esatta e (ovviamente) si è presentato il caso in cui è stato necessario lavorare con una tabella in continuo cambiamento. Nessuno si ricordava se il tipo di campo fosse cambiato o se fosse stata aggiunta una nuova colonna.

Purtroppo, i tecnici di Debris City non sono riusciti ad aiutare con questo caso, ecco perché viene scritta quest'articolo.

Passando al concreto

C'è una struttura della tabella risalente a tre mesi fa che non coincide con quella attuale (forse per un campo, o forse di più). La struttura della tabella:

CREA UNA TABELLA `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)
); 

in questo caso, è necessario estrarre:

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

Per il ripristino, viene utilizzata un'analisi byte per byte del file .ibd, seguita dalla conversione in una forma più leggibile. Poiché, per cercare il necessario, è sufficiente analizzare tipi di dati come int e datetime, in questo articolo saranno descritti solo questi, ma a volte farò riferimento anche ad altri tipi di dati, che potrebbero essere utili in altre situazioni simili.

Problema 1: nei campi con i tipi DATETIME e TEXT erano presenti valori NULL, e nel file sono semplicemente ignorati; per questo motivo, non sono riuscito a determinare la struttura per il ripristino nel mio caso. Nelle nuove colonne, il valore predefinito era null, e parte della transazione potrebbe essere stata persa a causa dell'impostazione innodb_flush_log_at_trx_commit = 0, quindi sarebbe stato necessario spendere tempo aggiuntivo per determinare la struttura.

Problema 2: va tenuto presente che le righe eliminate tramite DELETE si trovano comunque nel file ibd, ma quando si utilizza ALTER TABLE la loro struttura non verrà aggiornata. Di conseguenza, la struttura dei dati potrebbe variare dall'inizio alla fine del file. Se utilizzi spesso OPTIMIZE TABLE, è poco probabile che tu abbia problemi simili.

Si prega di notare, la versione del DBMS influisce sul modo in cui i dati sono memorizzati, e questo esempio potrebbe non funzionare con altre versioni principali. Nel mio caso, è stata utilizzata la versione per Windows di mariadb 10.1.24. Inoltre, anche se in mariadb lavori con tabelle InnoDB, in realtà esse sono XtraDB, il che esclude l'applicabilità del metodo con InnoDB mysql.

Analisi del file

In python, il tipo di dato bytes() visualizza i dati in unicode invece del classico insieme di numeri. Sebbene il file possa essere esaminato in questo modo, per comodità è possibile tradurre i byte in forma numerica convertendo l'array di byte in un array normale (list(example_byte_array)). In ogni caso, per l'analisi sono utili entrambi i metodi.

Dopo aver esaminato diversi file ibd, è possibile incontrare quanto segue:

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Inoltre, se si divide il file in base a queste parole chiave, si ottengono principalmente blocchi di dati uniformi. Utilizzeremo infimum come divisore.

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

Osservazione interessante, per le tabelle con un numero ridotto di dati, tra infimum e supremum c'è un indicatore sul numero di righe nel blocco.

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd — tabella di test con 1 riga

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd — tabella di test con 2 righe

L'array di stringhe table[0] può essere ignorato. Dopo averlo esaminato, non sono riuscito a trovare i dati grezzi delle tabelle. Probabilmente, questo blocco serve a memorizzare indici e chiavi.
Iniziando da table[1] e traducendola in un array numerico, è già possibile notare alcune regolarità, a sapere:

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Questi sono valori interi memorizzati nella stringa. Il primo byte indica se il numero è positivo o negativo. Nel mio caso, tutti i numeri sono positivi. Dagli altri 3 byte, è possibile determinare il numero utilizzando la seguente funzione. Script:

def find_int(val: str):  # esempio '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

Ad esempio, 128, 0, 0, 1 = 1, o 128, 0, 75, 108 = 19308.
Nella tabella era presente una chiave primaria con auto-incremento, e qui è possibile trovarla.

Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd

Confrontando i dati delle tabelle di test, è emerso che l'oggetto DATETIME consiste di 5 byte che iniziano con 153 (probabilmente indica intervalli annuali). Poiché l'intervallo di DATETIME è ‘1000-01-01’ a ‘9999-12-31’, penso che il numero di byte possa variare, ma nel mio caso, i dati rientrano nell'intervallo dal 2016 al 2019, quindi considereremo che 5 byte siano sufficienti.

Per determinare il tempo senza secondi, sono state scritte le seguenti funzioni. 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}

Per l'anno e il mese non sono riuscito a scrivere una funzione funzionante, quindi ho dovuto hardcodare. 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

Sono sicuro che, se si dedica un certo tempo, anche questo malinteso può essere risolto.
Di seguito, una funzione che restituisce un oggetto datetime da una stringa. Script:

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)

Sono stati rilevati valori ripetitivi da int, int, datetime, datetime Recupero dei dati da tabelle XtraDB senza file di struttura, utilizzando un'analisi byte per byte del file ibd, sembra proprio quello che ci serve. Inoltre, tale sequenza non si ripete due volte nella stringa.

Utilizzando un'espressione regolare, troviamo i dati necessari:

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)

Si prega di notare che nella ricerca di questa espressione, non sarà possibile determinare i valori NULL nei campi richiesti, ma nel mio caso questo non è critico. Successivamente, nell'anello, elaboriamo i risultati trovati. Script:

risultato = []
per val in multati:
    pre_risultato = []
    bd_int  = re.findall(r"128, d*, d*, d*", val)
    bd_data = re.findall(r"(153, 1[6,5,4,3]d, d*, d*, d*)", val)
    per it in bd_int:
        pre_risultato.append(find_int(bd_int[it]))
    per bd in bd_data:
        pre_risultato.append(find_data_time(bd))
    risultato.append(pre_risultato)

In realtà, tutti i dati dall'array risultato sono quelli di cui abbiamo bisogno. ###PS.###
Comprendo che questo metodo potrebbe non essere adatto a tutti, ma l'obiettivo principale dell'articolo è piuttosto stimolare all'azione che risolvere tutti i vostri problemi. Penso che la soluzione più corretta sarebbe cominciare a studiare il codice sorgente stesso mariadb, ma a causa del tempo limitato, l'approccio attuale è sembrato il più veloce.

In alcuni casi, analizzando il file, potete determinare la struttura approssimativa e ricostruirla in uno dei modi standard elencati sopra. Questo sarà molto più corretto e comporterà meno problemi.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster