Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

Préhistoire

Il se trouve que le serveur a été attaqué par un virus ransomware qui, par un « heureux accident », a partiellement laissé intacts les fichiers .ibd (fichiers de données brutes des tables innodb), mais a en même temps complètement crypté les fichiers .fpm ( fichiers de structure). Dans ce cas, .idb pourrait être divisé en :

  • soumis à restauration à l’aide d’outils et de guides standards. Pour de tels cas, il existe un excellent devenir;
  • tables partiellement chiffrées. Il s'agit principalement de grandes tables pour lesquelles (d'après ce que j'ai compris) les attaquants ne disposaient pas de suffisamment de RAM pour un cryptage complet ;
  • Eh bien, des tables entièrement cryptées qui ne peuvent pas être restaurées.

Il était possible de déterminer à quelle option appartiennent les tableaux en l'ouvrant simplement dans n'importe quel éditeur de texte sous l'encodage souhaité (dans mon cas, c'est UTF8) et en visualisant simplement le fichier pour la présence de champs de texte, par exemple :

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

De plus, au début du fichier, vous pouvez observer un grand nombre de 0 octets, et les virus qui utilisent l'algorithme de cryptage par blocs (le plus courant) les affectent généralement également.
Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

Dans mon cas, les attaquants ont laissé une chaîne de 4 octets (1, 0, 0, 0) à la fin de chaque fichier crypté, ce qui a simplifié la tâche. Pour rechercher des fichiers non infectés, le script suffisait :

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)

Ainsi, il s'est avéré trouver des fichiers appartenant au premier type. La seconde implique beaucoup de travail manuel, mais ce qui a été trouvé était déjà suffisant. Tout irait bien, mais tu dois savoir structure absolument précise et (bien sûr) un cas s'est produit où je devais travailler avec une table qui changeait fréquemment. Personne ne se souvenait si le type de champ avait été modifié ou si une nouvelle colonne avait été ajoutée.

Wilds City, malheureusement, n'a pas pu aider dans un tel cas, c'est pourquoi cet article est en cours de rédaction.

Plus près du point

Il existe une structure de table d'il y a 3 mois qui ne coïncide pas avec la structure actuelle (éventuellement un champ, voire plus). Structure du tableau :

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

dans ce cas, vous devez extraire :

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

Pour la récupération, une analyse octet par octet du fichier .ibd est utilisée, suivie d'une conversion sous une forme plus lisible. Puisque pour trouver ce dont nous avons besoin, il suffit d'analyser les types de données tels que int et datatime, l'article ne les décrira que, mais parfois nous ferons également référence à d'autres types de données, qui peuvent aider dans d'autres incidents similaires.

Problème 1: les champs de types DATETIME et TEXT avaient des valeurs NULL, et ils sont simplement ignorés dans le fichier, de ce fait, il n'a pas été possible de déterminer la structure à restaurer dans mon cas. Dans les nouvelles colonnes, la valeur par défaut était nulle et une partie de la transaction pouvait être perdue en raison du paramètre innodb_flush_log_at_trx_commit = 0, il faudrait donc consacrer plus de temps pour déterminer la structure.

Problème 2: il faut tenir compte du fait que les lignes supprimées via DELETE seront toutes dans le fichier ibd, mais avec ALTER TABLE leur structure ne sera pas mise à jour. De ce fait, la structure des données peut varier du début à la fin du fichier. Si vous utilisez souvent OPTIMIZE TABLE, il est peu probable que vous rencontriez un tel problème.

Noter, la version du SGBD affecte la façon dont les données sont stockées et cet exemple peut ne pas fonctionner pour d'autres versions majeures. Dans mon cas, la version Windows de mariadb 10.1.24 a été utilisée. De plus, bien que dans mariadb vous travailliez avec des tables InnoDB, en fait elles sont XtraDB, ce qui exclut l'applicabilité de la méthode avec InnoDB mysql.

analyse de fichier

En python, type de données octets() affiche les données Unicode à la place d’un ensemble régulier de nombres. Bien que vous puissiez afficher le fichier sous cette forme, pour plus de commodité, vous pouvez convertir les octets sous forme numérique en convertissant le tableau d'octets en un tableau normal (list(example_byte_array)). Dans tous les cas, les deux méthodes conviennent à l’analyse.

Après avoir parcouru plusieurs fichiers ibd, vous pouvez trouver ce qui suit :

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

De plus, si vous divisez le fichier par ces mots-clés, vous obtiendrez pour la plupart des blocs de données égaux. Nous utiliserons infimum comme diviseur.

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

Une observation intéressante : pour les tables avec une petite quantité de données, entre infimum et supremum il y a un pointeur vers le nombre de lignes du bloc.

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd — table de test avec 1ère rangée

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd - table de test à 2 lignes

La table de tableau de lignes [0] peut être ignorée. Après l'avoir parcouru, je n'ai toujours pas réussi à trouver les données brutes du tableau. Très probablement, ce bloc est utilisé pour stocker des index et des clés.
En commençant par table[1] et en le traduisant en un tableau numérique, vous pouvez déjà remarquer certains modèles, à savoir :

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

Ce sont des valeurs int stockées dans une chaîne. Le premier octet indique si le nombre est positif ou négatif. Dans mon cas, tous les chiffres sont positifs. À partir des 3 octets restants, vous pouvez déterminer le nombre à l'aide de la fonction suivante. Scénario:

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

Par exemple, 128, 0, 0, 1 = 1Ou 128, 0, 75, 108 = 19308.
La table avait une clé primaire avec incrémentation automatique, et elle peut également être trouvée ici

Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd

Après avoir comparé les données des tables de test, il a été révélé que l'objet DATETIME se compose de 5 octets et commence par 153 (indiquant très probablement des intervalles annuels). Étant donné que la plage DATTIME va de « 1000-01-01 » à « 9999-12-31 », je pense que le nombre d'octets peut varier, mais dans mon cas, les données se situent entre 2016 et 2019, nous supposerons donc que 5 octets suffisent.

Pour déterminer le temps sans secondes, les fonctions suivantes ont été écrites. Scénario:

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}

Il n'était pas possible d'écrire une fonction fonctionnelle pour l'année et le mois, j'ai donc dû la pirater. Scénario:

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

Je suis sûr que si vous y consacrez du temps, ce malentendu peut être corrigé.
Ensuite, une fonction qui renvoie un objet datetime à partir d'une chaîne. Scénario:

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)

Géré pour détecter les valeurs fréquemment répétées de int, int, datetime, datetime Récupération de données à partir de tables XtraDB sans fichier de structure à l'aide d'une analyse octet par octet du fichier ibd, il semble que c'est ce dont vous avez besoin. De plus, une telle séquence n’est pas répétée deux fois par ligne.

A l'aide d'une expression régulière, on trouve les données nécessaires :

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)

Veuillez noter que lors d'une recherche à l'aide de cette expression, il ne sera pas possible de déterminer les valeurs NULL dans les champs obligatoires, mais dans mon cas, ce n'est pas critique. Ensuite, nous parcourons ce que nous avons trouvé en boucle. Scénario:

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)

En fait, c'est tout, les données du tableau de résultats sont les données dont nous avons besoin. ###PS.###
Je comprends que cette méthode ne convient pas à tout le monde, mais l'objectif principal de l'article est de vous inciter à agir plutôt que de résoudre tous vos problèmes. Je pense que la solution la plus correcte serait de commencer à étudier le code source vous-même mariadb, mais en raison du temps limité, la méthode actuelle semble être la plus rapide.

Dans certains cas, après avoir analysé le fichier, vous pourrez déterminer la structure approximative et la restaurer en utilisant l'une des méthodes standards à partir des liens ci-dessus. Ce sera beaucoup plus correct et posera moins de problèmes.

Source: habr.com

Ajouter un commentaire