استخدام عمليات ربط الملفات للنسخ الاحتياطي على نظام التشغيل MacOS أثناء التنقل

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

صورة

لن أبدأ من بعيد ، أستطيع فقط أن أقول إن كل شيء بدأ بمشروع في معهد موسكو للفيزياء والتكنولوجيا ، قمت بتطويره مع المشرف الخاص بي في قسم قاعدة أكرونيس. لقد واجهنا مهمة تنظيم تخزين الملفات عن بُعد ، أو بالأحرى الحفاظ على الحالة الحالية للنسخ الاحتياطية الخاصة بها.

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

صورة

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

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

البحث عن حل آخر


على الرغم من القيود المفروضة على KAUTH ، يسمح لك KPI هذا بالإبلاغ عن استخدام ملف للتسجيل قبل جميع العمليات. يتم منح المطورين حق الوصول إلى تجريد ملف BSD في kernel - vnode. الغريب ، اتضح أن الترقيع vnode أسهل من استخدام تصفية وحدة التخزين. تحتوي بنية vnode على جدول الوظائف التي توفر العمل مع الملفات الحقيقية. لذلك ، كان لدينا فكرة لاستبدال هذا الجدول.

صورة

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

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

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

كيفية البحث عن الإزاحة: طريقة سهلة

إن أبسط طريقة للعثور على إزاحات الجدول في vnode هي إرشادي يستند إلى موقع الحقول في بنية ( رابط إلى Github ).

struct vnode { ... int (**v_op)(void *); /* vnode operations vector */ mount_t v_mount; /* ptr to vfs we are in */ ... } 

سنستخدم افتراض أن حقل v_op الذي نحتاج إليه هو إزالة 8 بايتات بالضبط من v_mount. يمكن الحصول على قيمة الأخير باستخدام KPI العام ( رابط إلى جيثب ):

 mount_t vnode_mount(vnode_t vp); 

مع العلم بقيمة v_mount ، سنبدأ في البحث عن "إبرة في كومة قش" - سوف ندرك قيمة المؤشر إلى vnode 'vp' كما uintptr_t ، وقيمة vnode_mount (vp) كـ uintptr_t. ويلي ذلك تكرارات للقيمة "المعقولة" لـ i ، حتى يتم الوفاء بـ "haystack [i] == needle". وإذا كان الافتراض حول موقع الحقول صحيحًا ، فإن الإزاحة v_op هي i-1.

 void* getVOPPtr(vnode_t vp) { auto haystack = (uintptr_t*) vp; auto needle = (uintptr_t) vnode_mount(vp); for (int i = 0; i < ATTEMPTCOUNT; i++) { if (haystack[i] == needle) { return haystack + (i - 1); } } return nullptr; } 

كيفية البحث عن إزاحة: تفكيك

على الرغم من بساطته ، فإن الطريقة الأولى لها عيب كبير. إذا غيرت Apple ترتيب الحقول في بنية vnode ، فسوف تنقطع الطريقة البسيطة. طريقة أكثر عالمية ، ولكن أقل تافهة هي لتفكيك النواة بشكل حيوي.

على سبيل المثال ، ضع في الاعتبار وظيفة kernel المفككة VNOP_CREATE ( رابط إلى Github ) على نظام التشغيل macOS 10.14.6. يتم تعليم الإرشادات التي تهمنا بسهم ->.

_VNOP_CREATE:
1 push rbp
2 mov rbp, rsp
3 push r15
4 push r14
5 push r13
6 push r12
7 push rbx
8 sub rsp, 0x48
9 mov r15, r8
10 mov r12, rdx
11 mov r13, rsi
-> 12 mov rbx, rdi
13 lea rax, qword [___stack_chk_guard]
14 mov rax, qword [rax]
15 mov qword [rbp+-48], rax
-> 16 lea rax, qword [_vnop_create_desc] ; _vnop_create_desc
17 mov qword [rbp+-112], rax
18 mov qword [rbp+-104], rdi
19 mov qword [rbp+-96], rsi
20 mov qword [rbp+-88], rdx
21 mov qword [rbp+-80], rcx
22 mov qword [rbp+-72], r8
-> 23 mov rax, qword [rdi+0xd0]
-> 24 movsxd rcx, dword [_vnop_create_desc]
25 lea rdi, qword [rbp+-112]
-> 26 call qword [rax+rcx*8]
27 mov r14d, eax
28 test eax, eax
….

 errno_t VNOP_CREATE(vnode_t dvp, vnode_t * vpp, struct componentname * cnp, struct vnode_attr * vap, vfs_context_t ctx) { int _err; struct vnop_create_args a; a.a_desc = &vnop;_create_desc; a.a_dvp = dvp; a.a_vpp = vpp; a.a_cnp = cnp; a.a_vap = vap; a.a_context = ctx; _err = (*dvp->v_op[vnop_create_desc.vdesc_offset])(&a;); … 

سنقوم بمسح إرشادات المجمع لإيجاد التحول في vnode dvp. "الغرض" من رمز المجمّع هو استدعاء دالة من جدول v_op. للقيام بذلك ، يجب أن يتبع المعالج الخطوات التالية:

  1. تحميل DVP للتسجيل
  2. إلغاء تسجيله للحصول على v_op (السطر 23)
  3. الحصول على vnop_create_desc.vdesc_offset (السطر 24)
  4. استدعاء وظيفة (خط 26)

إذا كان كل شيء واضحًا في الخطوات 2-4 ، فستظهر صعوبات في الخطوة الأولى. كيف نفهم سجل dvp الذي تم تحميله؟ للقيام بذلك ، استخدمنا طريقة لمحاكاة دالة تراقب حركات المؤشر المرغوب. وفقًا لاتفاقية استدعاء النظام V x86_64 ، يتم تمرير الوسيطة الأولى في سجل rdi. لذلك ، قررنا تتبع جميع السجلات التي تحتوي على rdi. في المثال الخاص بي ، هذه هي سجلات rbx و rdi. أيضًا ، يمكن حفظ نسخة من السجل على المكدس ، والتي توجد في إصدار تصحيح الأخطاء للنواة.

مع العلم أن سجلات rbx و rdi تخزن dvp ، اكتشفنا هذا السطر 23 vnode المفضول للحصول عليه v_op. لذلك نحن نفترض أن الإزاحة في الهيكل هي 0xd0. لتأكيد القرار الصحيح ، نواصل المسح والتأكد من استدعاء الوظيفة بشكل صحيح (الخطان 24 و 26).

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

استبدل المؤشرات في الجدول


بعد إيجاد v_op ، يطرح السؤال ، كيف نستخدم هذا المؤشر؟ هناك طريقتان مختلفتان - الكتابة فوق الوظيفة في الجدول (السهم الثالث في الصورة) أو الكتابة فوق الجدول في vnode (السهم الثاني في الصورة).

في البداية ، يبدو أن الخيار الأول أكثر ربحية ، لأننا نحتاج فقط إلى استبدال مؤشر واحد. ومع ذلك ، فإن هذا النهج له 2 عيوب كبيرة. أولاً ، يكون الجدول v_op هو نفسه بالنسبة لكل vnode لنظام ملفات معين (v_op لـ HFS + ، v_op لـ APFS ، ...) ، لذلك يلزم التصفية بواسطة vnode ، والتي قد تكون مكلفة للغاية - سيكون عليك تصفية vnode الزائد على كل عملية كتابة. ثانياً ، الجدول مكتوب على صفحة للقراءة فقط. يمكن التحايل على هذا القيد إذا كنت تستخدم التسجيل عبر IOMappedWrite64 ، متجاوزًا اختبارات النظام. أيضًا ، إذا تم شحن kext مع برنامج تشغيل نظام الملفات ، فسيكون من الصعب معرفة كيفية إزالة التصحيح.

الخيار الثاني أكثر استهدافًا وأمانًا - سيتم استدعاء أداة الاعتراض فقط من أجل vnode الضروري ، وتتيح ذاكرة vnode في البداية عمليات القراءة والكتابة. نظرًا لأنه يتم استبدال الجدول بأكمله ، فمن الضروري تخصيص ذاكرة أكثر قليلاً (80 وظيفة بدلاً من واحدة). ونظرًا لأن عدد الجداول يساوي عادةً عدد أنظمة الملفات ، فإن حد الذاكرة لا يكاد يذكر.

لهذا السبب يستخدم kext الطريقة الثانية ، على الرغم من أنني أكرر للوهلة الأولى أن هذا الخيار أسوأ.

صورة

نتيجة لذلك ، يعمل سائقنا على النحو التالي:

  1. يوفر KAUTH API vnode
  2. نحن استبدال الجدول vnode. إذا لزم الأمر ، نعترض العمليات فقط من أجل vnode "المثير للاهتمام" ، على سبيل المثال ، مستندات المستخدم
  3. عند الاعتراض ، نتحقق من العملية التي يتم تسجيلها ، ونقوم بتصفية "لدينا"
  4. نرسل طلب UserSpace متزامن إلى العميل ، الذي يقرر بالضبط ما يجب حفظه.

ماذا حدث


لدينا اليوم وحدة تجريبية ، وهي امتداد لنواة ماك أو إس وتراعي أي تغييرات في نظام الملفات على المستوى الحبيبي. تجدر الإشارة إلى أنه في نظام التشغيل MacOS 10.15 ، قدمت Apple إطارًا جديدًا ( رابط إلى EndpointSecurity ) لتلقي إخطارات بالتغييرات التي تطرأ على نظام الملفات ، والتي تم التخطيط لاستخدامها في Active Protection ، وبالتالي تم إعلان إهمال الحل الموضح في المقالة.

Source: https://habr.com/ru/post/ar484816/


All Articles