استعادة البيانات من جداول 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)

وهكذا اتضح العثور على ملفات تنتمي إلى النوع الأول. والثاني ينطوي على الكثير من العمل اليدوي، ولكن ما تم العثور عليه كان كافيا بالفعل. كل شيء سيكون على ما يرام، ولكن عليك أن تعرف هيكل دقيق تماما و(بالطبع) ظهرت حالة اضطررت فيها للعمل مع طاولة تتغير بشكل متكرر. لم يتذكر أحد ما إذا كان قد تم تغيير نوع الحقل أو إضافة عمود جديد.

ولسوء الحظ، لم تتمكن Wilds City من المساعدة في مثل هذه الحالة، ولهذا السبب يتم كتابة هذا المقال.

أوضح ماذا تقصد

هناك بنية جدول منذ 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 كثافة العمليات (11)؛
  • id_user كثافة العمليات (11)؛
  • date_start التاريخ والوقت؛
  • date_finish التاريخ والوقت.

من أجل الاسترداد، يتم استخدام تحليل بايت بايت لملف .ibd، متبوعًا بتحويلها إلى نموذج أكثر قابلية للقراءة. نظرًا لأنه للعثور على ما نحتاج إليه، نحتاج فقط إلى تحليل أنواع البيانات مثل int وdatatime، وسوف تصفها المقالة فقط، ولكن في بعض الأحيان سنشير أيضًا إلى أنواع البيانات الأخرى، والتي يمكن أن تساعد في حوادث أخرى مماثلة.

المشكلة 1: تحتوي الحقول ذات النوعين DATETIME وTEXT على قيم NULL، ويتم تخطيها ببساطة في الملف، ولهذا السبب، لم يكن من الممكن تحديد البنية المراد استعادتها في حالتي. في الأعمدة الجديدة، كانت القيمة الافتراضية فارغة، وقد يتم فقدان جزء من المعاملة بسبب الإعداد innodb_flush_log_at_trx_commit = 0، لذلك يجب قضاء وقت إضافي لتحديد البنية.

المشكلة 2: يجب أن يؤخذ في الاعتبار أن الصفوف المحذوفة عبر DELETE ستكون جميعها موجودة في ملف ibd، ولكن مع ALTER TABLE لن يتم تحديث بنيتها. ونتيجة لذلك، يمكن أن تختلف بنية البيانات من بداية الملف إلى نهايته. إذا كنت تستخدم OPTIMIZE TABLE غالبًا، فمن غير المرجح أن تواجه مثل هذه المشكلة.

لاحظ، يؤثر إصدار نظام إدارة قواعد البيانات (DBMS) على طريقة تخزين البيانات، وقد لا يعمل هذا المثال مع الإصدارات الرئيسية الأخرى. في حالتي، تم استخدام إصدار Windows من mariadb 10.1.24. أيضًا، على الرغم من أنك تعمل في mariadb مع جداول InnoDB، إلا أنها في الواقع كذلك XtraDB، مما يستثني إمكانية تطبيق الطريقة مع InnoDB mysql.

تحليل ملف

في بايثون، نوع البيانات بايت () يعرض بيانات Unicode بدلاً من مجموعة عادية من الأرقام. على الرغم من أنه يمكنك عرض الملف بهذا النموذج، إلا أنه من أجل الراحة، يمكنك تحويل البايتات إلى نموذج رقمي عن طريق تحويل مصفوفة البايت إلى مصفوفة عادية (list(example_byte_array)). على أية حال، كلتا الطريقتين مناسبتان للتحليل.

بعد البحث في العديد من ملفات ibd، يمكنك العثور على ما يلي:

استعادة البيانات من جداول XtraDB بدون ملف هيكلي باستخدام تحليل بايت بايت لملف ibd

علاوة على ذلك، إذا قمت بتقسيم الملف حسب هذه الكلمات الرئيسية، فستحصل في الغالب على كتل متساوية من البيانات. سوف نستخدم infimum كمقسوم.

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

ملاحظة مثيرة للاهتمام: بالنسبة للجداول التي تحتوي على كمية صغيرة من البيانات، يوجد بين الحد الأدنى والأعلى مؤشر لعدد الصفوف في الكتلة.

استعادة البيانات من جداول XtraDB بدون ملف هيكلي باستخدام تحليل بايت بايت لملف ibd — جدول الاختبار مع الصف الأول

استعادة البيانات من جداول XtraDB بدون ملف هيكلي باستخدام تحليل بايت بايت لملف ibd - جدول اختبار مكون من صفين

يمكن تخطي جدول صفيف الصف[0]. بعد البحث فيه، لم أتمكن من العثور على بيانات الجدول الأولية. على الأرجح، يتم استخدام هذه الكتلة لتخزين الفهارس والمفاتيح.
بدءًا من الجدول [1] وترجمته إلى مصفوفة رقمية، يمكنك بالفعل ملاحظة بعض الأنماط، وهي:

استعادة البيانات من جداول XtraDB بدون ملف هيكلي باستخدام تحليل بايت بايت لملف ibd

هذه هي قيم int المخزنة في سلسلة. يشير البايت الأول إلى ما إذا كان الرقم موجبًا أم سالبًا. في حالتي، جميع الأرقام إيجابية. من البايتات الثلاث المتبقية، يمكنك تحديد الرقم باستخدام الوظيفة التالية. النصي:

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" إلى "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

أنا متأكد من أنه إذا قضيت قدرًا كبيرًا من الوقت، فيمكن تصحيح سوء الفهم هذا.
بعد ذلك، دالة تقوم بإرجاع كائن التاريخ والوقت من سلسلة. النصي:

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)

يرجى ملاحظة أنه عند البحث باستخدام هذا التعبير، لن يكون من الممكن تحديد القيم الخالية في الحقول المطلوبة، ولكن في حالتي هذا ليس بالغ الأهمية. ثم نستعرض ما وجدناه في الحلقة. النصي:

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)

في الواقع، هذا كل شيء، البيانات من مصفوفة النتائج هي البيانات التي نحتاجها. ###ملاحظة.###
أدرك أن هذه الطريقة ليست مناسبة للجميع، ولكن الهدف الرئيسي من المقالة هو حثك على اتخاذ إجراء بدلاً من حل جميع مشاكلك. أعتقد أن الحل الصحيح هو البدء في دراسة الكود المصدري بنفسك MariaDB ل، ولكن نظرًا لضيق الوقت، يبدو أن الطريقة الحالية هي الأسرع.

في بعض الحالات، بعد تحليل الملف، ستتمكن من تحديد البنية التقريبية واستعادتها باستخدام إحدى الطرق القياسية من الروابط أعلاه. سيكون هذا أكثر صحة ويسبب مشاكل أقل.

المصدر: www.habr.com

إضافة تعليق