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

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

Contesto

È accaduto che il server sia stato attaccato da un virus ransomware, che per "felice coincidenza" ha parzialmente lasciato inalterati i file .ibd (file dei dati raw delle tabelle innodb), ma ha completamente crittografato i file .fpm (file di struttura). I file .idb possono essere divisi in:

  • quelli recuperabili tramite strumenti e guide standard. Per tali casi, c'è una soluzione ottima articolo;
  • tabelle parzialmente crittografate. Principalmente si tratta di tabelle grandi, su cui (come ho capito) gli aggressori non hanno avuto abbastanza memoria per una crittografia completa;
  • e tabelle completamente crittografate, non recuperabili.

Per determinare a quale opzione appartengano le tabelle, è sufficiente aprirle in qualsiasi editor di testo con la codifica appropriata (nel mio caso UTF8) e semplicemente controllare il file per la presenza di campi testuali, ad esempio:

Recupero dati da tabelle XtraDB senza file di struttura, utilizzando l'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 l'algoritmo di crittografia a blocchi (il più comune) di solito li colpiscono anch'essi.
Recupero dati da tabelle XtraDB senza file di struttura, utilizzando l'analisi byte per byte del file ibd

Nel mio caso, gli aggressori lasciavano alla fine di ogni file crittografato una stringa di 4 byte (1, 0, 0, 0), il che ha semplificato il compito. Per trovare i file non infetti bastava 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)

In questo modo sono riuscito a trovare i file appartenenti al primo tipo. Il secondo comporta una lunga attività manuale, ma quelli già trovati erano sufficienti. Il problema è che è necessario conoscere la struttura esatta e (ovviamente) si è presentato un caso in cui ho dovuto lavorare con una tabella in continua evoluzione. Nessuno ricordava se il tipo di campo fosse cambiato o se fosse stata aggiunta una nuova colonna.

Purtroppo, Debri City non è riuscita a fornire aiuto in questo caso, ecco perché scrivo questo articolo.

Andando al sodo

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

CREA TABELLA `table_1` (
    `id` INT (11),
    `data` DATETIME ,
    `descrizione` TEXT ,
    `id_punto` INT (11),
    `id_utente` INT (11),
    `data_inizio` DATETIME ,
    `data_fine` DATETIME ,
    `foto` INT (1),
    `id_cliente` INT (11),
    `stato` INT (1),
    `tempo_di_lead` TIME ,
    `stato_invio` TINYINT (4)
); 

in questo caso, è necessario estrarre:

  • id_punto INT (11);
  • id_utente INT (11);
  • data_inizio DATETIME ;
  • data_fine DATETIME .

Per il recupero viene utilizzata un'analisi byte per byte del file .ibd, con successivo trasferimento in una forma più leggibile. Poiché per cercare l'elemento richiesto ci basta 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 aiutare in situazioni simili.

Problema 1: nei campi con tipi DATETIME e TEXT c'erano valori NULL, e nel file vengono semplicemente ignorati, per questo motivo, non sono riuscito a determinare la struttura per il recupero nel mio caso. Nei nuovi 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 per determinare la struttura si sarebbe dovuto impiegare tempo aggiuntivo.

Problema 2: è importante notare che le righe rimosse tramite DELETE rimarranno comunque nel file ibd, ma la loro struttura non verrà aggiornata durante l'ALTER TABLE. Di conseguenza, la struttura dei dati può variare dall'inizio alla fine del file. Se utilizzi frequentemente OPTIMIZE TABLE, è improbabile che tu ti imbatta in un simile problema.

Attenzione, la versione del DBMS influisce sul modo in cui i dati vengono memorizzati e questo esempio potrebbe non funzionare per altre versioni maggiori. Nel mio caso è stata utilizzata la versione Windows di MariaDB 10.1.24. Inoltre, anche se in MariaDB lavori con tabelle InnoDB, di fatto 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 di un normale insieme di numeri. Anche se il file può essere considerato in questo modo, per comodità è possibile convertire i byte in una forma numerica trasformando l'array di byte in un normale array (list(example_byte_array)). In ogni caso, entrambi i metodi saranno utili per l'analisi.

Dopo aver esaminato diversi file ibd, è possibile incontrare i seguenti:

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

Inoltre, se suddividi il file secondo queste parole chiave, otterrai principalmente blocchi di dati relativamente uniformi. Utilizzeremo infimum come divisore.

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

Osservazione interessante, per le tabelle con un numero limitato di dati, tra l'infimum e il supremum c'è un indicatore del numero di righe nel blocco.

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

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

L'array di righe table[0] può essere saltato. Esaminandolo, non sono riuscito a scoprire i dati grezzi delle tabelle. Probabilmente, questo blocco serve per memorizzare indici e chiavi.
Iniziando da table[1] e convertendola in un array numerico, è già possibile notare alcune regolarità, ovvero:

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

Questi sono valori int memorizzati nella riga. 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. 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 anche.

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

Confrontando i dati delle tabelle di test, è emerso che l'oggetto DATETIME consiste di 5 byte e inizia con 153 (probabilmente indica intervalli annuali). Poiché l'intervallo di DATETIME è da '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 consideriamo che 5 byte siano sufficienti.

Per determinare l'ora senza i 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 certo che, se dedicato il giusto tempo, anche questo malinteso si può correggere.
Successivamente, 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)

Riusciti a individuare valori ripetuti da int, int, datetime, datetime Recupero dati da tabelle XtraDB senza file di struttura, utilizzando l'analisi byte per byte del file ibd, sembra proprio ciò che serve. Inoltre, questa 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)

Nota che, cercando con questa espressione, non sarà possibile identificare i valori NULL nei campi richiesti, ma nel mio caso non è critico. Dopo, si scorre il risultato trovato nel ciclo. Script:

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 sostanza, i dati dell'array result sono i dati di cui abbiamo bisogno. ###PS.###
Capisco che questo metodo non sia adatto a tutti, ma l'obiettivo principale dell'articolo è più quello di incoraggiare all'azione piuttosto che risolvere tutti i vostri problemi. Penso che la soluzione migliore sia iniziare a studiare il codice sorgente stesso mariadb, ma a causa del tempo limitato, questo metodo è sembrato il più veloce.

In alcuni casi, analizzando il file, sarete in grado di determinare la struttura approssimativa e recuperare con uno dei metodi standard dai link sopra. Questo sarà molto più corretto e creerà meno problemi.

Fonte: habr.com

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