Περί capabilities ή πως να καταργήσετε το sudo με ασφάλεια

Εισαγωγή

Το παραδοσιακό σχήμα για τα δικαιώματα στο UNIX χωρίζει τα δικαιώματα των διεργασιών (χαϊδευτικό για τα προγράμματα που τρέχουν στην μνήμη) σε δυο κατηγορίες: Σε αυτές που μπορούνε να κάνουν τα πάντα παρακάμπτοντας όλους τους ελέγχους, και σε αυτά που περιορίζονται σε ένα σύνολο από ασφάλειες λειτουργίες. Για παράδειγμα την κλήση συστήματος reboot(2), δεν θέλεις να μπορεί να την καλέσει οποιοδήποτε πρόγραμμα, αλλά μόνο ένας χρήστης που τρέχει με διακαιώματα root.

H λύση του suid

Αλλά πως θα γίνει ένας απλός χρήστης να μπορεί να κάνει reboot το σύστημα; Μια λύση είναι να γνωρίζει τον κωδικό και να γίνει root. Μια καλύτερη λύση είναι να του δώσεις δικαιώματα sudo και να τον περιορίσεις μόνο στην συγκεκριμένη εντολή. Αλλά υπάρχει και ένας άλλος τρόπος να θέσεις στο εκτελέσιμο αρχείο την σημαία 'suid'. Ένα αρχείο με αυτή την σημαία επιτρέπει σε κάθε χρήστη να τρέξει το πρόγραμμα σαν το χρήστη που τρέχει το αρχείο. Υπάρχει και η σημαία sgid όπου τρέχει με τα δικαιώματα της ομάδας που ανήκει το αρχείο.

Όταν βγήκε το πρώτο UNIX ή μόνη πατέντα που κατοχύρωσε η AT&T ήταν το κόλπο με το suid. Σήμερα έχει λήξει και ανήκει στο public domain. Ήταν πιο αγνές οι εποχές τότε :sweat_smile:.

Ας βρούμε στο σύστημα μας τα αρχεία που έχουν αυτές τις σημαίες

 sudo find / -perm /u=s,g=s \
             -and -not -type d \
             -and -not -wholename '/run/timeshift/*' \
             -and -not -wholename '/proc/*'  \
             -exec ls -la --time-style=+"" {} \; 2> /dev/null

Μερικά από αυτά

sudo  find / -perm -4000 -exec ls -l  --time-style=+"" {} \;
rwsr-xr-x 1 root root 55528  /bin/mount
-rwsr-xr-x 1 root root 67816  /bin/su
-rwsr-xr-x 1 root root 39144  /bin/umount
-rwxr-sr-x 1 root crontab 43720  /usr/bin/crontab
-rwxr-sr-x 1 root mlocate 47344  /usr/bin/mlocate
-rwxr-sr-x 1 root tty 35048  /usr/bin/wall
-rwxr-sr-x 1 root ssh 350504  /usr/bin/ssh-agent
-rwsr-xr-x 1 root root 393184  /usr/bin/firejail
-rwsr-xr-x 1 root root 85064  /usr/bin/chfn
-rwsr-xr-x 1 root root 53040  /usr/bin/chsh
-rwsr-xr-x 1 root root 68208  /usr/bin/passwd
-rwsr-xr-x 1 root root 166056  /usr/bin/sudo

Κάποιες εντολές όπως η /usr/bin/sudo θέτει το suid ('rws r-x r-x') και κάποιες όπως η /usr/bin/wall το sgid ('rwx r-s r-x').

Ένα παράδειγμα

Στο παράρτημα έχουμε τον κώδικά μιας απλής υπηρεσίας web. Αυτή ζητά πρόσβαση στην απαγορευμένη πόρτα 80 (κάθε πόρτα με αριθμό μικρότερο του 1024 είναι απαγορευμένη στα προγράμματα). Ας δοκιμάσουμε να την τρέξουμε

Running with user id=1000 and effective user id=1000 on port 80.
Error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }

Αν όμως δοκιμάσουμε με μια μη προστατευμένη πόρτα πχ την 3000 με την παράμετρο -p 3000 θα τρέξει κανονικά.

Ας το κάνουμε τώρα suid. Δεν αρκεί να του δώσουμε την σημαία, θα πρέπει και να το δώσουμε στον χρήστη root.

sudo chown root:root target/debug/hello_server
sudo chmod u+s target/debug/hello_server

Και τώρα τρέχει μια χαρά σαν χρήστης με uid=1000 μεν, αλλά με δικαιώματα root (euid=0).

Running with user id=1000 and effective user id=0 on port 80.

Παρατηρήσεις

Το πρόγραμμα θέλει αυξημένα δικαιώματα μόνο για να κάνει μια εργασία. Να αποκτήσει πρόσβαση σε μια απαγορευμένη πόρτα. Ένα ασφαλές πρόγραμμα θα πρέπει να κάνει κάτι από τα παρακάτω:

  • Να αφήσει τα αυξημένα δικαιώματα μόλις ανοίξει την πόρτα (αν φιλοτιμηθεί ο προγραμματιστής).
  • Να ανοίξει την πόρτα και να την περάσει σε ένα δεύτερο πρόγραμμα που δεν θα έχει suid
  • Να χρησιμοποιήσει το socket activation του systemd και να πάρει έτοιμη την πόρτα. Αυτή είναι και η καλύτερη λύση για υπηρεσίες συστήματος.

Αλλά υπάρχει και ένας πιο απλός τρόπος με την χρήση των capabilities.

Τι είναι τα capabilities

Στο παραδοσιακό σχήμα η λύση είναι είτε όλα είτε τίποτα. Στο Linux με τα capabilities κάθε προστατευμένη κλήση συστήματος απαιτεί το εκτελέσιμο να έχει ένα capability. (Θα δούμε παρακάτω ότι capabilities μπορούν να έχουν και οι διεργασίες).

Ένα παράδειγμα

Για να ανοιχθεί μια προστατευμένη πόρτα θα πρέπει να έχει την 'CAP_NET_BIND_SERVICE'. Ας την δώσουμε στο πρόγραμμα μας με την εντολή setcap(8).

sudo setcap "cap_net_bind_service=pe" target/debug/hello_server

και πλέον το πρόγραμμα θα τρέξει κανονικά σαν απλός χρήστης.

Running with user id=1000 and effective user id=1000 on port 80.

Μπορούμε να βρούμε τα capabilities κάποιου προγράμματος με την εντολή getcap(8).

getcap target/debug/hello_server
target/debug/hello_server = cap_net_bind_service+ep

Προτρέχω λίγο, επιστρέψτε σε αυτό το σχόλιο αργότερα, αλλά τι ακριβώς σημαίνει "cap_net_bind_service+pe"; Δεν σημαίνει προσθήκη στα σύνολα Permitted και Εffective, αλλά το 'e' σημαίνει ενεργοποιήση του Εffective bit.

Εξερευνώντας το σύστημα μας

Ας βρούμε τα αρχεία που έχουν capabilities στο σύστημα μας με την εντολή

getcap -r / 2>/dev/null | grep -v "/run/timeshift"

Δεν βρήκα και πολλά, μερικά από αυτά είναι

cap_net_bind_service+ep
/bin/ping = cap_net_raw+ep
/usr/bin/arping = cap_net_raw+ep
/usr/bin/gnome-keyring-daemon = cap_ipc_lock+ep
/usr/bin/mtr-packet = cap_net_raw+ep
/usr/bin/traceroute6.iputils = cap_net_raw+ep
...

Σίγουρα θα μπορούσε κάποια από την λίστα των suid/sgid θα να προστεθούν σε αυτή την λίστα. Σε μια μοντέρνα διανομή δεν είναι ευτυχώς πολλά τα προγράμματα που θέλουν να τρέχουν με suid και έχουμε και κάποια που κάνουν χρήση των capabilities.

Ένας άλλος τρόπος να δούμε την ίδια λίστα είναι με την εντολή filecap(8).

filecap -a  2> /dev/null | grep -v "/run/timeshift"

Επίσης μπορούμε να δούμε τα capabilities των διεργασιών που τρέχουν με την εντολή pscap(8).

pscap -a

Μερικά αποτελέσματα

ppid  pid   name        command           capabilities
0     1     root        systemd           full
1     498   root        systemd-journal   chown, dac_override, ...
1     546   root        systemd-udevd     full
1     997   root        haveged           sys_admin
1     1066  root        accounts-daemon   full
1     1067  root        acpid             full
1     1242  root        ModemManager      sys_admin
1     1276  root        lightdm           full
....

Ας δούμε και τι capabilities χρησιμοποιούν οι υπηρεσίες συστήματος netcap(8):

sudo  netcap
ppid  pid   acct       command          type port   capabilities
1     1292  root       nginx            tcp  80     full 
1     2231  root       hddtemp          tcp  7634   full 
1     1023407 root       cupsd            tcp  631    full 
1     1292  root       nginx            tcp6 80     full 
0     1     root       systemd          tcp6 22     full 
1     1023407 root       cupsd            tcp6 631    full 
0     1     root       systemd          tcp6 9090   full 
1     1023408 root       cups-browsed     udp  631    full 
1     1083  root       NetworkManager   udp6 546    dac_override, kill,  ....
1     1172639 root       atop             raw  0      full 
1     1083  root       NetworkManager   raw6 0      dac_override,  ...
....

Permitted, effective, inheritable, …

Μια διεργασία (για την ακρίβεια κάθε νήμα εκτέλεσης) έχει πολλά σύνολα από capabilities.

  • Permitted είναι μια λίστα από capabilities που μια διεργασία μπορεί να χρησιμοποιήσει αν θέλει.
  • Εffective είναι μια λίστα από capabilities που μια διεργασία χρησιμοποιεί μια δεδομένη στιγμή.
  • Ιnheritable Αν η διεργασία ξεκινήσει ένα πρόγραμμα αυτές θα είναι οι Permitted* που θα έχει αν τρέχει σαν root.
  • Bounding Ένας μηχανισμός περιορισμού του τι περνάει όταν η διεργασία τρέξει ένα πρόγραμμα με την execve(2). Είναι το υπερσύνολο όλων των δυνατών τιμών των υπολοίπων.
  • Ambient Τι περνάει από την 'execve()' όταν το πρόγραμμα δεν τρέχει σαν root.

Ένα αρχείο έχει τα σύνολα Permitted και Ιnheritable. Επίσης το Εffective είναι ένα bit που λέει αν οι Εffective είναι κενές ή ίδιες με τις Permitted. Ο λόγος για αυτό είναι πως δεν ξέρουν όλα τα προγράμματα για τα capabilities και τις κλήσεις συστήματος που τις πειράζουν. Τα capabilities ενός αρχείου αποθηκεύονται σαν εκτεταμένες ιδιότητες και ισχύουν οι γνωστοί περιορισμοί.



Ένα απλοποιημένο (σικ) διάγραμμα Image credit H. Plötz

Όπως μάλλον έγινε προφανές εσωτερικά η κατάσταση είναι αρκετά πολύπλοκη και οι φρικτές λεπτομέρειες μπορούν να βρεθούν στο capabilities(7). Στο ίδιο μέρος θα βρούμε και την λίστα των capabilities και τι κάνει κάθε μια. Υπάρχουν κάπου 40 διαφορετικές αυτή την στιγμή. Μια καλή παρουσίαση, με παραδείγματα, υπάρχει εδώ.

Capabilities και seccomp

Τα capabilities μοιάζουν πολύ με το seccomp που είναι ένα φίλτρο για τις κλήσεις συστήματος που επιτρέπετε να χρησιμοποιήσει μια διεργασία. Αλλά δεν μπορείς να χρησιμοποιήσεις το σύστημα των αρχείων και θα πρέπει να χρησιμοποιήσεις κάποιο εξωτερικό πρόγραμμα (όπως το systemd) για να κάνεις χρήση τους σε κάποιο πρόγραμμα που δεν τις χρησιμοποιεί στον κώδικα του.
Ένα capability επηρεάζει πολλές κλήσεις συστήματος και στην πράξη είναι ποιο εύκολο να βρεις τι απαιτεί ένα εκτελέσιμο, από το να κυνηγάς τις κλήσεις συστήματος.

Capabilities και systemd

Ένας άλλος τρόπος για υπηρεσίες συστήματος είναι αντί να τις προσθέσουμε στο αρχείο να χρησιμοποιήσουμε το systemd. Υπάρχουν δυο directives.

  • AmbientCapabilities= Προσθέτει capabilities σε μια υπηρεσία που κανονικά δεν θα είχε. Αρκεί συνήθως να θέσουμε αυτό.
  • CapabilityBoundingSet= Αφαιρεί capabilities απο μια υπηρεσία, έτσι δεν θα τις έχει ακόμα και αν τρέχει σαν χρήστης root.

Για περισσότερα systemd.exec(5).

Πώς να βρω τι capabilities απαιτούνται

Ένας τρόπος είναι να χρησιμοποιήσεις την strace(1) και να δεις ποιές κλήσεις αποτυγχάνουν. Αλλά με τον τρόπο αυτό είναι δύσκολο να βρείς τι πραγματικά απαιτείτε και το όχι. Ας δούμε ένα άλλο τρόπο. Με την εντολή capsh(1) έχουμε ένα κέλυφος με ενεργά κάποια capabilities, αλλά θα την χρησιμοποιήσουμε μόνο για να δούμε τι έχουμε στο κέλυφος μας.

capsh --print
Current: =
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,.....
Ambient set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=1000(talos) euid=1000(talos)
gid=1000(talos)
groups=27(sudo),46(plugdev),108(kvm),114(lpadmin),...
Guessed mode: UNCERTAIN (0)

Θα χρησιμοποιήσουμε την εντολή setpriv(1) για να φτιάξουμε ένα περιβάλλον εκτέλεσης με ενεργα κάποια επιπλέον capabilities:

sudo setpriv --inh-caps +net_bind_service --ambient-caps +all \
             --reuid 1000 -- capsh --print
Current: = cap_net_bind_service+eip
Bounding set =cap_chown, ...,  cap_net_bind_service,... 
Ambient set =cap_net_bind_service
uid=1000(talos) euid=1000(talos)
gid=0(root)
groups=0(root)

και πλέον μπορούμε να κάνουμε τις δοκιμές μας μέχρι να καταφέρουμε να τρέξει :sweat_smile:

sudo setpriv --inh-caps +net_bind_service --ambient-caps +all \
             --reuid 1000 -- target/debug/hello_server -p 80
Running with user id=1000 and effective user id=1000 on port 80.

POSIX συμβατότητα

Υπήρξε μια προσπάθεια για τυποποίηση, αλλά όπως τόσες άλλες σταμάτησε και μέναμε στο draft. Το Linux έχει βασιστεί σε αυτό, αλλά δεν το ακολουθεί και έχει προσθέσει και πολλές δικές του. Αντι για απευθείας χρήση είναι καλύτερο να χρησιμοποιηθεί η libcap-ng.

Συμπεράσματα

Τα capabillities αντικαθιστούν τον μηχανισμό του suid ένα μόνιμο πονοκέφαλο για την ασφάλεια κάθε UNIX συστήματος. Με την βοήθεια τους μπορούμε να έχουμε ένα σύστημα πολύ ποιο ασφαλές και τα χρησιμοποιούν προγράμματα όπως το docker το firejail και το systemd. Αν και η πλήρη κατανόηση τους είναι μια απαιτητική θα έλεγα διαδικασία, τα βασικά για αυτά θα πρέπει να τα ξέρει σήμερα κάθε διαχειριστής συστήματος.

Ήταν ένα πολύ απαιτητικό άρθρο τόσο για τον συγγραφέα όσο και για τον αναγνώστη . Και σίγουρα, δεν μπορεί κάποιο λάθος θα το έχει :sweat_smile:. Διορθώστε με στα σχόλια. Αν κρατήσεις κάτι από το άρθρο κράτησε τις εντολές setcap(8), getcap(8) καθώς και την γενική ιδέα. Είδαμε στην πράξη πως δεν είναι και τόσο δύσκολο. Και κύδος (kudos) που τα κατάφερες και έφτασες μέχρι εδώ :slight_smile:.

Παράρτημα

Το πρόγραμμα hello_server είναι απλό και γραμμένο σε :rust:, και είναι ένας απλός (πολύ απλός) web server. Εξ ορισμού θα προσπαθήσει να τρέξει στην πόρτα 80, αν και μπορεί ο χρήστης να την αλλάξει με --port=XXX. Υποστηρίζει επίσης το socket activation του systemd.

Ο κώδικας του προγράμματος.

main.rs
// Put this on Cargo.toml
// [dependencies]
// actix-web = "2.0.0"
// actix-rt = "1.1.1"
// listenfd = "0.3.3"
// getopts = "0.2.21"
// libc = "0.2"

extern crate getopts;
extern crate libc;
use getopts::Options;

use actix_web::{web, App, HttpRequest, HttpServer};
use listenfd::ListenFd;

async fn index(_req: HttpRequest) -> &'static str {
    "Welcome, my son,
Welcome to the machine.
Where have you been?
It's alright, we know where you've been"
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {

    let args: Vec<String> = std::env::args().collect();
    let mut opts = Options::new();
    opts.optopt("p", "port", "Bind port", "<port default 3000>"); 
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => { m }
        Err(f) => { panic!(f.to_string()) }
    };

    let port = match matches.opt_str("port") {
        Some(x) =>  { x }
        None => { "80".to_string() }
    };

    unsafe {
        let uid =  libc::getuid(); 
        let euid = libc::geteuid();
        print!("Running with user id={} and effective user id={} on port {}.\n",uid,euid,port);
    }

    let mut server = HttpServer::new(|| {
        App::new().service(web::resource("/").to(index))
    });

    let mut listenfd = ListenFd::from_env();    
    server = match listenfd.take_tcp_listener(0)? {
        Some(listener) => server.listen(listener)?,
        None => server.bind(format!("127.0.0.1:{}",port))?,
    };

    server.run().await?;

    Ok(())
}

Η εντολή ping

Ένα σύντομο σχόλιο για την εντολή ping:

Στα περισσότερα άρθρα για τα capabilities χρησιμοποιούν την εντολή ping. Αλλά η ping μπορεί να κάνει χρήση ενός ειδικού χαρακατηριστικού του πυρήνα (που υπάρχει εδώ και κοντά 10 χρόνια), και δεν θέλει καμία ειδική δυνατότητα. Το χαρακτηριστικό είναι απενεργοποιημένο στις περισσότερες διανομές και ή μόνη που του κάνει χρήση, από όσο γνωρίζω, είναι η Alpine. Για τον λόγο αυτό προτιμήθηκε να γραφτεί ένα απλό πρόγραμμα από την αρχή.