اعتراض وظائف في نواة لينكس باستخدام ftrace

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

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

(صورة البطريق أعلاه: © En3l with DeviantArt .)

المناهج المعروفة


واجهة برمجة تطبيقات أمان Linux


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

لسوء الحظ ، لدى Linux Security API بعض القيود الهامة:

  • لا يمكن تحميل وحدات الحماية ديناميكيًا ، فهي جزء من النواة وتتطلب إعادة البناء
  • يمكن أن يكون هناك وحدة أمان واحدة فقط في النظام (مع بعض الاستثناءات)

إذا كان وضع مطوري النواة غامضًا فيما يتعلق بتعدد الوحدات ، فإن حظر التحميل الديناميكي أمر أساسي: يجب أن تكون وحدة الأمان جزءًا من النواة لضمان الأمان باستمرار ، من لحظة التحميل.

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

لهذه الأسباب ، لم تناسبنا Security API ، وإلا سيكون خيارًا مثاليًا.

تعديل جدول استدعاء النظام


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

هذا النهج له مزايا معينة:

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

ومع ذلك ، يعاني أيضًا من بعض العيوب:

  • التعقيد الفني للتنفيذ. في حد ذاته ، ليس من الصعب استبدال المؤشرات في الجدول. لكن المهام ذات الصلة تتطلب حلولا غير واضحة ومؤهلات معينة:
    • جدول استدعاء نظام البحث
    • تعديل حماية الجدول تجاوز
    • استبدال ذري وآمن

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

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

باستخدام kprobes


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

فوائد استخدام kprobes لاعتراض:

  • واجهة برمجة تطبيقات ناضجة. Kprobes موجودة وتحسنت منذ زمن سحيق (2002). لديهم واجهة موثقة جيدًا ، وقد تم بالفعل العثور على معظم المزالق ، وتم تحسين عملهم قدر الإمكان ، وما إلى ذلك. بشكل عام ، جبل كامل من المزايا على الدراجات التجريبية ذاتية الصنع.
  • اعتراض أي مكان في القلب. يتم تنفيذ Kprobes باستخدام نقاط التوقف (تعليمات int3) المضمنة في التعليمات البرمجية القابلة للتنفيذ لـ kernel. هذا يسمح لك بتثبيت kprobes حرفيا في أي مكان في أي وظيفة ، إذا كانت معروفة. وبالمثل ، يتم تنفيذ kretprobes من خلال انتحال عنوان الإرجاع على المكدس وتسمح لك باعتراض الإرجاع من أي وظيفة (باستثناء تلك التي لا تعود من حيث المبدأ للتحكم).

مساوئ kprobes:

  • الصعوبة الفنية. Kprobes هي مجرد طريقة لتعيين نقطة توقف في أي مكان في النواة. للحصول على حجج دالة أو قيم المتغيرات المحلية ، تحتاج إلى معرفة أي السجلات أو مكانها في المكدس ، واستخراجها بشكل مستقل من هناك. لحظر استدعاء دالة ، يجب عليك تعديل حالة العملية يدويًا بحيث يعتقد المعالج أنه قد أعاد بالفعل التحكم من الوظيفة.
  • تم إهمال Jprobes. Jprobes هي إضافة لـ kprobes تتيح لك اعتراض مكالمات الوظائف بسهولة. ستقوم باستخراج الحجج الخاصة بالوظيفة بشكل مستقل من السجلات أو المكدس واستدعاء المعالج الخاص بك ، والذي يجب أن يكون له نفس توقيع الدالة الموصلة. الصيد هو أن jprobes موقوف ومقطوع من النوى الحديثة.
  • النفقات العامة غير العادية. نقاط التوقف باهظة الثمن ، لكنها لمرة واحدة. لا تؤثر نقاط التوقف على الوظائف الأخرى ، ولكن معالجتها مكلفة نسبيًا. لحسن الحظ ، يتم تنفيذ التحسين السريع للهندسة المعمارية x86_64 ، مما يقلل بشكل كبير من تكلفة kprobes ، ولكنه لا يزال أكثر من ، على سبيل المثال ، عند تعديل جدول استدعاء النظام.
  • حدود kretprobes. يتم تنفيذ Kretprobes عن طريق انتحال عنوان المرسل على المكدس. وبناءً على ذلك ، يحتاجون إلى تخزين العنوان الأصلي في مكان ما من أجل العودة إليه بعد معالجة kretprobe. يتم تخزين العناوين في مخزن مؤقت ذي حجم ثابت. في حالة الفائض ، عندما يتم تنفيذ عدد كبير جدًا من المكالمات المتزامنة للوظيفة التي تم اعتراضها في النظام ، ستتخطى kretprobes العمليات.
  • بثق معطل. نظرًا لأن kprobes يعتمد على تسجيلات المقاطعات والمحاولات ، للمزامنة ، يتم تنفيذ جميع المعالجات مع تعطيل الاستباقية. يفرض هذا قيودًا معينة على المعالجات: لا يمكنك الانتظار فيها - خصص الكثير من الذاكرة ، وقم بإدخال / إخراج ، والنوم في المؤقتات والأشارات ، وغيرها من الأشياء المعروفة.

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

الربط


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

هذه هي الطريقة التي يتم بها تنفيذ التحسين السريع لـ kprobes. باستخدام الربط ، يمكنك تحقيق نفس النتائج ، ولكن دون تكاليف إضافية ل kprobes والتحكم الكامل في الموقف.

فوائد الربط واضحة:

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

ومع ذلك ، فإن العيب الرئيسي لهذا النهج يحجب الصورة بشكل خطير:

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

    نعم ، يمكنك التجسس على kprobes واستخدام إطار Livepatch الداخلي ، لكن الحل النهائي لا يزال معقدًا للغاية. من المخيف تخيل عدد مشاكل النوم في كل تنفيذ جديد.

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

نهج جديد مع ftrace


Ftrace هو إطار تتبع kernel على مستوى الوظيفة. تم تطويره منذ عام 2008 ولديه واجهة رائعة لبرامج المستخدم. يسمح لك Ftrace بتتبع تكرار ومدة مكالمات الوظائف ، وعرض الرسوم البيانية للمكالمات ، وتصفية الوظائف التي تهم القالب ، وما إلى ذلك. يمكنك البدء في القراءة حول ميزات ftrace من هنا ، ثم اتبع الروابط والوثائق الرسمية.

يطبق ftrace على أساس مفاتيح المترجم -pg و -mfentry ، التي تُدرج الاستدعاء إلى وظيفة التتبع الخاصة mcount () أو __fentry __ () في بداية كل وظيفة. عادة ، في برامج المستخدم ، يتم استخدام ميزة المترجم هذه من قبل المحللون لتتبع المكالمات إلى جميع الوظائف. تستخدم النواة هذه الوظائف لتنفيذ إطار عمل ftrace.

إن استدعاء ftrace من كل وظيفة ، بالطبع ، ليس رخيصًا ، لذا فإن التحسين متاح للهياكل المعمارية الشائعة: ftrace الديناميكي . خلاصة القول هي أن النواة تعرف موقع جميع المكالمات إلى mcount () أو __fentry __ () وفي المراحل الأولى من التحميل تستبدل رمز الجهاز الخاص بها بـ nop - وهي تعليمات خاصة لا تفعل شيئًا. عند تضمين التتبع في الوظائف المطلوبة ، تتم إضافة مكالمات ftrace مرة أخرى. وبالتالي ، إذا لم يتم استخدام ftrace ، فإن تأثيره على النظام يكون ضئيلاً.

وصف الوظائف المطلوبة


يمكن وصف كل وظيفة تم اعتراضها بالبنية التالية:

 /** * struct ftrace_hook -    * * @name:    * * @function:  -,     *   * * @original:   ,     *  ,    * * @address:   ,    * * @ops:   ftrace,  , *      */ struct ftrace_hook { const char *name; void *function; void *original; unsigned long address; struct ftrace_ops ops; }; 

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

 #define HOOK(_name, _function, _original) \ { \ .name = (_name), \ .function = (_function), \ .original = (_original), \ } static struct ftrace_hook hooked_functions[] = { HOOK("sys_clone", fh_sys_clone, &real_sys_clone), HOOK("sys_execve", fh_sys_execve, &real_sys_execve), }; 

الأغلفة على الوظائف التي تم اعتراضها هي كما يلي:

 /* *        execve(). *     .      *  :       , *    ABI (  "asmlinkage"). */ static asmlinkage long (*real_sys_execve)(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp); /* *      .   —  *   .      *  .      ,  *    . */ static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { long ret; pr_debug("execve() called: filename=%p argv=%p envp=%p\n", filename, argv, envp); ret = real_sys_execve(filename, argv, envp); pr_debug("execve() returns: %ld\n", ret); return ret; } 

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

التهيئة Ftrace


أولاً ، نحتاج إلى العثور على عنوان الوظيفة التي سنعترضها وحفظها. يتيح لك Ftrace تتبع الوظائف بالاسم ، ولكننا ما زلنا بحاجة إلى معرفة عنوان الوظيفة الأصلية من أجل الاتصال بها.

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

 static int resolve_hook_address(struct ftrace_hook *hook) { hook->address = kallsyms_lookup_name(hook->name); if (!hook->address) { pr_debug("unresolved symbol: %s\n", hook->name); return -ENOENT; } *((unsigned long*) hook->original) = hook->address; return 0; } 

بعد ذلك ، تحتاج إلى تهيئة بنية ftrace_ops . إنه ملزم
المجال مجرد ظائف ، مما يشير إلى رد الاتصال ، ولكننا بحاجة أيضا
ضع بعض الأعلام المهمة:

 int fh_install_hook(struct ftrace_hook *hook) { int err; err = resolve_hook_address(hook); if (err) return err; hook->ops.func = fh_ftrace_thunk; hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_IPMODIFY; /* ... */ } 

fh_ftrace_thunk () هو رد الاتصال الذي ستستدعيه ftrace عند تتبع دالة. عنه لاحقا. ستكون العلامات التي نضعها ضرورية لإكمال الاعتراض. يوجهون ftrace لحفظ واستعادة سجلات المعالج ، والتي يمكن تغيير محتوياتها في رد الاتصال.

الآن نحن على استعداد لتمكين اعتراض. للقيام بذلك ، يجب عليك أولاً تمكين ftrace للوظيفة التي تهمنا باستخدام ftrace_set_filter_ip () ، ثم السماح لـ ftrace بالاتصال باستدعاءنا باستخدام register_ftrace_function ():

 int fh_install_hook(struct ftrace_hook *hook) { /* ... */ err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); if (err) { pr_debug("ftrace_set_filter_ip() failed: %d\n", err); return err; } err = register_ftrace_function(&hook->ops); if (err) { pr_debug("register_ftrace_function() failed: %d\n", err); /*    ftrace   . */ ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); return err; } return 0; } 

يتم إيقاف الاعتراض بالمثل ، فقط في الترتيب العكسي:

 void fh_remove_hook(struct ftrace_hook *hook) { int err; err = unregister_ftrace_function(&hook->ops); if (err) { pr_debug("unregister_ftrace_function() failed: %d\n", err); } err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); if (err) { pr_debug("ftrace_set_filter_ip() failed: %d\n", err); } } 

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

تنفيذ ربط وظيفة


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

الاستدعاء الخاص بـ ftrace على النحو التالي:

 static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) { struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); regs->ip = (unsigned long) hook->function; } 

باستخدام الماكرو container_of () ، نحصل على عنوان struct ftrace_hook على عنوان struct ftrace_hook المضمنة فيه ، ثم نستبدل قيمة التسجيل٪ rip في struct pt_regs . هذا كل شيء. بالنسبة للهياكل الأخرى غير x86_64 ، قد يُسمى هذا السجل بشكل مختلف (مثل IP أو الكمبيوتر الشخصي) ، لكن الفكرة تنطبق من حيث المبدأ عليها.

لاحظ مؤهل notrace المضافة لرد الاتصال. يمكنهم الإبلاغ عن الميزات التي لا يُسمح بتتبعها باستخدام ftrace. على سبيل المثال ، هذه هي الطريقة التي يتم بها تمييز وظائف ftrace نفسها التي تنطوي عليها عملية التتبع. هذا يساعد على منع النظام من التجمد في حلقة لا نهائية عند تتبع جميع الوظائف في النواة (يمكن أن تفعل ftrace ذلك).

عادة ما يتم استدعاء رد فعل ftback مع تعطيل البثق (مثل kprobes). قد تكون هناك استثناءات ، ولكن لا يجب الاعتماد عليها. ومع ذلك ، في حالتنا ، هذا التقييد ليس مهمًا ، لذلك نستبدل ثمانية بايتات فقط في البنية.

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

حماية المكالمة العودية


: , ftrace, , . - .

, — parent_ip — ftrace-, , . . , .

, parent_ip , — - . , .

, ( ). , . .

, ftrace- :

 static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) { struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); /*      . */ if (!within_module(parent_ip, THIS_MODULE)) regs->ip = (unsigned long) hook->function; } 

/ :

  • . . , , .
  • . . , .
  • . kretprobes , ( ). , .


: ls , . (, Bash) fork () + execve () . clone() execve() . , execve(), .

- :

sequence-

, ( ) ( ), ftrace ( ) ( ).

  1. SYSCALL.entry_SYSCALL_64 (). 64- 64- .
  2. . , , do_syscall_64 (), . sys_call_tablesys_execve ().
  3. ftrace. __fentry__ (), ftrace. , , nop , sys_execve() .
  4. Ftrace . ftrace , . , %rip, .
  5. . parent_ip , do_syscall_64() — sys_execve() — , %rip pt_regs .
  6. Ftrace . FTRACE_SAVE_REGS, ftrace pt_regs . ftrace . %rip — — .
  7. -. - sys_execve() . fh_sys_execve (). , do_syscall_64().
  8. . . fh_sys_execve() ( ) . . — sys_execve() , real_sys_execve , .
  9. . sys_execve(), ftrace . , -…
  10. . sys_execve() fh_sys_execve(), do_syscall_64(). sys_execve() . : ftrace sys_execve() .
  11. . sys_execve() fh_sys_execve(). . , execve() , , , . .
  12. . fh_sys_execve() do_syscall_64(), , . .
  13. . IRET ( SYSRET, execve() — IRET), . ( ) .


, :

  • API . . , , . — -, .
  • . . - , , , - . ( ), .
  • . , ftrace, . kprobes ftrace.

?

  • . ftrace :
    • kallsyms
    • ftrace
    • ftrace,

    . , , , , . , - , .
  • ftrace , kprobes ( ftrace ), , , . , ftrace — , «» ftrace .
  • . , . , , ftrace . , , .
  • ftrace. parent_ip ftrace . , . , : ftrace , 5 ( call), ftrace .

.


, ftrace kallsyms. :

  • CONFIG_FTRACE
  • CONFIG_KALLSYMS

, ftrace .

  • CONFIG_DYNAMIC_FTRACE_WITH_REGS

, 3.19 , FTRACE_OPS_FL_IPMODIFY. %rip, 3.19 . , — .

, ftrace : , ( ).

  • CONFIG_HAVE_FENTRY

x86_64 , i386 — . - i386 ftrace , ftrace . %eip — , , .

ftrace 32- x86. , ( «»), , ftrace.


: . , , . , .

, . - ftrace- parent_ip . - , ftrace , - .

, , , . , -.

:

 static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { long ret; pr_debug("execve() called: filename=%p argv=%p envp=%p\n", filename, argv, envp); ret = real_sys_execve(filename, argv, envp); pr_debug("execve() returns: %ld\n", ret); return ret; } 

— :

 static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { long ret; pr_devel("execve() called: filename=%p argv=%p envp=%p\n", filename, argv, envp); ret = real_sys_execve(filename, argv, envp); pr_devel("execve() returns: %ld\n", ret); return ret; } 

, ? , . - , .

, , , pr_devel() . printk- . , , DEBUG. :

 static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { return real_sys_execve(filename, argv, envp); } 

. (tail call optimization). , . :

 0000000000000000 <fh_sys_execve>: 0: e8 00 00 00 00 callq 5 <fh_sys_execve+0x5> 5: ff 15 00 00 00 00 callq *0x0(%rip) b: f3 c3 repz retq 

— :

 0000000000000000 <fh_sys_execve>: 0: e8 00 00 00 00 callq 5 <fh_sys_execve+0x5> 5: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax c: ff e0 jmpq *%rax 

CALL — __fentry__(), . real_sys_execve ( ) CALL fh_sys_execve() RET. real_sys_execve() JMP.

«» , , CALL. , — parent_ip . fh_sys_execve() , — . parent_ip , .

, . . .

-:

 #pragma GCC optimize("-fno-optimize-sibling-calls") 

الخلاصة


… Linux — . , - , .

, Github .

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


All Articles