کتاب "BPF برای نظارت بر لینوکس"

کتاب "BPF برای نظارت بر لینوکس"سلام بر اهالی خبر! ماشین مجازی BPF یکی از مهمترین اجزای هسته لینوکس است. استفاده صحیح از آن به مهندسان سیستم این امکان را می دهد که عیوب را پیدا کنند و حتی پیچیده ترین مشکلات را حل کنند. شما یاد خواهید گرفت که چگونه برنامه هایی بنویسید که رفتار هسته را نظارت و تغییر می دهند، چگونه کدهایی را برای نظارت بر رویدادها در هسته پیاده سازی کنید و خیلی چیزهای دیگر. دیوید کالاورا و لورنزو فونتانا به شما کمک می کنند تا قدرت BPF را باز کنید. دانش خود را در مورد بهینه سازی عملکرد، شبکه، امنیت گسترش دهید. - از BPF برای نظارت و اصلاح رفتار هسته لینوکس استفاده کنید. - کد را برای نظارت ایمن وقایع هسته بدون نیاز به کامپایل مجدد هسته یا راه اندازی مجدد سیستم تزریق کنید. - از نمونه کدهای مناسب در C، Go یا Python استفاده کنید. - با داشتن چرخه عمر برنامه BPF کنترل را در دست بگیرید.

امنیت هسته لینوکس، ویژگی های آن و Seccomp

BPF یک راه قدرتمند برای گسترش هسته بدون به خطر انداختن ثبات، امنیت یا سرعت ارائه می دهد. به همین دلیل، توسعه دهندگان هسته فکر کردند که ایده خوبی است که از تطبیق پذیری آن برای بهبود جداسازی فرآیند در Seccomp با اجرای فیلترهای Seccomp پشتیبانی شده توسط برنامه های BPF، که به عنوان Seccomp BPF نیز شناخته می شود، استفاده کنند. در این فصل توضیح خواهیم داد که Seccomp چیست و چگونه از آن استفاده می شود. سپس یاد خواهید گرفت که چگونه فیلترهای Seccomp را با استفاده از برنامه های BPF بنویسید. پس از آن، به قلاب‌های داخلی BPF که در هسته ماژول‌های امنیتی لینوکس موجود است نگاه می‌کنیم.

ماژول‌های امنیتی لینوکس (LSM) چارچوبی هستند که مجموعه‌ای از توابع را ارائه می‌دهند که می‌توان از آنها برای پیاده‌سازی مدل‌های امنیتی مختلف به صورت استاندارد استفاده کرد. LSM را می توان مستقیماً در درخت منبع هسته مانند Apparmor، SELinux و Tomoyo استفاده کرد.

بیایید با بحث در مورد قابلیت های لینوکس شروع کنیم.

توانمندی ها

ماهیت قابلیت‌های لینوکس این است که شما باید به یک فرآیند غیرمجاز برای انجام یک کار خاص مجوز بدهید، اما بدون استفاده از 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

با این حال، از آنجایی که ما امتیازات root را اعطا نمی کنیم، این کد هنگام اتصال پورت با خطا مواجه می شود:

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 و قابلیت تغییر واقعی شناسه کاربر را مشخص می‌کنیم. root to nobody، یعنی cap_setuid و cap_setgid.
  • —keep=1 — می‌خواهیم قابلیت‌های نصب‌شده را هنگام جابجایی از اکانت root حفظ کنیم.
  • —user="nobody" - کاربر نهایی که برنامه را اجرا می کند هیچ کس خواهد بود.
  • —addamb=cap_net_bind_service — پاکسازی قابلیت‌های مرتبط را پس از تغییر حالت روت تنظیم کنید.
  • - -c "./capabilities" - فقط برنامه را اجرا کنید.

قابلیت‌های پیوندی نوع خاصی از قابلیت‌ها هستند که وقتی برنامه‌های فعلی با استفاده از () execve آن‌ها را اجرا می‌کنند، توسط برنامه‌های فرزند به ارث می‌رسند. تنها قابلیت هایی که مجاز به همراهی یا به عبارت دیگر به عنوان قابلیت های محیطی هستند، قابل ارث هستند.

احتمالاً پس از مشخص کردن قابلیت در گزینه --caps، تعجب می کنید که +eip به چه معناست. از این پرچم ها برای تعیین این که قابلیت های زیر استفاده می شود:

-باید فعال شود (p);

-در دسترس برای استفاده (e)؛

-می تواند توسط فرآیندهای فرزند (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 یک پوسته بنویسید. برای اطلاعات بیشتر، man 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 تک خطی در تابع هسته 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

Seccomp مخفف Secure Computing است و یک لایه امنیتی است که در هسته لینوکس پیاده سازی شده است که به توسعه دهندگان اجازه می دهد تا تماس های سیستمی خاصی را فیلتر کنند. اگرچه Seccomp از نظر قابلیت‌ها با لینوکس قابل مقایسه است، اما توانایی آن در مدیریت تماس‌های سیستمی خاص، آن را در مقایسه با آنها انعطاف‌پذیرتر می‌کند.

ویژگی‌های Seccomp و Linux متقابلاً منحصر به فرد نیستند و اغلب برای بهره‌مندی از هر دو رویکرد با هم استفاده می‌شوند. به عنوان مثال، ممکن است بخواهید به یک فرآیند قابلیت CAP_NET_ADMIN را بدهید اما به آن اجازه ندهید اتصالات سوکت را بپذیرد و تماس‌های سیستمی Accept و Accept4 را مسدود کند.

روش فیلتر Seccomp مبتنی بر فیلترهای BPF است که در حالت SECCOMP_MODE_FILTER کار می کنند و فیلتر تماس سیستم به همان روشی که برای بسته ها انجام می شود انجام می شود.

فیلترهای Seccomp با استفاده از prctl از طریق عملیات PR_SET_SECCOMP بارگیری می شوند. این فیلترها به شکل یک برنامه BPF هستند که برای هر بسته Seccomp که توسط ساختار seccomp_data نمایش داده می شود، اجرا می شود. این ساختار شامل معماری مرجع، یک اشاره گر به دستورالعمل های پردازنده در زمان فراخوانی سیستم، و حداکثر شش آرگومان فراخوانی سیستم است که به صورت uint64 بیان می شود.

ساختار seccomp_data از کد منبع هسته در فایل 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 به فضای کاربر ارسال می شود. بسته به علت خطا، مقادیر مختلف errno برگردانده می شود. لیستی از اعداد خطا در بخش بعدی ارائه شده است.

- SECCOMP_RET_TRACE - برای اطلاع دادن به ردیاب ptrace با استفاده از - PTRACE_O_TRACESECCOMP برای رهگیری زمانی که یک فراخوانی سیستمی برای مشاهده و کنترل آن فرآیند اجرا می شود، استفاده می شود. اگر یک ردیاب متصل نباشد، یک خطا برگردانده می شود، errno روی -ENOSYS تنظیم می شود و فراخوانی سیستم اجرا نمی شود.

- SECCOMP_RET_LOG - تماس سیستم برطرف و ثبت شد.

- SECCOMP_RET_ALLOW - تماس سیستمی به سادگی مجاز است.

ptrace یک فراخوانی سیستمی برای پیاده سازی مکانیسم های ردیابی در فرآیندی به نام tracee با قابلیت نظارت و کنترل بر اجرای فرآیند است. برنامه ردیابی می تواند به طور موثر بر اجرا تأثیر بگذارد و رجیسترهای حافظه ردیابی را تغییر دهد. در زمینه Seccomp، زمانی که توسط کد وضعیت SECCOMP_RET_TRACE فعال می شود، از ptrace استفاده می شود، بنابراین ردیاب می تواند از اجرای فراخوانی سیستم جلوگیری کرده و منطق خود را پیاده سازی کند.

خطاهای Seccomp

هر از چند گاهی در حین کار با Seccomp با خطاهای مختلفی مواجه می شوید که با مقدار برگشتی از نوع SECCOMP_RET_ERRNO مشخص می شوند. برای گزارش یک خطا، فراخوانی سیستم seccomp به جای 1 -0 را برمی‌گرداند.

خطاهای زیر ممکن است:

- EACCESS - تماس گیرنده مجاز به برقراری تماس سیستمی نیست. این معمولاً به این دلیل اتفاق می افتد که امتیازات CAP_SYS_ADMIN را ندارد یا no_new_privs با استفاده از prctl تنظیم نشده است (بعداً در مورد این موضوع صحبت خواهیم کرد).

— EFAULT — آرگومان های تصویب شده (args در ساختار seccomp_data) آدرس معتبری ندارند.

- EINVAL - چهار دلیل در اینجا می تواند وجود داشته باشد:

-عملیات درخواستی ناشناخته است یا توسط هسته در پیکربندی فعلی پشتیبانی نمی شود.

-پرچم های مشخص شده برای عملیات درخواستی معتبر نیستند.

-عملیات شامل BPF_ABS است، اما مشکلاتی با افست مشخص شده وجود دارد که ممکن است از اندازه ساختار seccomp_data بیشتر باشد.

-تعداد دستورالعمل های ارسال شده به فیلتر از حداکثر بیشتر است.

- ENOMEM - حافظه کافی برای اجرای برنامه وجود ندارد.

- EOPNOTSUPP - عملیات نشان داد که با SECCOMP_GET_ACTION_AVAIL این عمل در دسترس است، اما هسته از بازگشت در آرگومان ها پشتیبانی نمی کند.

— ESRCH — هنگام همگام سازی جریان دیگری مشکلی رخ داد.

- ENOSYS - هیچ ردیاب متصل به عملکرد SECCOMP_RET_TRACE وجود ندارد.

prctl یک فراخوانی سیستمی است که به یک برنامه فضای کاربر اجازه می دهد تا جنبه های خاصی از یک فرآیند مانند endianness بایت، نام رشته ها، حالت محاسبات ایمن (Seccomp)، امتیازات، رویدادهای Perf و غیره را دستکاری (تنظیم و دریافت) کند.

Seccomp ممکن است به نظر شما یک فناوری sandbox به نظر برسد، اما اینطور نیست. Seccomp ابزاری است که به کاربران اجازه می دهد مکانیزم جعبه شنی را توسعه دهند. حال بیایید ببینیم که چگونه برنامه های تعامل کاربر با استفاده از فیلتری که مستقیماً توسط فراخوانی سیستم Seccomp فراخوانی می شود ایجاد می شوند.

مثال فیلتر BPF Seccomp

در اینجا نحوه ترکیب دو عملی که قبلاً مورد بحث قرار گرفت را نشان خواهیم داد، یعنی:

- ما یک برنامه Seccomp BPF می نویسیم که بسته به تصمیمات گرفته شده به عنوان فیلتر با کدهای بازگشتی مختلف استفاده می شود.

- فیلتر را با استفاده از prctl بارگیری کنید.

ابتدا به هدرهایی از کتابخانه استاندارد و هسته لینوکس نیاز دارید:

#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 | (error & 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;

پس از تعریف کد فیلتر در ساختار socket_filter، باید یک 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 بارگیری کند، که در متغیر prog از نوع 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 کاندید خوبی به نظر می رسد. او معمولاً اینگونه رفتار می کند:

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

فوق العاده! در اینجا نحوه استفاده از برنامه wrapper ما به نظر می رسد: ما به سادگی برنامه ای را که می خواهیم آزمایش کنیم به عنوان اولین آرگومان ارسال می کنیم:

./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 برای کار با آنها استفاده کنید، به شما داده باشیم.

درباره نویسندگان

دیوید کالاورا مدیر ارشد فناوری در Netlify است. او در پشتیبانی Docker کار می کرد و در توسعه ابزارهای Runc، Go و BCC و همچنین سایر پروژه های منبع باز مشارکت داشت. او به خاطر کارش بر روی پروژه های داکر و توسعه اکوسیستم پلاگین داکر شهرت دارد. دیوید علاقه زیادی به نمودارهای شعله دارد و همیشه به دنبال بهینه سازی عملکرد است.

لورنزو فونتانا در تیم منبع باز Sysdig کار می کند، جایی که او در درجه اول بر روی Falco متمرکز است، پروژه بنیاد محاسبات بومی Cloud که امنیت زمان اجرای کانتینر و تشخیص ناهنجاری را از طریق یک ماژول هسته و eBPF فراهم می کند. او علاقه زیادی به سیستم های توزیع شده، شبکه های تعریف شده نرم افزاری، هسته لینوکس و تجزیه و تحلیل عملکرد دارد.

» جزئیات بیشتر در مورد کتاب را می توانید در اینجا بیابید وب سایت ناشر
» فهرست مندرجات
» گزیده ای

برای Khabrozhiteley 25% تخفیف با استفاده از کوپن - لینـوکــس

پس از پرداخت نسخه کاغذی کتاب، کتاب الکترونیکی از طریق ایمیل ارسال می شود.

منبع: www.habr.com

اضافه کردن نظر