Кніга "BPF для маніторынгу Linux"

Кніга "BPF для маніторынгу Linux"Прывітанне, Хаброжыцелі! Віртуальная машына BPF - адзін з найважнейшых кампанентаў ядра Linux. Яе пісьменнае ўжыванне дазволіць сістэмным інжынерам знаходзіць збоі і вырашаць нават самыя складаныя праблемы. Вы навучыцеся ствараць праграмы, якія адсочваюць і якія мадыфікуюць паводзіны ядра, зможаце бяспечна ўкараняць код для назірання падзей у ядры і шматлікае іншае. Дэвід Калавера і Ларэнца Фантана дапамогуць вам раскрыць магчымасці BPF. Пашырце свае веды аб аптымізацыі прадукцыйнасці, сетках, бяспецы. - Выкарыстоўвайце BPF для адсочвання і мадыфікацыі паводзін ядра Linux. - Укараняйце код для бяспечнага маніторынгу падзей у ядры - без неабходнасці перакампіляваць ядро ​​або перазагружаць сістэму. - Карыстайцеся зручнымі прыкладамі кода на C, Go або Python. - Кіруйце сітуацыяй, валодаючы жыццёвым цыклам праграмы BPF.

Бяспека ядра Linux, яго магчымасці і Seccomp

BPF дае магутны спосаб пашырэння ядра без шкоды для стабільнасці, бяспекі і хуткасці. Па гэтай прычыне распрацоўшчыкі ядра падумалі, што было б нядрэнна выкарыстоўваць яго ўніверсальнасць для паляпшэння ізаляцыі працэсаў у Seccomp шляхам рэалізацыі фільтраў Seccomp, якія падтрымліваюцца праграмамі BPF, вядомымі таксама як Seccomp BPF. У гэтым раздзеле мы распавядзем, што такое Seccomp і як ён ужываецца. Затым вы даведаецеся, як пісаць фільтры Seccomp з дапамогай праграм BPF. Пасля гэтага разгледзім убудаваныя пасткі BPF, якія ёсць у ядры для модуляў бяспекі Linux.

Модулі бяспекі Linux (LSM) - гэта платформа, якая прадстаўляе набор функцый, якія можна ўжываць для стандартызаванай рэалізацыі розных мадэляў бяспекі. LSM можа выкарыстоўвацца непасрэдна ў дрэве зыходнага кода ядра, напрыклад Apparmor, SELinux і Tomoyo.

Пачнём з абмеркавання магчымасцяў Linux.

Магчымасці

Сутнасць магчымасцяў Linux складаецца ў тым, што вам трэба падаць непрывілеяванаму працэсу дазвол на выкананне вызначанай задачы, але без выкарыстання suid для гэтай мэты, ці іншым чынам зрабіць працэс прывілеяваным, памяншаючы магчымасць нападаў і забяспечваючы працэсу магчымасць выканання вызначаных задач. Напрыклад, калі вашаму з дадаткам трэба адкрыць прывілеяваны порт, дапусцім, 80, замест запуску працэсу ад імя root можаце проста даць яму магчымасць 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 (сродак кіравання абалонкай) - гэта інструмент, які запускае абалонку з пэўным наборам магчымасцяў.

У гэтым выпадку, як ужо гаварылася, замест падавання поўных мае рацыю root можна дазволіць прывязку прывілеяваных портаў, падаўшы магчымасць 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' - паколькі нам трэба змяніць карыстальніка (мы не жадаем запускацца з правамі root), пакажам cap_net_bind_service і магчымасць фактычна змяніць ідэнтыфікатар карыстача з root на nobody, а менавіта cap
  • —keep=1 — жадаем захаваць усталяваныя магчымасці, калі выканана пераключэнне з акаўнта root.
  • -user="nobody" - канчатковым карыстальнікам, які запускае праграму, будзе nobody.
  • -addamb=cap_net_bind_service - задаем ачыстку звязаных магчымасцяў пасля пераключэння з рэжыму root.
  • - -c "./capabilities" - проста запускаем праграму.

Змяненні, магчымасці - гэта асаблівы від магчымасцяў, якія ўспадкоўваюцца даччынымі праграмамі, калі бягучая праграма выконвае іх з дапамогай execve(). Успадкоўвацца могуць толькі магчымасці, дазволеныя як злучаныя, ці, іншымі словамі, як магчымасці асяроддзя.

Верагодна, вам цікава, што значыць +eip пасля ўказання магчымасці ў опцыі -caps. Гэтыя сцягі выкарыстоўваюцца для вызначэння таго, што магчымасць:

-павінна быць актываваная (p);

-даступная для прымянення (е);

-можа быць успадкавана даччынымі працэсамі (i).

Паколькі мы жадаем выкарыстаць cap_net_bind_service, трэба зрабіць гэта са сцягам e. Затым запусцім абалонку ў камандзе. У выніку запусціцца двайковы файл capabilities, і нам трэба пазначыць яго сцягам i. Нарэшце, мы хочам, каб магчымасць была актываваная (мы гэта зрабілі, не мяняючы UID) з дапамогай p. Гэта выглядае як cap_net_bind_service+eip.

Можаце праверыць вынік з дапамогай ss. Трохі скароцім выснову, каб ён змясціўся на старонцы, але ён пакажа злучаны порт і ідэнтыфікатар карыстача, выдатныя ад 0, у дадзеным выпадку 65 534:

# 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 capable, які ўсталёўвае 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, гэта ўзровень бяспекі, рэалізаваны ў ядры Linux, які дазваляе распрацоўнікам фільтраваць вызначаныя сістэмныя выклікі. Хоць Seccomp супастаўны з магчымасцямі Linux, яго здольнасць кіраваць вызначанымі сістэмнымі выклікамі робіць яго нашмат больш гнуткім у параўнанні з імі.

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 (Bad System Call) адпраўляецца задзірлівай яго задачы.

- 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, з магчымасцю назірання і кантролю за выкананнем працэсу. Праграма трасіроўкі можа эфектыўна ўплываць на выкананне і змяняць рэгістры памяці tracee. У кантэксце Seccomp ptrace выкарыстоўваецца, калі запускаецца кодам стану SECCOMP_RET_TRACE, такім чынам, трасіроўшчык можа прадухіліць выкананне сістэмнага выкліку і рэалізаваць уласную логіку.

Памылкі 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 - гэта сістэмны выклік, які дазваляе праграме карыстацкай прасторы кіраваць (усталёўваць і атрымліваць) пэўнымі аспектамі працэсу, такімі як парадкавы нумар байтаў, імёны струменяў, рэжым абароненых вылічэнняў (Seccomp), прывілеі, падзеі Perf і т. д.

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 роўным arch. Калі гэта так, пераходзіць са зрушэннем 0 да наступнай інструкцыі, у адваротным выпадку, каб выдаць памылку, пераскочыць са зрушэннем 3 (у дадзеным выпадку), таму што arch не супадае.

- 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, не маючы мае рацыю root.

Цяпер мы можам выклікаць функцыю install_filter. Заблакуем усе сістэмныя выклікі write, якія адносяцца да архітэктуры 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

Выдатна! Вось як выглядае выкарыстанне нашай праграмы-абалонкі: мы проста перадаем праграму, якую жадаем пратэставаць, у якасці першага аргументу:

./filter-write "ls -la"

Пасля выканання гэтая праграма выдае зусім пустую выснову. Тым не менш мы можам прымяніць strace, каб убачыць, што адбываецца:

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

Вынік працы моцна скарочаны, але адпаведная яго частка паказвае, што запісы блакуюцца з памылкай EPERM – той самай, якую мы наладзілі. Гэта азначае, што праграма нічога не выводзіць, таму што не можа атрымаць доступ да сістэмнага выкліку write:

[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, а таксама іншых праектаў з адчыненым зыходным кодам. Вядомы сваёй працай над праектамі Docker і развіццём экасістэмы плагінаў Docker. Дэвід вельмі любіць флейм-графы і заўсёды імкнецца да аптымізацыі прадукцыйнасці.

Ларэнца Фантана працуе ў камандзе распрацоўнікаў ПА з адчыненым зыходным кодам у Sysdig, дзе ў асноўным займаецца Falco - праектам Cloud Native Computing Foundation, які забяспечвае бяспеку асяроддзя выканання кантэйнераў і выяўленне анамалій праз модуль ядра і eBPF. Ён захоплены размеркаванымі сістэмамі, праграмна вызначанымі сеткамі, ядром Linux і аналізам прадукцыйнасці.

» Больш падрабязна з кнігай можна азнаёміцца ​​на сайце выдавецтва
» Змест
» урывак

Для Хаброжыцеляў зніжка 25% па купоне Linux

Па факце аплаты папяровай версіі кнігі на e-mail дасылаецца электронная кніга.

Крыніца: habr.com

Дадаць каментар