Έχεις αναρωτηθεί ποτέ, γιατί για ένα απλό reboot
του server χρειάζεσαι sudo; Ενώ για το ping
δεν χρειάζεσαι; Ετοιμάσου να κολυμπήσεις στα βαθιά.
Εισαγωγή
Το παραδοσιακό σχήμα για τα δικαιώματα στο UNIX χωρίζει τα δικαιώματα των διεργασιών (χαϊδευτικό για τα προγράμματα που τρέχουν στην μνήμη) σε δυο κατηγορίες:
- Σε αυτές που μπορούνε να κάνουν τα πάντα παρακάμπτοντας όλους τους ελέγχους,
- και σε αυτά που περιορίζονται σε ένα σύνολο από ασφάλειες λειτουργίες.
Για παράδειγμα, την κλήση συστήματος reboot
, δεν θέλεις να μπορεί να την καλέσει οποιοδήποτε πρόγραμμα (καλό η κακόβουλο), αλλά μόνο ένας χρήστης που τρέχει με διακαιώματα root.
H λύση του suid
Αλλά πως θα γίνει ένας απλός χρήστης να μπορεί να κάνει reboot το σύστημα;
Μια λύση είναι να γνωρίζει τον κωδικό και να γίνει root. Μια καλύτερη λύση είναι να του δώσεις δικαιώματα sudo
και να τον περιορίσεις μόνο στην συγκεκριμένη εντολή.
Αλλά υπάρχει και ένας άλλος τρόπος να θέσεις στο εκτελέσιμο αρχείο την suid
flag. Ένα αρχείο με αυτή την flag επιτρέπει σε κάθε χρήστη να τρέξει το πρόγραμμα σαν το χρήστη που τρέχει το αρχείο. Υπάρχει και η sgid
flag όπου τρέχει με τα δικαιώματα της ομάδας που ανήκει το αρχείο.
Όταν βγήκε το πρώτο UNIX το κόλπο με το suid εφευρέθηκε από τον Dennis Ritchie το 1979 και το κατοχύρωσε με δίπλωμα ευρεσιτεχνίας αριθμού : US4135240. Φυσικά η συγκεκριμένη «πατέντα λογισμικού» πλέον έχει λήξει και ανήκει στο public domain.
Ας βρούμε στο σύστημα μας τα αρχεία που έχουν αυτές τις σημαίες
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
('rw
s
r-x r-x'
) και κάποιες όπως η /usr/bin/wall
το sgid
('rwx r-
s
r-x'
).
Ένα παράδειγμα
Το πρόγραμμα hello_server
είναι απλό και γραμμένο σε Rust, και είναι ένας απλός (πολύ απλός) web server. Εξ ορισμού θα προσπαθήσει να τρέξει στην port 80
, αν και μπορεί ο χρήστης να την αλλάξει με --port=XXX
. Υποστηρίζει επίσης το socket activation
του systemd
.
Ο κώδικας του προγράμματος:
// 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(())
}
Αυτή ζητά πρόσβαση στην απαγορευμένη πόρτα 80 (κάθε πόρτα με αριθμό μικρότερο του 1024 είναι απαγορευμένη στα προγράμματα). Ας δοκιμάσουμε να την τρέξουμε:
Running with user id=1000 and effective user id=1000 on port 80.
Error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
Αν όμως δοκιμάσουμε με μια μη προστατευμένη port πχ την 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 μπορούν να έχουν και οι διεργασίες).
Τα capabilities με ένα παράδειγμα
Για να ανοιχθεί μια προστατευμένη port θα πρέπει να έχει την CAP_NET_BIND_SERVICE
. Ας την δώσουμε στο πρόγραμμα μας με την εντολή setcap
.
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
.
getcap target/debug/hello_server
target/debug/hello_server = cap_net_bind_service+ep
Προτρέχω λίγο, επιστρέψτε σε αυτό το σχόλιο αργότερα, αλλά τι ακριβώς σημαίνει cap_net_bind_service+pe
; Δεν σημαίνει προσθήκη στα σύνολα Permitted και Εffective, αλλά το e
σημαίνει ενεργοποίηση του Effective 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
.
filecap -a 2> /dev/null | grep -v "/run/timeshift"
Επίσης μπορούμε να δούμε τα capabilities των διεργασιών που τρέχουν με την εντολή pscap
.
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
:
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, …
Μια διεργασία (για την ακρίβεια κάθε thread εκτέλεσης) έχει πολλά σύνολα από capabilities.
- Permitted είναι μια λίστα από capabilities που μια διεργασία μπορεί να χρησιμοποιήσει αν θέλει.
- Εffective είναι μια λίστα από capabilities που μια διεργασία χρησιμοποιεί μια δεδομένη στιγμή.
- Ιnheritable Αν η διεργασία ξεκινήσει ένα πρόγραμμα αυτές θα είναι οι Permitted* που θα έχει αν τρέχει σαν root.
- Bounding Ένας μηχανισμός περιορισμού του τι περνάει όταν η διεργασία τρέξει ένα πρόγραμμα με την
execve
. Είναι το υπερσύνολο όλων των δυνατών τιμών των υπολοίπων. - Ambient Τι περνάει από την
execve()
όταν το πρόγραμμα δεν τρέχει σαν root.
Ένα αρχείο έχει τα σύνολα Permitted και Ιnheritable. Επίσης το Εffective είναι ένα bit που λέει αν οι Εffective είναι κενές ή ίδιες με τις Permitted. Ο λόγος για αυτό είναι πως δεν ξέρουν όλα τα προγράμματα για τα capabilities και τις κλήσεις συστήματος που τις πειράζουν. Τα capabilities ενός αρχείου αποθηκεύονται σαν εκτεταμένες ιδιότητες και ισχύουν οι γνωστοί περιορισμοί

Όπως μάλλον έγινε προφανές εσωτερικά η κατάσταση είναι αρκετά πολύπλοκη και οι φρικτές λεπτομέρειες μπορούν να βρεθούν στο capabilities
. Μια καλή παρουσίαση, με παραδείγματα, υπάρχει εδώ. Στο ίδιο μέρος θα βρούμε και την λίστα των capabilities και τι κάνει κάθε μια. Υπάρχουν κάπου 40 διαφορετικές αυτή την στιγμή.
Capabilities και seccomp
Τα capabilities μοιάζουν πολύ με το seccomp που είναι ένα φίλτρο για τις κλήσεις συστήματος που επιτρέπεται να χρησιμοποιήσει μια διεργασία.
Αλλά δεν μπορείς να χρησιμοποιήσεις το σύστημα των αρχείων και θα πρέπει να χρησιμοποιήσεις κάποιο εξωτερικό πρόγραμμα (όπως το systemd
) για να κάνεις χρήση τους σε κάποιο πρόγραμμα που δεν τις χρησιμοποιεί στον κώδικα του.
Ένα capability επηρεάζει πολλές κλήσεις συστήματος και στην πράξη είναι ποιο εύκολο να βρεις τι απαιτεί ένα εκτελέσιμο, από το να κυνηγάς τις κλήσεις συστήματος.
Capabilities και systemd
Ένας άλλος τρόπος για υπηρεσίες συστήματος είναι αντί να τις προσθέσουμε στο αρχείο να χρησιμοποιήσουμε το systemd
. Υπάρχουν δυο directives.
AmbientCapabilities=
Προσθέτει capabilities σε μια υπηρεσία που κανονικά δεν θα είχε. Αρκεί συνήθως να θέσουμε αυτό.CapabilityBoundingSet=
Αφαιρεί capabilities απο μια υπηρεσία, έτσι δεν θα τις έχει ακόμα και αν τρέχει σαν χρήστηςroot
.
Για περισσότερα systemd.exec(5).
Πώς να βρω τι capabilities απαιτούνται
Ένας τρόπος είναι να χρησιμοποιήσεις την strace
και να δεις ποιες κλήσεις αποτυγχάνουν. Αλλά με τον τρόπο αυτό είναι δύσκολο να βρεις τι πραγματικά απαιτείται και τι όχι.
Ας δούμε ένα άλλο τρόπο. Με την εντολή capsh
έχουμε ένα κέλυφος με ενεργά κάποια 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
για να φτιάξουμε ένα περιβάλλον εκτέλεσης με ενεργά κάποια επιπλέον 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)
και πλέον μπορούμε να κάνουμε τις δοκιμές μας μέχρι να καταφέρουμε να τρέξει
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
.
Ένα σύντομο σχόλιο για την εντολή ping
:
Στα περισσότερα άρθρα για τα capabilities χρησιμοποιούν την εντολή ping
. Αλλά η ping
μπορεί να κάνει χρήση ενός ειδικού χαρακατηριστικού του πυρήνα (που υπάρχει εδώ και κοντά 10 χρόνια), και δεν θέλει καμία ειδική δυνατότητα. Το χαρακτηριστικό είναι απενεργοποιημένο στις περισσότερες διανομές και ή μόνη που του κάνει χρήση, από όσο γνωρίζω, είναι η Alpine. Για τον λόγο αυτό προτιμήθηκε να γραφτεί ένα απλό πρόγραμμα από την αρχή.
Αν και η πλήρη κατανόηση των capabilities είναι μια απαιτητική θα έλεγα διαδικασία, τα βασικά για αυτά θα πρέπει να τα ξέρει σήμερα κάθε SysAdmin. Ήταν ένα πολύ απαιτητικό άρθρο τόσο για τον συγγραφέα όσο και για τον αναγνώστη . Και σίγουρα, δεν μπορεί… κάποιο λάθος θα έχει γίνει. Διορθώστε με στα σχόλια.
Αν κρατήσεις κάτι από το άρθρο κράτησε τις εντολές setcap, getcap καθώς και την γενική ιδέα. Είδαμε στην πράξη πως δεν είναι και τόσο δύσκολο. Και κύδος (kudos) που τα κατάφερες και έφτασες μέχρι εδώ .
Σου άρεσε το άρθρο; Πες την άποψή σου... έστω και Ανώνυμα: