Ողջույն Խաբրո բնակիչներ։ 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-ի հնարավորությունների քննարկումից:
· ¶ R'RѕR RјRѕR RЅRѕSЃS, Fe
Linux-ի հնարավորությունների իմաստը կայանում է նրանում, որ դուք պետք է թույլտվություն տրամադրեք ոչ արտոնյալ գործընթացին որոշակի առաջադրանք կատարելու համար, բայց առանց այդ նպատակով suid-ի օգտագործման, կամ այլ կերպ արտոնագրեք գործընթացը՝ նվազեցնելով հարձակման մակերեսը և թույլ տալով գործընթացին կատարել որոշակի առաջադրանքներ: Օրինակ, եթե ձեր հավելվածին անհրաժեշտ է բացել արտոնյալ նավահանգիստ, ասենք 80, գործընթացը որպես root գործարկելու փոխարեն, կարող եք պարզապես տրամադրել CAP_NET_BIND_SERVICE հնարավորությունը:
Դիտարկենք main.go անունով 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 1capsh-ը (կեղևի կառավարման գործիք) գործիք է, որը սկսում է կեղևը որոշակի հնարավորություններով:
Այս դեպքում, ինչպես արդեն նշվեց, լրիվ արմատային իրավունքներ տրամադրելու փոխարեն, դուք կարող եք թույլատրել արտոնյալ նավահանգիստների կապում՝ տրամադրելով 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 որպես shell.
- —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' — քանի որ մենք պետք է փոխենք օգտատիրոջը (մենք չենք ուզում գործարկել որպես root), մենք նշում ենք cap_net_bind_service-ը և օգտվողի ID-ն արմատից ոչ ոքի փոխելու հնարավորությունը, այն է՝ cap_setuid և cap_setuid:
- --keep=1 - մենք ցանկանում ենք պահպանել տեղադրված գործառույթները root հաշվից անցնելիս:
- --user="nobody" — ծրագիրը վարող վերջնական օգտվողը կլինի ոչ ոք:
- --addamb=cap_net_bind_service — մենք սահմանում ենք կապակցված հնարավորությունների մաքրումը արմատային ռեժիմից անցնելուց հետո:
- — -c «./կարողություններ» — պարզապես գործարկեք ծրագիրը:
Կապակցված հնարավորությունները հատուկ տեսակի հնարավորություններ են, որոնք ժառանգվում են մանկական ծրագրերի կողմից, երբ ընթացիկ ծրագիրը դրանք կատարում է execve(-ի միջոցով): Միայն այն կարողությունները, որոնք լուծվում են որպես փոխկապակցված, կամ այլ կերպ ասած, որպես շրջակա միջավայրի հնարավորություններ, կարող են ժառանգվել:
Դուք հավանաբար մտածում եք, թե ինչ է նշանակում +eip՝ --caps տարբերակում հնարավորություն նշելուց հետո: Այս դրոշները օգտագործվում են որոշելու համար, թե արդյոք կարողությունը հետևյալն է.
- պետք է ակտիվացված լինի (p);
- հասանելի է օգտագործման համար (e);
- կարող է ժառանգվել երեխայի գործընթացներով (i):
Քանի որ մենք ցանկանում ենք օգտագործել cap_net_bind_service, մենք պետք է դա անենք e դրոշով: Այնուհետև մենք կգործարկենք հրամանի վահանակը: Սա կաշխատի երկուական հնարավորությունները, և մենք պետք է այն նշենք i դրոշով: Վերջապես, մենք ցանկանում ենք, որ ֆունկցիան ակտիվացվի (մենք դա արեցինք առանց UID-ը փոխելու) օգտագործելով p. Կարծես cap_net_bind_service+eip.
Դուք կարող եք ստուգել արդյունքը ss. Մենք մի փոքր կկրճատենք ելքը, որպեսզի տեղավորվի էջի վրա, բայց այն ցույց կտա հարակից նավահանգիստը և օգտագործողի ID-ն, բացի 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 է, այն սահմանվում է որպես հաստատուն՝ ID/piua-ում ներառելով աղբյուրը: 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 հնարավորությունը, բայց թույլ չտալ, որ այն ընդունի վարդակից միացումներ՝ արգելափակելով ընդունել և ընդունել4 համակարգի զանգերը:
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 արժեք: Կախված սխալի պատճառներից, վերադարձվում են տարբեր սխալ արժեքներ: Սխալների թվերի ցանկը ներկայացված է հաջորդ բաժնում:
- SECCOMP_RET_TRACE - օգտագործվում է ptrace հետագծողին ծանուցելու համար - PTRACE_O_TRACESECCOMP-ով, երբ համակարգային զանգ է կատարվում՝ գործընթացը տեսնելու և վերահսկելու համար: Եթե հետագծիչը կցված չէ, սխալ է վերադարձվում, errno-ն սահմանվում է -ENOSYS, և համակարգի կանչը չի կատարվում:
— SECCOMP_RET_LOG — համակարգային զանգը թույլատրված է և գրանցված:
— SECCOMP_RET_ALLOW — համակարգային զանգը պարզապես թույլատրված է:
ptrace-ը համակարգային կանչ է՝ հետագծման մեխանիզմների ներդրման համար, որը կոչվում է 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 կառուցվածքում) վավեր հասցե չունեն.
— ԷԻՆՎԱԼ — դրա համար կարող է լինել չորս պատճառ.
- պահանջվող գործողությունը անհայտ է կամ չի աջակցվում միջուկի կողմից ընթացիկ կազմաձևում;
- նշված դրոշներն անվավեր են պահանջվող գործողության համար.
- գործողությունը ներառում է BPF_ABS, բայց կան խնդիրներ նշված օֆսեթի հետ, որը կարող է գերազանցել seccomp_data կառուցվածքի չափը;
- ֆիլտրին փոխանցված հրահանգների քանակը գերազանցում է առավելագույնը.
— ENOMEM — ծրագիրը գործարկելու համար բավարար հիշողություն չկա.
— EOPNOTSUPP — գործողությունը ցույց տվեց, որ գործողությունը հասանելի է SECCOMP_GET_ACTION_AVAIL-ով, սակայն միջուկը չի աջակցում արգումենտներում վերադարձը.
— ESRCH — խնդիր է առաջացել մեկ այլ թեմա համաժամեցնելիս.
— ENOSYS — SECCOMP_RET_TRACE գործողությանը կցված հետագծող չկա:
prctl-ը համակարգային զանգ է, որը թույլ է տալիս օգտատեր-տիեզերական ծրագրին վերահսկել (սահմանել և ստանալ) գործընթացի որոշակի ասպեկտներ, ինչպիսիք են բայթերի կարգը, թելերի անունները, ապահով հաշվարկման ռեժիմը (Seccomp), արտոնությունները, Perf իրադարձությունները և այլն:
Seccomp-ը ձեզ կարող է թվալ ավազի տուփի տեխնոլոգիա, բայց դա այդպես չէ: Seccomp-ը կոմունալ ծրագիր է, որը թույլ է տալիս օգտատերերին մշակել Sandbox մեխանիզմ: Հիմա եկեք տեսնենք, թե ինչպես են ստեղծվում օգտատերերի փոխազդեցության ծրագրերը, օգտագործելով զտիչը, որը կոչվում է անմիջապես 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),
}; Հրահանգները սահմանվում են linux/filter.h-ում սահմանված BPF_STMT և BPF_JUMP մակրոներով:
Եկեք անցնենք հրահանգներին:
— 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, կամար, 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 օբյեկտի կամ JIT-ով կազմված C ծրագրի փոխարեն օգտագործվում հրահանգների ցանկ։Դրա համար երկու պատճառ կա.
• Նախ, Seccomp-ը eBPF-ի փոխարեն օգտագործում է cBPF (դասական BPF), ինչը նշանակում է, որ այն չունի գրանցամատյաններ, միայն կուտակիչ՝ հաշվարկի վերջին արդյունքը պահելու համար, ինչպես կարող եք տեսնել օրինակում:
• Երկրորդ, 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, որը պարունակվում է sock_fprog տիպի prog փոփոխականում.
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-ը սահմանելու համար և այդպիսով խուսափել այն իրավիճակից, երբ երեխայի գործընթացները ավելի բարձր արտոնություններ են ստանում, քան իրենց ծնողները: Այս դեպքում կարող ենք install_filter ֆունկցիայի մեջ կատարել հետևյալ prctl կանչերը՝ առանց արմատային իրավունքներ ունենալու։
Այժմ մենք կարող ենք զանգահարել 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-ը և լավ պատկերացում ունեք, թե ինչ կարող եք անել դրա հետ: Բայց մի՞թե լավ չի լինի նույն բանին հասնել cBPF-ի փոխարեն eBPF-ի միջոցով՝ դրա ողջ հզորությունն օգտագործելու համար:
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 interceptors-ի գաղափարն այն է, որ նրանք կարող են պաշտպանություն ապահովել յուրաքանչյուր eBPF օբյեկտի համար՝ ապահովելով, որ միայն համապատասխան արտոնություններ ունեցողները կարող են գործողություններ կատարել քարտերի և ծրագրերի վրա:
Ամփոփում
Անվտանգությունը մի բան չէ, որը դուք կարող եք կիրառել բոլորի համար միանգամյա ձևով այն ամենի համար, ինչ ցանկանում եք պաշտպանել: Կարևոր է, որ կարողանանք պաշտպանել համակարգերը տարբեր մակարդակներում և տարբեր ձևերով: Հավատացեք, թե ոչ, համակարգը ապահովելու լավագույն միջոցը տարբեր տեսանկյուններից պաշտպանության տարբեր մակարդակների կազմակերպումն է, որպեսզի մի մակարդակի անվտանգության խախտումը թույլ չտա մուտք գործել ամբողջ համակարգ: Հիմնական մշակողները մեծ աշխատանք են կատարել՝ տրամադրելով մեզ տարբեր շերտերի և հպման կետերի հավաքածու: Հուսով ենք, որ մենք ձեզ լավ պատկերացում ենք տվել, թե ինչ են շերտերը և ինչպես օգտագործել BPF ծրագրերը դրանց հետ աշխատելու համար:
Հեղինակների մասին
Դեյվիդ Կալավերա Netlify-ի CTO-ն է: Նա աշխատել է Docker-ի աջակցության ոլորտում և նպաստել Runc, Go և BCC գործիքների, ինչպես նաև բաց կոդով այլ նախագծերի մշակմանը: Հայտնի է Docker նախագծերի վրա իր աշխատանքով և Docker plugin էկոհամակարգի մշակմամբ: Դեյվիդը բոցային գրաֆիկների մեծ երկրպագու է և միշտ ձգտում է օպտիմալացնել կատարումը:
Լորենցո Ֆոնտանա աշխատում է բաց կոդով թիմում Sysdig-ում, որտեղ նա հիմնականում կենտրոնանում է Falco-ի՝ Cloud Native Computing Foundation նախագծի վրա, որն ապահովում է կոնտեյների գործարկման ժամանակի անվտանգություն և անոմալիաների հայտնաբերում միջուկի մոդուլի և eBPF-ի միջոցով: Նա կրքոտ է բաշխված համակարգերով, ծրագրային ապահովմամբ սահմանված ցանցերով, Linux միջուկով և կատարողականի վերլուծությամբ:
» Գրքի մասին ավելի մանրամասն կարող եք գտնել այստեղ
»
»
Khabrozhiteley-ի համար 25% զեղչ՝ օգտագործելով կտրոնը - Linux
Գրքի թղթային տարբերակի վճարումից հետո էլեկտրոնային գիրք կուղարկվի էլեկտրոնային փոստով:
Source: www.habr.com
