Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

Pré-história

Acontece que o servidor foi atacado por um vírus ransomware, que, por um “feliz acidente”, deixou parcialmente os arquivos .ibd (arquivos de dados brutos das tabelas innodb) intactos, mas ao mesmo tempo criptografou completamente os arquivos .fpm ( arquivos de estrutura). Neste caso, .idb poderia ser dividido em:

  • sujeito a restauração através de ferramentas e guias padrão. Para esses casos, existe uma excelente tornar-se;
  • tabelas parcialmente criptografadas. Em sua maioria, são tabelas grandes, para as quais (pelo que entendi) os invasores não tinham RAM suficiente para criptografia completa;
  • Bem, tabelas totalmente criptografadas que não podem ser restauradas.

Foi possível determinar a qual opção as tabelas pertencem simplesmente abrindo-a em qualquer editor de texto na codificação desejada (no meu caso é UTF8) e simplesmente visualizando o arquivo quanto à presença de campos de texto, por exemplo:

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

Além disso, no início do arquivo você pode observar um grande número de 0 bytes, e vírus que usam o algoritmo de criptografia de bloco (o mais comum) geralmente os afetam também.
Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

No meu caso, os invasores deixaram uma string de 4 bytes (1, 0, 0, 0) no final de cada arquivo criptografado, o que simplificou a tarefa. Para procurar arquivos não infectados, bastava o 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)

Assim, foram encontrados arquivos pertencentes ao primeiro tipo. A segunda envolve muito trabalho manual, mas o que foi encontrado já foi suficiente. Tudo ficaria bem, mas você precisa saber estrutura absolutamente precisa e (é claro) surgiu um caso em que tive que trabalhar com uma mesa que mudava frequentemente. Ninguém se lembrava se o tipo de campo foi alterado ou se uma nova coluna foi adicionada.

Infelizmente, Wilds City não pôde ajudar nesse caso, e é por isso que este artigo está sendo escrito.

Mais perto de casa

Existe uma estrutura de tabela de 3 meses atrás que não coincide com a atual (possivelmente um campo e possivelmente mais). Estrutura da tabela:

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

neste caso, você precisa extrair:

  • id_point int(11);
  • id_user int(11);
  • date_start DATA HORA;
  • date_finish DATA HORA.

Para recuperação, é usada uma análise byte a byte do arquivo .ibd, seguida de sua conversão para um formato mais legível. Como para encontrar o que precisamos precisamos apenas analisar tipos de dados como int e datatime, o artigo irá descrevê-los apenas, mas às vezes também faremos referência a outros tipos de dados, que podem ajudar em outros incidentes semelhantes.

Problema 1: campos com tipos DATETIME e TEXT possuíam valores NULL, e são simplesmente ignorados no arquivo, por isso não foi possível determinar a estrutura a ser restaurada no meu caso. Nas novas colunas, o valor padrão era nulo e parte da transação poderia ser perdida devido à configuração innodb_flush_log_at_trx_commit = 0, portanto, seria necessário gastar mais tempo para determinar a estrutura.

Problema 2: deve-se levar em consideração que as linhas excluídas via DELETE estarão todas no arquivo ibd, mas com ALTER TABLE sua estrutura não será atualizada. Como resultado, a estrutura de dados pode variar do início ao fim do arquivo. Se você usa OPTIMIZE TABLE com frequência, é improvável que encontre esse problema.

Nota, a versão do DBMS afeta a forma como os dados são armazenados e este exemplo pode não funcionar para outras versões principais. No meu caso, foi usada a versão windows do mariadb 10.1.24. Além disso, embora no mariadb você trabalhe com tabelas InnoDB, na verdade elas são XtraDBName, o que exclui a aplicabilidade do método com InnoDB mysql.

Análise de arquivo

Em python, tipo de dados bytes () exibe dados Unicode no lugar de um conjunto normal de números. Embora você possa visualizar o arquivo neste formato, por conveniência você pode converter os bytes em formato numérico convertendo a matriz de bytes em uma matriz regular (lista(example_byte_array)). Em qualquer caso, ambos os métodos são adequados para análise.

Depois de examinar vários arquivos ibd, você encontrará o seguinte:

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

Além disso, se você dividir o arquivo por essas palavras-chave, obterá principalmente blocos de dados iguais. Usaremos o ínfimo como divisor.

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

Uma observação interessante: para tabelas com pequena quantidade de dados, entre o ínfimo e o supremo existe um ponteiro para o número de linhas do bloco.

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd — tabela de teste com 1ª linha

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd - tabela de teste com 2 linhas

A matriz de linhas table[0] pode ser ignorada. Depois de examiná-lo, ainda não consegui encontrar os dados brutos da tabela. Muito provavelmente, este bloco é usado para armazenar índices e chaves.
Começando pela tabela[1] e traduzindo-a em um array numérico, já é possível notar alguns padrões, a saber:

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

Estes são valores int armazenados em uma string. O primeiro byte indica se o número é positivo ou negativo. No meu caso, todos os números são positivos. Dos 3 bytes restantes, você pode determinar o número usando a seguinte função. Roteiro:

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

Por exemplo, 128, 0, 0, 1 = 1Ou 128, 0, 75, 108 = 19308.
A tabela tinha uma chave primária com incremento automático e também pode ser encontrada aqui

Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd

Depois de comparar os dados das tabelas de teste, foi revelado que o objeto DATETIME consiste em 5 bytes e começou com 153 (provavelmente indicando intervalos anuais). Como o intervalo DATTIME é '1000-01-01' a '9999-12-31', acho que a quantidade de bytes pode variar, mas no meu caso os dados caem no período de 2016 a 2019, então vamos assumir esses 5 bytes são suficientes.

Para determinar o tempo sem segundos, as seguintes funções foram escritas. Roteiro:

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}

Não foi possível escrever uma função funcional para o ano e o mês, então tive que hackear. Roteiro:

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

Tenho certeza de que se você gastar muito tempo, esse mal-entendido poderá ser corrigido.
A seguir, uma função que retorna um objeto datetime de uma string. Roteiro:

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)

Gerenciado para detectar valores repetidos com frequência de int, int, datetime, datetime Recuperando dados de tabelas XtraDB sem um arquivo de estrutura usando análise byte a byte do arquivo ibd, parece que é disso que você precisa. Além disso, tal sequência não é repetida duas vezes por linha.

Usando uma expressão regular, encontramos os dados necessários:

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)

Observe que ao pesquisar usando esta expressão, não será possível determinar valores NULL nos campos obrigatórios, mas no meu caso isso não é crítico. Em seguida, analisamos o que encontramos em um loop. Roteiro:

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)

Na verdade, isso é tudo, os dados da matriz de resultados são os dados que precisamos. ###PS.###
Entendo que esse método não é adequado para todos, mas o objetivo principal do artigo é estimular a ação, em vez de resolver todos os seus problemas. Acho que a solução mais correta seria começar você mesmo a estudar o código-fonte mariadb, mas devido ao tempo limitado, o método atual parecia ser o mais rápido.

Em alguns casos, após analisar o arquivo, você poderá determinar a estrutura aproximada e restaurá-la usando um dos métodos padrão dos links acima. Isso será muito mais correto e causará menos problemas.

Fonte: habr.com

Adicionar um comentário