كتاب "BPF لمراقبة Linux"

كتاب "BPF لمراقبة Linux"مرحبًا سكان الخبر! يعد الجهاز الظاهري BPF أحد أهم مكونات Linux kernel. سيسمح استخدامه الصحيح لمهندسي النظام بالعثور على الأخطاء وحل المشكلات الأكثر تعقيدًا. ستتعلم كيفية كتابة البرامج التي تراقب سلوك النواة وتعديلها، وكيفية تنفيذ التعليمات البرمجية بأمان لمراقبة الأحداث في النواة، وغير ذلك الكثير. سيساعدك David Calavera وLorenzo Fontana على إطلاق العنان لقوة BPF. قم بتوسيع معرفتك بتحسين الأداء والشبكات والأمان. - استخدم BPF لمراقبة وتعديل سلوك Linux kernel. - أدخل التعليمات البرمجية لمراقبة أحداث النواة بشكل آمن دون الحاجة إلى إعادة ترجمة النواة أو إعادة تشغيل النظام. - استخدم أمثلة التعليمات البرمجية المناسبة في C أو Go أو Python. - السيطرة من خلال امتلاك دورة حياة برنامج BPF.

Linux Kernel Security وميزاته وSeccomp

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

وحدات أمان Linux (LSM) هي إطار عمل يوفر مجموعة من الوظائف التي يمكن استخدامها لتنفيذ نماذج أمان مختلفة بطريقة موحدة. يمكن استخدام LSM مباشرة في شجرة مصدر النواة، مثل Apparmor وSELinux وTomoyo.

لنبدأ بمناقشة إمكانيات Linux.

قدرات

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

فكر في برنامج Go المسمى main.go:

package main
import (
            "net/http"
            "log"
)
func main() {
     log.Fatalf("%v", http.ListenAndServe(":80", nil))
}

يخدم هذا البرنامج خادم HTTP على المنفذ 80 (هذا منفذ مميز). عادةً ما نقوم بتشغيله مباشرة بعد التجميع:

$ go build -o capabilities main.go
$ ./capabilities

ومع ذلك، نظرًا لأننا لا نمنح امتيازات الجذر، فسيؤدي هذا الرمز إلى ظهور خطأ عند ربط المنفذ:

2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1

capsh (مدير الصدفة) هو أداة تقوم بتشغيل الصدفة بمجموعة محددة من الإمكانات.

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

# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' 
   --keep=1 --user="nobody" 
   --addamb=cap_net_bind_service -- -c "./capabilities"

دعونا نفهم هذا الفريق قليلا.

  • capsh - استخدم capsh كصدفة.
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - نظرًا لأننا بحاجة إلى تغيير المستخدم (لا نريد التشغيل كجذر)، فسنحدد cap_net_bind_service والقدرة على تغيير معرف المستخدم فعليًا من الجذر إلى لا أحد، وهي cap_setuid وcap_setgid.
  • —keep=1 — نريد الاحتفاظ بالإمكانيات المثبتة عند التبديل من الحساب الجذر.
  • —user=“nobody” — المستخدم النهائي الذي يقوم بتشغيل البرنامج لن يكون أحدًا.
  • —addamb=cap_net_bind_service — اضبط مسح الإمكانيات ذات الصلة بعد التبديل من وضع الجذر.
  • - -c "./capabilities" - فقط قم بتشغيل البرنامج.

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

ربما تتساءل عما يعنيه +eip بعد تحديد الإمكانية في خيار --caps. تُستخدم هذه العلامات لتحديد القدرة:

-يجب تفعيلها (ع)؛

- متاح للاستخدام (هـ)؛

- يمكن أن تكون موروثة عن طريق العمليات الفرعية (i).

وبما أننا نريد استخدام cap_net_bind_service، فعلينا القيام بذلك باستخدام العلامة e. ثم سنبدأ الصدفة في الأمر. سيؤدي هذا إلى تشغيل القدرات الثنائية ونحتاج إلى وضع علامة عليها بالعلامة i. وأخيرًا، نريد تفعيل الميزة (لقد فعلنا ذلك دون تغيير UID) باستخدام p. يبدو مثل cap_net_bind_service+eip.

يمكنك التحقق من النتيجة باستخدام ss. لنختصر المخرجات قليلًا لتناسب الصفحة، ولكنها ستظهر المنفذ المرتبط ومعرف المستخدم بخلاف 0، في هذه الحالة 65:

# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0

في هذا المثال، استخدمنا لغة capsh، ولكن يمكنك كتابة الصدفة باستخدام libcap. لمزيد من المعلومات، راجع رجل 3 libcap.

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

لفهم قدرات برنامجنا بشكل أفضل، يمكننا استخدام أداة BCC القادرة، والتي تحدد kprobe لوظيفة cap_capable للنواة:

/usr/share/bcc/tools/capable
TIME      UID  PID   TID   COMM               CAP    NAME           AUDIT
10:12:53 0 424     424     systemd-udevd 12 CAP_NET_ADMIN         1
10:12:57 0 1103   1101   timesync        25 CAP_SYS_TIME         1
10:12:57 0 19545 19545 capabilities       10 CAP_NET_BIND_SERVICE 1

يمكننا تحقيق نفس الشيء باستخدام bpftrace مع kprobe أحادي الخط في دالة kernel cap_capable :

bpftrace -e 
   'kprobe:cap_capable {
      time("%H:%M:%S ");
      printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
    }' 
    | grep -i capabilities

سيؤدي هذا إلى إخراج شيء مثل ما يلي إذا تم تمكين إمكانيات برنامجنا بعد kprobe:

12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1

العمود الخامس هو القدرات التي تحتاجها العملية، وبما أن هذا الناتج يتضمن أحداث غير التدقيق، فإننا نرى جميع عمليات التحقق غير التدقيق وأخيرًا القدرة المطلوبة مع تعيين علامة التدقيق (الأخيرة في الإخراج) على 1. القدرة. إحداها التي نهتم بها هي CAP_NET_BIND_SERVICE، ويتم تعريفها كثابت في كود مصدر النواة في الملف include/uapi/linux/ability.h بالمعرف 10:

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">

غالبًا ما يتم تمكين الإمكانات في وقت التشغيل للحاويات مثل runC أو Docker للسماح لها بالعمل في وضع غير مميز، ولكن يُسمح لها فقط بالإمكانيات اللازمة لتشغيل معظم التطبيقات. عندما يتطلب تطبيق ما إمكانات معينة، يمكن لـ Docker توفيرها باستخدام --cap-add:

docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy

سيمنح هذا الأمر الحاوية إمكانية CAP_NET_ADMIN، مما يسمح لها بتكوين ارتباط شبكة لإضافة الواجهة dummy0.

يوضح القسم التالي كيفية استخدام ميزات مثل التصفية، ولكن باستخدام تقنية مختلفة تتيح لنا تنفيذ عوامل التصفية الخاصة بنا برمجيًا.

سيكومب

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

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

تعتمد طريقة التصفية Seccomp على عوامل تصفية BPF التي تعمل في وضع SECCOMP_MODE_FILTER، ويتم إجراء تصفية مكالمات النظام بنفس الطريقة المتبعة مع الحزم.

يتم تحميل مرشحات Seccomp باستخدام prctl من خلال عملية PR_SET_SECCOMP. تأخذ عوامل التصفية هذه شكل برنامج BPF الذي يتم تنفيذه لكل حزمة Seccomp ممثلة في بنية seccomp_data. تحتوي هذه البنية على البنية المرجعية، ومؤشر لتعليمات المعالج في وقت استدعاء النظام، وستة وسائط لاستدعاء النظام كحد أقصى، يتم التعبير عنها بـ uint64.

هذا ما تبدو عليه بنية seccomp_data من كود مصدر kernel في ملف linux/seccomp.h:

struct seccomp_data {
int nr;
      __u32 arch;
      __u64 instruction_pointer;
      __u64 args[6];
};

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

بعد استلام كل حزمة Seccomp، يجب على المرشح إجراء المعالجة لاتخاذ القرار النهائي وإخبار النواة بما يجب فعله بعد ذلك. يتم التعبير عن القرار النهائي بإحدى قيم الإرجاع (رموز الحالة).

- SECCOMP_RET_KILL_PROCESS - يقتل العملية بأكملها مباشرة بعد تصفية استدعاء النظام الذي لم يتم تنفيذه لهذا السبب.

- SECCOMP_RET_KILL_THREAD - ينهي مؤشر الترابط الحالي فورًا بعد تصفية استدعاء النظام الذي لم يتم تنفيذه لهذا السبب.

— SECCOMP_RET_KILL — الاسم المستعار لـ SECCOMP_RET_KILL_THREAD، المتبقي للتوافق مع الإصدارات السابقة.

- SECCOMP_RET_TRAP - يُحظر استدعاء النظام، ويتم إرسال إشارة SIGSYS (استدعاء النظام السيئ) إلى المهمة التي تستدعيها.

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

- SECCOMP_RET_TRACE - يستخدم لإعلام متتبع ptrace باستخدام - PTRACE_O_TRACESECCOMP للاعتراض عند تنفيذ استدعاء النظام لرؤية هذه العملية والتحكم فيها. إذا لم يكن المتتبع متصلاً، فسيتم إرجاع خطأ، ويتم تعيين errno على -ENOSYS، ولا يتم تنفيذ استدعاء النظام.

- SECCOMP_RET_LOG - تم حل مكالمة النظام وتسجيلها.

- SECCOMP_RET_ALLOW - يُسمح باستدعاء النظام بكل بساطة.

ptrace هو استدعاء نظام لتنفيذ آليات التتبع في عملية تسمى Tracee، مع القدرة على مراقبة تنفيذ العملية والتحكم فيه. يمكن لبرنامج التتبع التأثير بشكل فعال على تنفيذ وتعديل سجلات ذاكرة التتبع. في سياق Seccomp، يتم استخدام ptrace عند تشغيله بواسطة رمز الحالة SECCOMP_RET_TRACE، بحيث يمكن للمتتبع منع استدعاء النظام من التنفيذ وتنفيذ المنطق الخاص به.

أخطاء سيكومب

من وقت لآخر، أثناء العمل مع Seccomp، ستواجه أخطاء متنوعة، والتي يتم تحديدها بواسطة قيمة إرجاع من النوع SECCOMP_RET_ERRNO. للإبلاغ عن خطأ، سيرجع استدعاء النظام seccomp -1 بدلاً من 0.

الأخطاء التالية ممكنة:

- الوصول - لا يسمح للمتصل بإجراء مكالمة النظام. يحدث هذا عادةً لأنه لا يتمتع بامتيازات CAP_SYS_ADMIN أو أنه لم يتم تعيين no_new_privs باستخدام prctl (سنتحدث عن هذا لاحقًا)؛

— EFAULT — لا تحتوي الوسائط التي تم تمريرها (الوسائط الموجودة في بنية seccomp_data) على عنوان صالح؛

- أينفال - يمكن أن يكون هناك أربعة أسباب هنا:

- العملية المطلوبة غير معروفة أو غير مدعومة من قبل النواة في التكوين الحالي؛

- الأعلام المحددة غير صالحة للعملية المطلوبة؛

- تتضمن العملية BPF_ABS، ولكن هناك مشاكل في الإزاحة المحددة، والتي قد تتجاوز حجم بنية seccomp_data؛

-عدد التعليمات التي تم تمريرها إلى المرشح يتجاوز الحد الأقصى؛

— ENOMEM — الذاكرة غير كافية لتنفيذ البرنامج؛

- EOPNOTSUPP - أشارت العملية إلى أنه باستخدام SECCOMP_GET_ACTION_AVAIL كان الإجراء متاحًا، لكن النواة لا تدعم الإرجاع في الوسائط؛

— ESRCH — حدثت مشكلة عند مزامنة دفق آخر؛

- ENOSYS - لا يوجد أي تتبع مرتبط بإجراء SECCOMP_RET_TRACE.

prctl هو استدعاء نظام يسمح لبرنامج مساحة المستخدم بمعالجة (تعيين والحصول) على جوانب معينة من العملية، مثل البايت endianness، وأسماء الخيوط، ووضع الحساب الآمن (Seccomp)، والامتيازات، وأحداث الأداء، وما إلى ذلك.

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

مثال على عامل تصفية BPF Seccomp

وسنبين هنا كيفية الجمع بين الإجراءين اللذين ناقشناهما سابقاً، وهما:

— سنكتب برنامج Seccomp BPF، والذي سيتم استخدامه كمرشح برموز إرجاع مختلفة اعتمادًا على القرارات المتخذة؛

- قم بتحميل الفلتر باستخدام prctl.

تحتاج أولاً إلى رؤوس من المكتبة القياسية ونواة Linux:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

قبل تجربة هذا المثال، يجب أن نتأكد من ترجمة النواة مع ضبط CONFIG_SECCOMP وCONFIG_SECCOMP_FILTER على y. على جهاز يعمل، يمكنك التحقق من هذا مثل هذا:

cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP

بقية الكود عبارة عن وظيفة install_filter مكونة من جزأين. يحتوي الجزء الأول على قائمتنا لتعليمات تصفية BPF:

static int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
  };

يتم تعيين التعليمات باستخدام وحدات الماكرو BPF_STMT وBPF_JUMP المحددة في ملف linux/filter.h.
دعونا نذهب من خلال التعليمات.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, Arch))) - يتم تحميل النظام وتجميعه من BPF_LD في شكل كلمة BPF_W، وتقع بيانات الحزمة في إزاحة ثابتة BPF_ABS.

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, Arch, 0, 3) - يتحقق باستخدام BPF_JEQ مما إذا كانت قيمة البنية في ثابت التراكم BPF_K تساوي القوس. إذا كان الأمر كذلك، فإنه يقفز عند الإزاحة 0 إلى التعليمات التالية، وإلا فإنه يقفز عند الإزاحة 3 (في هذه الحالة) لإلقاء خطأ لأن القوس غير متطابق.

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - يتم التحميل والتراكم من BPF_LD في شكل كلمة BPF_W، وهو رقم استدعاء النظام الموجود في الإزاحة الثابتة لـ BPF_ABS.

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — يقارن رقم استدعاء النظام بقيمة المتغير nr. إذا كانا متساويين، ينتقل إلى التعليمات التالية ويعطل استدعاء النظام، وإلا يسمح باستدعاء النظام باستخدام SECCOMP_RET_ALLOW.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (خطأ & SECCOMP_RET_DATA)) - ينهي البرنامج باستخدام BPF_RET ونتيجة لذلك ينتج خطأ SECCOMP_RET_ERRNO بالرقم من المتغير err.

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - ينهي البرنامج باستخدام BPF_RET ويسمح بتنفيذ استدعاء النظام باستخدام SECCOMP_RET_ALLOW.

SECCOMP هو CBPF
ربما تتساءل عن سبب استخدام قائمة التعليمات بدلاً من كائن ELF المترجم أو برنامج C المترجم من JIT.

هناك سببان لهذا.

• أولا، يستخدم Seccomp cBPF (BPF الكلاسيكي) وليس eBPF، مما يعني: أنه لا يحتوي على سجلات، ولكن فقط مجمع لتخزين نتيجة الحساب الأخيرة، كما يمكن رؤيته في المثال.

• ثانيًا، يقبل Seccomp مؤشرًا لمجموعة من تعليمات BPF مباشرةً ولا شيء غير ذلك. تساعد وحدات الماكرو التي استخدمناها ببساطة في تحديد هذه التعليمات بطريقة سهلة الاستخدام للمبرمجين.

إذا كنت بحاجة إلى مزيد من المساعدة في فهم هذا التجميع، ففكر في الكود الكاذب الذي يفعل نفس الشيء:

if (arch != AUDIT_ARCH_X86_64) {
    return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
    return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;

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

struct sock_fprog prog = {
   .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
   .filter = filter,
};

لم يتبق سوى شيء واحد للقيام به في وظيفة install_filter - تحميل البرنامج نفسه! للقيام بذلك، نستخدم prctl، مع أخذ PR_SET_SECCOMP كخيار للدخول في وضع الحوسبة الآمنة. ثم نخبر الوضع بتحميل الفلتر باستخدام SECCOMP_MODE_FILTER، الموجود في متغير البرنامج من النوع sock_fprog:

  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

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

الآن يمكننا استدعاء وظيفة install_filter. دعونا نحظر جميع مكالمات نظام الكتابة المتعلقة ببنية X86-64 ونمنح ببساطة إذنًا يمنع جميع المحاولات. بعد تثبيت الفلتر، نواصل التنفيذ باستخدام الوسيط الأول:

int main(int argc, char const *argv[]) {
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
   perror("prctl(NO_NEW_PRIVS)");
   return 1;
  }
   install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
  return system(argv[1]);
 }

هيا بنا نبدأ. لتجميع برنامجنا يمكننا استخدام clang أو gcc، وفي كلتا الحالتين يتم تجميع الملف main.c فقط بدون خيارات خاصة:

clang main.c -o filter-write

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

ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c

رائع! إليك ما يبدو عليه استخدام برنامج التغليف الخاص بنا: نحن ببساطة نمرر البرنامج الذي نريد اختباره باعتباره الوسيط الأول:

./filter-write "ls -la"

عند تنفيذ هذا البرنامج، ينتج مخرجات فارغة تمامًا. ومع ذلك، يمكننا استخدام strace لمعرفة ما يحدث:

strace -f ./filter-write "ls -la"

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

[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation not permitted)

أنت الآن تفهم كيفية عمل Seccomp BPF ولديك فكرة جيدة عما يمكنك فعله به. ولكن ألا ترغب في تحقيق نفس الشيء باستخدام eBPF بدلاً من cBPF لتسخير قوتها الكاملة؟

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

مصائد BPF LSM

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

في وقت كتابة هذا التقرير، كانت النواة تحتوي على سبعة خطافات مرتبطة ببرامج BPF، وSELinux هو LSM المدمج الوحيد الذي ينفذها.

الكود المصدري للاعتراضات موجود في شجرة النواة في الملف include/linux/security.h:

extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);

سيتم استدعاء كل منهم في مراحل مختلفة من التنفيذ:

—security_bpf — يجري فحصًا أوليًا لاستدعاءات نظام BPF المنفذة؛

-security_bpf_map - يتحقق عندما تقوم النواة بإرجاع واصف ملف للخريطة؛

-security_bpf_prog - يتحقق عندما تقوم النواة بإرجاع واصف الملف لبرنامج eBPF؛

—security_bpf_map_alloc — يتحقق مما إذا كان قد تم تهيئة حقل الأمان داخل خرائط BPF؛

-security_bpf_map_free - يتحقق مما إذا كان حقل الأمان قد تم مسحه داخل خرائط BPF؛

—security_bpf_prog_alloc — يتحقق مما إذا كان قد تم تهيئة حقل الأمان داخل برامج BPF؛

-security_bpf_prog_free - يتحقق مما إذا كان حقل الأمان قد تم مسحه داخل برامج BPF.

الآن، بعد رؤية كل هذا، نفهم: الفكرة وراء أجهزة اعتراض LSM BPF هي أنها يمكن أن توفر الحماية لكل كائن eBPF، مما يضمن أن أولئك الذين لديهم الامتيازات المناسبة فقط هم من يمكنهم تنفيذ العمليات على البطاقات والبرامج.

ملخص

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

عن المؤلفين

ديفيد كالافيرا هو CTO في Netlify. عمل في دعم Docker وساهم في تطوير أدوات Runc وGo وBCC، بالإضافة إلى مشاريع أخرى مفتوحة المصدر. معروف بعمله في مشاريع Docker وتطوير النظام البيئي لمكونات Docker الإضافية. ديفيد شغوف جدًا بالرسوم البيانية اللهبية ويتطلع دائمًا إلى تحسين الأداء.

لورنزو فونتانا يعمل ضمن فريق مفتوح المصدر في Sysdig، حيث يركز بشكل أساسي على Falco، وهو مشروع Cloud Native Computing Foundation الذي يوفر أمان وقت تشغيل الحاوية والكشف عن الحالات الشاذة من خلال وحدة kernel وeBPF. إنه شغوف بالأنظمة الموزعة والشبكات المعرفة بالبرمجيات ونواة Linux وتحليل الأداء.

»لمزيد من المعلومات حول الكتاب ، يرجى زيارة موقع الناشر
» جدول المحتويات
» مقتطفات

ل Khabrozhiteli خصم 25٪ على الكوبون - لينكس

عند الدفع مقابل النسخة الورقية من الكتاب ، يتم إرسال كتاب إلكتروني إلى البريد الإلكتروني.

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

إضافة تعليق