Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

перадгісторыя

Так адбылося, што сервер быў атакаваны вірусам шыфравальшчыкам, які па "шчаслівай выпадковасці", часткова адставіў не кранутымі файлы .ibd (файлы волкіх дадзеных innodb табліц), але пры гэтым цалкам зашыфраваў файлы .fpm (файлы структур). Пры гэтым .idb можна было падзяліць на:

  • якія падлягаюць аднаўленню праз стандартныя сродкі і гайды. Для такіх выпадкаў, ёсць выдатная стацца;
  • часткова зашыфраваныя табліцы. Пераважна гэта вялікія табліцы, на якія (як я зразумеў), зламыснікамі не хапіла аператыўнай памяці на поўнае шыфраванне;
  • ну і цалкам зашыфраваныя табліцы, якія не падлягаюць аднаўленню.

Вызначыць да якога з варыянту ставяцца табліцы атрымалася банальным адкрыўшы ў любым тэкставым рэдактары пад патрэбнай кадоўкай (у маім выпадку гэта UTF8) і проста прагледзець файл на наяўнасць тэкставых палёў, напрыклад:

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

Таксама, у пачатку файла можна назіраць вялікую колькасць 0-вых байт, а вірусы выкарыстоўвалыя алгарытм блокавага шыфравання (найболей распаўсюджана), звычайна і іх закранаюць.
Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

У маім выпадку, зламыснікі ў канцы кожнага зашыфраванага файла пакідалі радок з 4 байт (1, 0, 0, 0), што спрасціла задачу. Для пошуку не заражаных файлаў хапіла і скрыпту:

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)

Такім чынам атрымалася знайсці файлы прыналежныя да першага тыпу. Другі мае на ўвазе доўгую мануальшчыну, але знойдзенага ўжо было дастаткова. Усё б добра, але неабходна ведаць абсалютна дакладную структуру і (зразумела) выйшаў такі выпадак, што прыйшлося працаваць з часта якая змяняецца табліцай. Ніхто і не памятаў, ці тып поля мяняўся, ці дабаўляўся новы слупок.

Нетры сіці нажаль не змаглі дапамагчы з такім выпадкам, таму і пішацца дадзены артыкул.

Бліжэй да справы

Ёсць структура табліцы 3-х месячнай даўнасці не якая супадае з бягучай (магчыма па адным полі, а магчымае і больш). Структура табліцы:

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

пры гэтым, трэба атрымаць:

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

Для аднаўленне выкарыстоўваецца пабайтавы аналіз .ibd файла, з наступным перакладам іх у больш чытэльны выгляд. Так як для пошуку патрабаванага, нам дастаткова прааналізаваць такія тыпы дадзеных як int і datatime, у артыкуле будуць апісаны толькі яны, але часам будуць спасылацца і на іншыя тыпы дадзеных, што можа дапамагчы ў іншых падобных здарэннях.

Праблема 1: у палях з тыпамі DATETIME і TEXT меліся NULL значэнне, і ў файле яны проста прапускаюцца, з-за гэтага, вызначыць структуру для ўзнаўлення ў маім выпадку не атрымалася. У новых слупках значэнне па змаўчанні было null, а частка транзакцыя магла быць страчаная з-за налады innodb_flush_log_at_trx_commit = 0, таму для вызначэння структуры прыйшлося б выдаткаваць дадатковы час.

Праблема 2: варта ўлічыць, што радкі выдаленыя праз DELETE, усё роўна будуць знаходзіцца ў ibd файле, але пры ALTER TABLE іх структура абновяцца не будзе. У выніку, структура дадзеных можа вар'іравацца ад пачатку файла, да яго канца. Калі вы часта карыстаецеся OPTIMIZE TABLE, то з падобнай праблемай ці наўрад сутыкнецеся.

Звярніце ўвагу, версія СКБД уплывае на спосаб захоўвання дадзеных, і дадзены прыклад можа не спрацаваць для іншых мажорных версій. У маім выпадку выкарыстоўвалася windows версія mariadb 10.1.24. Таксама, хоць і ў mariadb вы працуеце з InnoDB табліцамі, але па факце яны з'яўляюцца XtraDB, Што выключае прымяняльнасць метаду з InnoDB mysql.

Аналіз файла

У python, тып дадзеных bytes() адлюстроўвае дадзеныя ў юнікодзе ў месца звычайнага набору лікаў. Хоць разглядаць файл можна і ў такім выглядзе, але для выгоды можна перавесці байты ў лікавы выгляд перавядучы масіў байт у звычайны масіў (list(example_byte_array)). У любым выпадку, для аналізу прыгладзяцца абодва спосабу.

Прагледзеўшы некалькі ibd файлаў, можна сустрэць наступныя:

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

Пры чым, калі дзяліць файл па гэтых ключавых словах, атрымаюцца пераважна роўныя блокі дадзеных. Будзем выкарыстоўваць infimum як дзельнік.

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

Цікавае назіранне, для табліц з невялікай колькасцю дадзеных, паміж infimum і supremum ёсць паказальнік на колькасць радкоў у блоку.

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла - тэставая табліца з 1-ым радком

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла - тэставая табліца з 2-ма радкамі

Масіў радкоў table[0] можна прапусціць. Праглядзеўшы яго, мне так і не атрымалася выявіць волкія дадзеныя табліц. Хутчэй за ўсё, дадзены блок служыць для захоўвання азначнікаў і ключоў.
Пачынальна з table[1] і перавядучы яе ў лікавы масіў, ужо можна заўважыць некаторыя заканамернасці, а менавіта:

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

Гэта int значэння захоўваюцца ў радку. Першы байт паказвае ці з'яўляецца лік дадатным, ці адмоўным. У маім выпадку, усе лікі станоўчыя. З астатніх 3-х байт, можна вызначыць лік выкарыстоўваючы наступную функцыю. Скрыпт:

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

Напрыклад, 128, 0, 0, 1 = 1, Або 128, 0, 75, 108 = 19308.
У табліцы меўся першасны ключ з аўтаінкрыментам, і тут яго таксама можна выявіць

Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла

Супаставіўшы дадзеныя з тэставых табліц, было выяўлена, што аб'ект DATETIME складаецца з 5 байт пачынаўся са 153 (хутчэй за ўсё паказвае на гадавыя прамежкі). Бо дыяпазон DATTIME роўны '1000-01-01' to '9999-12-31', думаю лік байт можа можа адрознівацца, але ў маім выпадку, дадзеныя падпадаюць у прамежак ад 2016 па 2019 гады, таму будзем лічыць, што 5 байт дастаткова.

Для вызначэння часу без секунд былі напісаныя наступныя функцыі. Скрыпт:

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}

Для года і месяца не ўдалося напісаць разумна-працуючую функцыю, таму давялося харкадзіць. Скрыпт:

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

Упэўнены, калі выдаткаваць n лік часу, тое і гэтае непаразуменне можна выправіць.
Далей, функцыя якая вяртае аб'ект datetime з радка. Скрыпт:

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)

Атрымалася выявіць часта паўтаральныя значэнні з int, int, datetime, datetime Аднаўленне дадзеных з XtraDB табліц без файла структуры, выкарыстоўваючы пабайтавы аналіз ibd файла, падобна гэта тое, што трэба. Прычым такая паслядоўнасць двойчы за радок не паўтараецца.

Выкарыстоўваючы рэгулярнае выраз, знаходзім неабходныя дадзеныя:

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)

Звярніце ўвагу, што пры пошуку па дадзеным выразе, не атрымаецца вызначыць NULL значэння ў патрабаваных палях, але ў маім выпадку гэта не крытычна. Пасля ў цыкле перабіраем знойдзенае. Скрыпт:

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)

Уласна ўсё, дадзеныя з масіва result, гэта і ёсць неабходныя нам дадзеныя. ###PS.###
Я разумею што такі спосаб падыдзе далёка не ўсім, але асноўная мэта артыкула хутчэй натыкнуць на дзеянне, чым вырашыць усе вашыя праблемы. Думаю найбольш правільнае рашэнне было б пачаць вывучаць зыходны код самой MariaDB, але ў сувязі з абмежаваным часам, бягучы спосаб здаўся найболей хуткі.

У некаторых выпадках, прааналізаваўшы файл, вы зможаце вызначыць прыкладную структуру і аднавіць адным са стандартных спосабаў са спасылак вышэй. Гэта будзе значна правільней і выкліча менш праблем.

Крыніца: habr.com

Дадаць каментар