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

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

Sfondo

È successo così che il server è stato attaccato da un virus ransomware che, per un "fortunato incidente", ha parzialmente lasciato intatti i file .ibd (file di dati grezzi delle tabelle innodb), ma allo stesso tempo ha crittografato completamente i file .fpm ( file di struttura). In questo caso il file .idb potrebbe essere suddiviso in:

  • soggetto a restauro tramite strumenti e guide standard. Per questi casi, c'è un eccellente diventare;
  • tabelle parzialmente crittografate. Per lo più si tratta di tabelle di grandi dimensioni, per le quali (a quanto ho capito) gli aggressori non avevano abbastanza RAM per la crittografia completa;
  • Bene, tabelle completamente crittografate che non possono essere ripristinate.

È stato possibile determinare a quale opzione appartengono le tabelle semplicemente aprendolo in qualsiasi editor di testo con la codifica desiderata (nel mio caso è UTF8) e semplicemente visualizzando il file per la presenza di campi di testo, ad esempio:

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

Inoltre, all'inizio del file è possibile osservare un gran numero di 0 byte e i virus che utilizzano l'algoritmo di crittografia a blocchi (il più comune) di solito colpiscono anche loro.
Recupero dei dati dalle tabelle XtraDB senza un file di struttura utilizzando l'analisi byte per byte del file ibd

Nel mio caso, gli aggressori hanno lasciato una stringa di 4 byte (1, 0, 0, 0) alla fine di ogni file crittografato, semplificando così il compito. Per cercare file non infetti era sufficiente lo 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)

Pertanto, si è scoperto che sono stati trovati file appartenenti al primo tipo. La seconda prevede molto lavoro manuale, ma quello che è stato trovato era già sufficiente. Andrebbe tutto bene, ma devi saperlo struttura assolutamente precisa e (ovviamente) si è verificato un caso in cui ho dovuto lavorare con un fasciatoio frequente. Nessuno ricordava se il tipo di campo era stato modificato o se era stata aggiunta una nuova colonna.

Wilds City, sfortunatamente, non ha potuto aiutare in un caso del genere, motivo per cui è stato scritto questo articolo.

Più vicino al punto

Esiste una struttura di una tabella di 3 mesi fa che non coincide con quella attuale (forse un campo e forse più). Struttura della tabella:

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

in questo caso è necessario estrarre:

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

Per il ripristino, viene utilizzata un'analisi byte per byte del file .ibd, seguita dalla loro conversione in un formato più leggibile. Poiché per trovare ciò di cui abbiamo bisogno dobbiamo solo analizzare tipi di dati come int e datatime, l'articolo descriverà solo questi, ma a volte faremo riferimento anche ad altri tipi di dati, che possono aiutare in altri incidenti simili.

Problema 1: i campi con tipo DATETIME e TEXT avevano valori NULL e vengono semplicemente saltati nel file, per questo motivo nel mio caso non è stato possibile determinare la struttura da ripristinare. Nelle nuove colonne, il valore predefinito era null e parte della transazione potrebbe andare persa a causa dell'impostazione innodb_flush_log_at_trx_commit = 0, quindi sarebbe necessario ulteriore tempo per determinare la struttura.

Problema 2: va tenuto presente che le righe cancellate tramite DELETE saranno tutte nel file ibd, ma con ALTER TABLE la loro struttura non verrà aggiornata. Di conseguenza, la struttura dei dati può variare dall'inizio alla fine del file. Se usi spesso OPTIMIZE TABLE, è improbabile che riscontri un problema del genere.

Nota, la versione del DBMS influisce sulla modalità di archiviazione dei dati e questo esempio potrebbe non funzionare per altre versioni principali. Nel mio caso è stata utilizzata la versione Windows di mariadb 10.1.24. Inoltre, sebbene in mariadb lavori con le tabelle InnoDB, in effetti lo sono XtraDB, che esclude l'applicabilità del metodo con InnoDB mysql.

Analisi dei file

In Python, tipo di dati byte () visualizza i dati Unicode al posto di un normale set di numeri. Sebbene sia possibile visualizzare il file in questo formato, per comodità è possibile convertire i byte in forma numerica convertendo l'array di byte in un array regolare (list(example_byte_array)). In ogni caso, entrambi i metodi sono adatti per l'analisi.

Dopo aver esaminato diversi file ibd, puoi trovare quanto segue:

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

Inoltre, se dividi il file per queste parole chiave, otterrai per lo più blocchi di dati uniformi. Useremo il minimo come divisore.

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

Un'osservazione interessante: per le tabelle con una piccola quantità di dati, tra il minimo e il massimo c'è un puntatore al numero di righe nel blocco.

Recupero dei dati dalle tabelle XtraDB senza un file di struttura utilizzando l'analisi byte per byte del file ibd — tabella di prova con 1a riga

Recupero dei dati dalle tabelle XtraDB senza un file di struttura utilizzando l'analisi byte per byte del file ibd - tabella di prova con 2 righe

La tabella dell'array di righe[0] può essere saltata. Dopo averlo esaminato, non sono ancora riuscito a trovare i dati grezzi della tabella. Molto probabilmente, questo blocco viene utilizzato per archiviare indici e chiavi.
Partendo da table[1] e traducendolo in un array numerico, puoi già notare alcuni schemi, vale a dire:

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

Questi sono valori int memorizzati in una stringa. Il primo byte indica se il numero è positivo o negativo. Nel mio caso tutti i numeri sono positivi. Dai restanti 3 byte è possibile determinare il numero utilizzando la seguente funzione. Sceneggiatura:

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

Per esempio, 128, 0, 0, 1 = 1O 128, 0, 75, 108 = 19308.
La tabella aveva una chiave primaria con incremento automatico e può essere trovata anche qui

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

Confrontando i dati delle tabelle di test, è emerso che l'oggetto DATETIME è composto da 5 byte e inizia con 153 (molto probabilmente indicando intervalli annuali). Poiché l'intervallo DATTIME va da '1000-01-01' a '9999-12-31', penso che il numero di byte possa variare, ma nel mio caso i dati rientrano nel periodo dal 2016 al 2019, quindi assumeremo quei 5 byte sono sufficienti.

Per determinare l'ora senza secondi, sono state scritte le seguenti funzioni. Sceneggiatura:

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}

Non era possibile scrivere una funzione funzionale per l'anno e il mese, quindi ho dovuto modificarla. Sceneggiatura:

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 dedichi n tempo, questo malinteso può essere corretto.
Successivamente, una funzione che restituisce un oggetto datetime da una stringa. Sceneggiatura:

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)

Gestito per rilevare valori ripetuti frequentemente da int, int, datetime, datetime Recupero dei dati dalle tabelle XtraDB senza un file di struttura utilizzando l'analisi byte per byte del file ibd, sembra che questo sia ciò di cui hai bisogno. Inoltre, tale sequenza non viene ripetuta due volte per riga.

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)

Tieni presente che durante la ricerca utilizzando questa espressione, non sarà possibile determinare valori NULL nei campi richiesti, ma nel mio caso questo non è fondamentale. Quindi esaminiamo ciò che abbiamo trovato in un ciclo. Sceneggiatura:

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)

In realtà, questo è tutto, i dati dell'array dei risultati sono i dati di cui abbiamo bisogno. ###PS.###
Capisco che questo metodo non è adatto a tutti, ma l'obiettivo principale dell'articolo è stimolare l'azione piuttosto che risolvere tutti i tuoi problemi. Penso che la soluzione più corretta sarebbe iniziare a studiare tu stesso il codice sorgente MariaDB, ma a causa del tempo limitato, il metodo attuale sembrava essere il più veloce.

In alcuni casi, dopo aver analizzato il file, sarai in grado di determinarne la struttura approssimativa e ripristinarla utilizzando uno dei metodi standard dai collegamenti sopra. Questo sarà molto più corretto e causerà meno problemi.

Fonte: habr.com

Aggiungi un commento