Topleaked: أداة لالتقاط تسرب الذاكرة

صورة

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


فكرة


الخدمة الأصلية كانت مكتوبة في C ++ و Perl ، على الرغم من أن هذا لا يلعب دورًا خاصًا. كل ما هو موضح أدناه ينطبق على أي لغة تقريبًا.


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


بالإضافة إلى vtbl ، هناك تسلسلات أخرى تحدث بشكل متكرر: الثوابت التي تقوم بتهيئة الحقول ، ورؤوس HTTP في أجزاء الخطوط ، والمؤشرات إلى الوظائف.
إذا كنت محظوظًا بما فيه الكفاية للعثور على مؤشر ، فيمكننا استخدام gdb لفهم ما يشير إليه (ما لم يكن هناك بالطبع أحرف تصحيح). في حالة البيانات ، يمكنك محاولة النظر إليها وفهم مكان استخدامها. بالنظر إلى المستقبل ، لاحظت أن ذلك يحدث في كل من ذلك والآخر ، ومن خلال جزء من الخط ، من الممكن تمامًا فهم ماهية هذا الجزء من البروتوكول ، وحيث يكون من الضروري إجراء مزيد من الحفر.


تم تجسيد الفكرة وتم نسخ التطبيق الأول من تطبيق stackoverflow. https://stackoverflow.com/questions/7439170/is-there-a-way-to-find-leaked-memory-using-a-core-file


hexdump core.10639 | awk '{printf "%s%s%s%s\n%s%s%s%s\n", $5,$4,$3,$2,$9,$8,$7,$6}' | sort | uniq -c | sort -nr | head 

عمل البرنامج النصي لمدة 15 دقيقة على تفريغ لدينا ، عاد مجموعة من الخطوط ، و ... لا شيء. ليس مؤشر واحد ، لا شيء مفيد.


فرزها


التطوير المدفوع بـ Stackoverflow له عيوبه. لا يمكنك فقط نسخ البرنامج النصي ونأمل أن يعمل كل شيء. في هذا السيناريو بالذات ، هناك نوع من إعادة ترتيب البايتات يلفت الأنظار على الفور. السؤال الذي يطرح نفسه هو أيضًا ، لماذا التباديل في 4. لا تحتاج إلى أن تكون متخصصًا كبيرًا لفهم أن هذه التباديل تعتمد على النظام الأساسي: طلب الشهادة والبايت.


لفهم كيف تبدو بالضبط ، تحتاج إلى فهم تنسيق ملف تفريغ الذاكرة ، LITTLE- و BIG endian ، أو يمكنك ببساطة إعادة ترتيب البايتات في القطع الموجودة بطرق مختلفة ومنح gdb. يا معجزة! بالترتيب المباشر ، ترى بايت gdb الشخصية وتقول إنها مؤشر إلى وظيفة!


في حالتنا ، كان مؤشرًا إلى إحدى وظائف القراءة والكتابة في المخازن المؤقتة openssl. لتخصيص المدخلات والمخرجات ، يتم استخدام نهج نظام OOP - هيكل به مجموعة من المؤشرات إلى الوظائف ، وهو نوع من الواجهة أو بالأحرى vtbl. هذه الهياكل مع مؤشرات تبين أن بجنون كثيرة. سمحت لنا نظرة فاحصة على الكود المسؤول عن إعداد هذه الهياكل وإنشاء مخازن مؤقتة بالعثور بسرعة على الخطأ. كما اتضح ، عند تقاطع C ++ و C ، لم تكن هناك كائنات RAII وفي حالة حدوث خطأ ، لم تترك العودة المبكرة فرصة لتحرير الموارد. لا أحد خمّن تحميل الخدمة بمصادقة ssl غير صحيحة في الوقت المناسب ، لذلك فاتهم. كما أن كيفية الاتصال بـ 6 غيغابايت من مصافحات SSL غير الصحيحة أمر مثير للاهتمام ، ولكن كما يقولون ، هذه قصة مختلفة تمامًا. تم حل المشكلة.


topleaked


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


تركيب


لا توجد تجميعات ثنائية ، لذلك بطريقة أو بأخرى ستحتاج إلى تجميع المشروع من المصدر. للقيام بذلك ، تحتاج إلى برنامج التحويل البرمجي D. هناك ثلاثة خيارات: dmd هو المترجم المرجعي ، يعتمد ldc على llvm و gdc ، المتضمن في gcc ، بدءًا من الإصدار 9. لذلك قد لا تضطر إلى تثبيت أي شيء إذا كان لديك أحدث إصدار. إذا قمت بالتثبيت ، فإنني أوصي بـ ldc ، لأنه يتحسن بشكل أفضل. يمكن العثور على الثلاثة على الموقع الرسمي .
يتم توفير إدارة حزمة dub مع برنامج التحويل البرمجي. استخدامه ، يتم تثبيت topleaked مع أمر واحد:


 dub fetch topleaked 

في المستقبل ، سوف نستخدم الأمر للبدء:


 dub run topleaked -brelease-nobounds -- <filename> [<options>...] 

من أجل عدم تكرار تشغيل dub وسيطة المترجم brelease-nobounds ، يمكنك تنزيل المصادر من github وجمع الملف القابل للتنفيذ:


 dub build -brelease-nobounds 

في جذر مجلد المشروع سوف تظهر على السطح.


استخدام


لنأخذ برنامج C ++ بسيط مع تسرب للذاكرة.


 #include <iostream> #include <assert.h> #include <unistd.h> class A { size_t val = 12345678910; virtual ~A(){} }; int main() { for (size_t i =0; i < 1000000; i++) { new A(); } std::cout << getpid() << std::endl; sleep(200); } 

نكملها من خلال القتل -6 ، من نحصل على تفريغ الذاكرة. الآن يمكنك الركض إلى أعلى وإلقاء نظرة على النتائج

 ./toleaked -n10 leak.core 

الخيار -n هو حجم الجزء العلوي الذي نحتاجه. عادةً ما تكون القيم بين 10 و 200 منطقية ، وهذا يتوقف على مقدار "القمامة" الموجودة. تنسيق الإخراج الافتراضي هو أعلى سطرا في شكل مقروء الإنسان.


 0x0000000000000000 : 1050347 0x0000000000000021 : 1000003 0x00000002dfdc1c3e : 1000000 0x0000558087922d90 : 1000000 0x0000000000000002 : 198 0x0000000000000001 : 180 0x00007f4247c6a000 : 164 0x0000000000000008 : 160 0x00007f4247c5c438 : 153 0xffffffffffffffff : 141 

إنه ذو فائدة قليلة ، إلا أنه يمكننا رؤية الرقم 0x2dfdc1c3e ، وهو أيضًا 12345678910 ، والذي يحدث مليون مرة. بالفعل هذا يمكن أن يكون كافيا ، لكنني أريد أكثر من ذلك. من أجل رؤية أسماء الفئات للكائنات المتسربة ، يمكنك إرسال النتيجة إلى gdb عن طريق إعادة توجيه دفق الإخراج القياسي إلى إدخال gdb مع ملف تفريغ مفتوح. -ogdb - خيار تغيير التنسيق إلى gdb مفهومة.


 $ ./topleaked -n10 -ogdb /home/core/leak.1002.core | gdb leak /home/core/leak.1002.core ...<   gdb  > #0 0x00007f424784e6f4 in __GI___nanosleep (requested_time=requested_time@entry=0x7ffcfffedb50, remaining=remaining@entry=0x7ffcfffedb50) at ../sysdeps/unix/sysv/linux/nanosleep.c:28 28 ../sysdeps/unix/sysv/linux/nanosleep.c: No such file or directory. (gdb) $1 = 1050347 (gdb) 0x0: Cannot access memory at address 0x0 (gdb) No symbol matches 0x0000000000000000. (gdb) $2 = 1000003 (gdb) 0x21: Cannot access memory at address 0x21 (gdb) No symbol matches 0x0000000000000021. (gdb) $3 = 1000000 (gdb) 0x2dfdc1c3e: Cannot access memory at address 0x2dfdc1c3e (gdb) No symbol matches 0x00000002dfdc1c3e. (gdb) $4 = 1000000 (gdb) 0x558087922d90 <_ZTV1A+16>: 0x87721bfa (gdb) vtable for A + 16 in section .data.rel.ro of /home/g.smorkalov/dlang/topleaked/leak (gdb) $5 = 198 (gdb) 0x2: Cannot access memory at address 0x2 (gdb) No symbol matches 0x0000000000000002. (gdb) $6 = 180 (gdb) 0x1: Cannot access memory at address 0x1 (gdb) No symbol matches 0x0000000000000001. (gdb) $7 = 164 (gdb) 0x7f4247c6a000: 0x47ae6000 (gdb) No symbol matches 0x00007f4247c6a000. (gdb) $8 = 160 (gdb) 0x8: Cannot access memory at address 0x8 (gdb) No symbol matches 0x0000000000000008. (gdb) $9 = 153 (gdb) 0x7f4247c5c438 <_ZTVN10__cxxabiv120__si_class_type_infoE+16>: 0x47b79660 (gdb) vtable for __cxxabiv1::__si_class_type_info + 16 in section .data.rel.ro of /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (gdb) $10 = 141 (gdb) 0xffffffffffffffff: Cannot access memory at address 0xffffffffffffffff (gdb) No symbol matches 0xffffffffffffffff. (gdb) quit 

القراءة ليست بسيطة جدا ، ولكن ممكن. خطوط النموذج 4 $ = 1،000،000 تعكس الموضع في الأعلى وعدد الأحداث التي تم العثور عليها. فيما يلي نتائج تشغيل x ورمز المعلومات للقيمة. هنا يمكننا أن نرى أن vtable لـ A تحدث مليون مرة ، وهو ما يتوافق مع مليون كائن تم تسريبه من الفئة A.

لتحليل جزء من الملف (إذا كان حجمه أكبر من اللازم) ، تتم إضافة خيارات الإزاحة والحد - بدءًا من مكان وعدد البايتات المراد قراءتها.


يؤدي


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


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


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


لغة د


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


 auto all = input.sort; ValCount[] res = new ValCount[min(all.length, maxSize)]; return all.group.map!((p) => ValCount(p[0],p[1])) .topNCopy!"a.count>b.count"(res, Yes.sortOutput); 

كل ذلك بفضل النطاقات البطيئة ووجود خوارزميات جاهزة عليها في المكتبة القياسية ، مثل المجموعة و topN.


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


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


في المجموع ، تكون تكاليف العمالة قابلة للمقارنة مع لغات البرمجة النصية ، وأفضل بكثير في الأداء. من المحتمل ، يمكنك تحقيق ذلك إلى أقصى حد ممكن ، لأن نفس الكود في C و D سيعملان بنفس السرعة.


مؤشرات وموانع للاستخدام


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

المشكلات المعروفة


أثناء الاختبار ، تم استخدام ملفات التفريغ التي تم تلقيها عن طريق إرسال إشارة إلى العملية. مع هذه الملفات ، كل شيء يعمل بشكل جيد. عند إزالة ملف تفريغ ، يكتب الأمر gcore بعض رؤوس ELF الأخرى ويحدث إزاحة بواسطة عدد غير محدد من وحدات البايت. أي أن قيم المؤشرات غير محاذاة إلى 8 في الملف ، لذلك يتم الحصول على نتائج لا معنى لها. بالنسبة للحل ، تم تقديم خيار الإزاحة - لقراءة الملف ليس أولاً ، ولكن تم إزاحته بواسطة بايت الإزاحة (عادةً 4).
لحل هذا ، أخطط لإضافة قراءة نتيجة objdump-s من stdin. حسنًا ، إما أن تقوم بتوصيل libelf وتحليله بنفسك ، ولكنه سيقتل "النظام الأساسي" ، ويكون stdout أكثر مرونة وأقرب إلى طريقة يونكس.


مراجع


مشروع جيثب
المجمعين د
السؤال الأصلي على stackoverflow

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


All Articles