Βιβλίο "BPF για παρακολούθηση Linux"

Βιβλίο "BPF για παρακολούθηση Linux"Γεια σας, κάτοικοι του Khabro! Η εικονική μηχανή BPF είναι ένα από τα πιο σημαντικά στοιχεία του πυρήνα του Linux. Η σωστή χρήση του θα επιτρέψει στους μηχανικούς συστημάτων να βρουν σφάλματα και να λύσουν ακόμη και τα πιο περίπλοκα προβλήματα. Θα μάθετε πώς να γράφετε προγράμματα που παρακολουθούν και τροποποιούν τη συμπεριφορά του πυρήνα, πώς να εφαρμόζετε με ασφάλεια κώδικα για την παρακολούθηση συμβάντων στον πυρήνα και πολλά άλλα. Ο David Calavera και ο Lorenzo Fontana θα σας βοηθήσουν να ξεκλειδώσετε τη δύναμη του BPF. Επεκτείνετε τις γνώσεις σας σχετικά με τη βελτιστοποίηση απόδοσης, τη δικτύωση, την ασφάλεια. - Χρησιμοποιήστε το BPF για να παρακολουθείτε και να τροποποιείτε τη συμπεριφορά του πυρήνα του Linux. - Εισάγετε κώδικα για την ασφαλή παρακολούθηση των συμβάντων του πυρήνα χωρίς να χρειάζεται να μεταγλωττίσετε ξανά τον πυρήνα ή να επανεκκινήσετε το σύστημα. — Χρησιμοποιήστε βολικά παραδείγματα κώδικα σε C, Go ή Python. - Πάρτε τον έλεγχο κατέχοντας τον κύκλο ζωής του προγράμματος BPF.

Linux Kernel Security, τα χαρακτηριστικά του και Seccomp

Το BPF παρέχει έναν ισχυρό τρόπο επέκτασης του πυρήνα χωρίς να θυσιάζεται η σταθερότητα, η ασφάλεια ή η ταχύτητα. Για αυτόν τον λόγο, οι προγραμματιστές του πυρήνα θεώρησαν ότι θα ήταν καλή ιδέα να χρησιμοποιήσουν την ευελιξία του για να βελτιώσουν την απομόνωση διεργασιών στο Seccomp εφαρμόζοντας φίλτρα Seccomp που υποστηρίζονται από προγράμματα BPF, γνωστά και ως Seccomp BPF. Σε αυτό το κεφάλαιο θα εξηγήσουμε τι είναι το Seccomp και πώς χρησιμοποιείται. Στη συνέχεια, θα μάθετε πώς να γράφετε φίλτρα Seccomp χρησιμοποιώντας προγράμματα BPF. Μετά από αυτό, θα δούμε τα ενσωματωμένα άγκιστρα BPF που περιλαμβάνονται στον πυρήνα για λειτουργικές μονάδες ασφαλείας Linux.

Τα Linux Security Modules (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 to nobody, δηλαδή cap_setuid και cap_setgid.
  • —keep=1 — θέλουμε να διατηρήσουμε τις εγκατεστημένες δυνατότητες κατά την εναλλαγή από τον λογαριασμό root.
  • —user=“nobody” — ο τελικός χρήστης που τρέχει το πρόγραμμα θα είναι κανείς.
  • —addamb=cap_net_bind_service — ορίστε την εκκαθάριση των σχετικών δυνατοτήτων μετά την εναλλαγή από τη λειτουργία root.
  • - -c "./capabilities" - απλά εκτελέστε το πρόγραμμα.

Οι συνδεδεμένες δυνατότητες είναι ένα ειδικό είδος δυνατοτήτων που κληρονομούνται από θυγατρικά προγράμματα όταν το τρέχον πρόγραμμα τις εκτελεί χρησιμοποιώντας την execve(). Μόνο οι δυνατότητες που επιτρέπεται να συσχετίζονται, ή με άλλα λόγια, ως δυνατότητες περιβάλλοντος, μπορούν να κληρονομηθούν.

Πιθανότατα αναρωτιέστε τι σημαίνει +eip αφού καθορίσετε τη δυνατότητα στην επιλογή --caps. Αυτές οι σημαίες χρησιμοποιούνται για να προσδιοριστεί ότι η ικανότητα:

-πρέπει να είναι ενεργοποιημένο (p);

-διαθέσιμο για χρήση (ε);

-μπορεί να κληρονομηθεί από τις θυγατρικές διεργασίες (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 και είναι ένα επίπεδο ασφαλείας που εφαρμόζεται στον πυρήνα του 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, με δυνατότητα παρακολούθησης και ελέγχου της εκτέλεσης της διαδικασίας. Το πρόγραμμα ανίχνευσης μπορεί να επηρεάσει αποτελεσματικά την εκτέλεση και να τροποποιήσει τους καταχωρητές μνήμης του 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 είναι μια κλήση συστήματος που επιτρέπει σε ένα πρόγραμμα χώρου χρήστη να χειρίζεται (ρυθμίζει και παίρνει) συγκεκριμένες πτυχές μιας διεργασίας, όπως endianness byte, ονόματα νημάτων, ασφαλή λειτουργία υπολογισμού (Seccomp), δικαιώματα, συμβάντα Perf κ.λπ.

Το Seccomp μπορεί να σας φαίνεται σαν μια τεχνολογία sandbox, αλλά δεν είναι. Το 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),
  };

Οι οδηγίες ορίζονται χρησιμοποιώντας τις μακροεντολές 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, τόξο, 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;

Αφού ορίσετε τον κωδικό φίλτρου στη δομή 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. Ας αποκλείσουμε όλες τις κλήσεις συστήματος εγγραφής που σχετίζονται με την αρχιτεκτονική 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 - το ίδιο που διαμορφώσαμε. Αυτό σημαίνει ότι το πρόγραμμα δεν βγάζει τίποτα επειδή δεν μπορεί να έχει πρόσβαση στην κλήση συστήματος εγγραφής:

[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. Ο David είναι πολύ παθιασμένος με τα γραφήματα φλόγας και πάντα αναζητά τη βελτιστοποίηση της απόδοσης.

Λορέντζο Φοντάνα εργάζεται στην ομάδα ανοιχτού κώδικα στο Sysdig, όπου επικεντρώνεται κυρίως στο Falco, ένα έργο του Cloud Native Computing Foundation που παρέχει ασφάλεια χρόνου εκτέλεσης κοντέινερ και ανίχνευση ανωμαλιών μέσω μιας μονάδας πυρήνα και eBPF. Είναι παθιασμένος με τα κατανεμημένα συστήματα, τη δικτύωση που ορίζεται από λογισμικό, τον πυρήνα του Linux και την ανάλυση απόδοσης.

» Περισσότερες λεπτομέρειες για το βιβλίο μπορείτε να βρείτε στη διεύθυνση ιστοσελίδα του εκδότη
» πίνακας περιεχομένων
» Απόσπασμα

Για Khabrozhiteley 25% έκπτωση με χρήση κουπονιού - Linux

Με την πληρωμή της έντυπης έκδοσης του βιβλίου, θα αποσταλεί ηλεκτρονικό βιβλίο με e-mail.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο