pkexec : μια ανάλυση της ευπάθειας.

Πριν λίγες μέρες είχε προκύψει ένα σημαντικό κενό ασφάλειας στο Polkit. Κοιτάζοντας το πρόβλημα λίγο σε βάθος είδα κάτι παράξενο στην συμπεριφορά μιας σημαντικής κλήσης συστήματος της execve(2). Η κλήση αυτή είναι υπεύθυνη για την εκτέλεση ενός προγράμματος. Και το κενό ασφάλειας προέκυπτε επειδή το πρόγραμμα μπορούσε να ήταν κενό!.

Αν και το πρόβλημα λύθηκε με πολύ απλό τρόπο, απλά τσέκαρε αν το πρόγραμμα που θέλεις να τρέξεις δεν είναι κενό, η πραγματική λύση, είναι να το ελέγχει ο πυρήνας. Γιατί δεν το έκανε αυτό το προφανές; Απορία το είχα.

Η απάντηση στο ερώτημα έχει να κάνει με την μακρά ιστορία του UNIX. Το POSIX απαιτεί να μην κάνεις το έλεγχο και υπάρχουν προγράμματα που βασίζονται σε αυτό. Και ο πυρήνας έχει πολιτική να μην σπάει ποτέ το API. Έτσι αν και το πρόβλημα ήταν γνωστό, δεν διορθώθηκε.

Όσοι ξέρουν λίγο C σίγουρα θα έχουν πέσει πάνω στην παρακάτω γραμμή, εδώ σε όλο της το μεγαλείο, που είναι η συνάρτηση με την οποία ξεκινάει κάθε πρόγραμμα [1]

    int main(int argc, char *argv[], char *envp[]) {
    ....
    return EXIT_SUCESS;
    }

To envp είναι το περιβάλλον εκτέλεσης όπως ορίζεται από τις μεταβλητές περιβάλλοντος. Και κάτι που δεν ήξερα πως ισχύει και είναι αναλλοίωτο είναι η παρακάτω σχέση:

    envp == argv + argc + 1

Αν λοιπόν argv[0] == NULL θα τρέξει το σύστημα χαρωπά ότι υπάρχει στο περιβάλλον. Και έχει το suid bit άρα μπορεί να τρέξει τα πάντα σαν οποιοσδήποτε χρήστης. Αχ αυτό πραγματικά πόνεσε :smiling_face_with_tear: :smiling_face_with_tear:. Μπορεί να μην το έπιασες αμέσως γιατί ίσως να μην ξέρεις C, δεν είναι προαπαιτούμενο, αλλά πίστεψε με, πραγματικά πόνεσε.

Για μια καλύτερη ανάλυση, για την ιστορία της ευπάθειας, και για τις προτάσεις που έγιναν για να αντιμετωπιστεί διάβασε:

Ο προγραμματισμός λοιπόν σε χαμηλό επίπεδο έχει τις παγίδες του. Ελπίζω να βρήκατε το παραπάνω κάπως χρήσιμο. Όσο για το τι μπορεί να γίνει χωρίς να σπάσει (τουλάχιστον πολύ) το API κάτι θα βρεθεί. Το BSD κάνει τον έλεγχο ήδη και δεν έγινε κάτι τραγικό. Τα περισσότερα προγράμματα τρέχουν μια χαρά, αλλά το BSD δεν τρέχει όσα τρέχει το Linux. Βέβαια τα περισσότερα προγράμματα που επηρεάζονται είναι κακογραμμένα test, αλλά δεν έχει τόση σημασία. Ακόμα και αν είναι test αυτό που σπάει εξακολουθεί να σπάει. Σύντομα λοιπόν θα βρεθεί μια λύση.

Δεν ξέρω αν βρήκατε τα παραπάνω συναρπαστικά, ακατανόητα (ή και τα δύο). Αλλά ήταν μια πολύ εύκολη ανάλυση για να καταλάβεις μια ευπάθεια, αλλά και τους λόγους που υπήρξε. Καθώς και την δουλεία που συμβαίνει στον πυρήνα από τους προγραμματιστές του για μας. Σε μένα τουλάχιστον λύθηκε η ειλικρινής μου απορία “Μα καλά γιατί στο καλό δεν κάνει αυτόν τον απλό έλεγχο;” Ήξερα βέβαια πως η απάντηση δεν θα αργούσε να δοθεί.


[1] Δηλαδή αυτή που σαν προγραμματιστής βλέπεις να ξεκινάει, αλλά πριν φτάσεις σε αυτήν πολλά άλλα πράγματα μπορούν να συμβούν. Κάθε προγραμματιστής της C++ ξέρει πως να τρέξει πράγματα πριν από την main το γνωστό φιάσκο με τα στατικά αντικείμενα. Αλλά μπορείς να το κάνεις και με απλή C αν ξέρεις το πως. Αλλά ας κρατήσουμε τα πράγματα απλά.

[2] Μια άλλη ενδιαφέρουσα συζήτηση για το αν μπορούμε να “παρακάμψουμε” την χρήση suid προγραμμάτων σε μια μοντέρνα διανομή Linux.

6 «Μου αρέσει»

Δείτε και μια εκμετάλλευση της ευπάθεας σε βίντεο από το Hak5 :

Αφού υπάρχει η “pkexec” στα Linux Man Pages…κάποιοι θα την αξιοποιούσαν και προς την κακή κατεύθυνση … Μου είχε κάτσει πολύ άσχημα στο στομάχι όταν είδα αυτήν την εντολή καθώς έψαχνα για την καλύτερη δυνατή αξιοποίηση των fork/exec με ασφάλεια !
Δεν πήγε όμως το μυαλό μου σε μια τέτοια εξέλιξη…!!

Θα ξεκινήσω με μια απλή ερώτηση:

Ποια είναι η διαφορά μεταξύ των:
sudo su, sudo su -, sudo su - root, sudo -i και sudo -s ;

ΟΚ ίσως τελικά να μη είναι τόσο απλή η δουλεία της sudo. Όπως είπε κάποιος, αμα μπλέξεις με κληρονομικά δεν βγάζεις άκρη.

The thing with setuid/setgid is that the invoked privileged process inherits a lot of implicit state and context that people aren’t really aware of or fully understand. i.e. it’s not just env vars and argv, it’s cgroup memberships, audit fields, security contexts, open fds, child pids, parent pids, cpu masks, IO/CPU scheduling priorities, various prctl() settings, tty control, signal masks + handlers, … and so on. And it’s not even clear what gets inherited as many of these process properties are added all the time.

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

Η μόνη πατέντα που πήρε το UNIX στην πρώτη του έκδοση είναι το κόλπο με το suid bit. Και αν μείνεις στο κλασικό UNIX/Posix δεν υπάρχει άλλος δρόμος. Στο Linux ευτυχώς έχουμε τα capabilities που περιορίζουν την ανάγκη για το sudo (αλλά δεν θα μπορούσαν να αντικαταστήσουν το sudo ή to pkexec) καθώς και τον μηχανισμό ενεργοποίησης υπηρεσιών του systemd/polkit/dbus (μεταξύ άλλων). Και σιγά σιγά περνάμε σε πιο μοντέρνους και ασφαλείς τρόπους.

Μα αυτά είναι ενάντια στο πνεύμα του UNIX κουλουπού κουλουπου, η με άλλα λόγια λέμε και κανένα τραγούδι του Νταλάρα (σε εκτέλεση Μόνικα) να περάσει η ώρα.

Όσο υπάρχει λοιπόν ανάγκη για προγράμματα που θέλουν suid τόσο θα έχουμε πιθανά κενά ασφαλείας, ανεξάρτητα του πως τα λέμε. Ευτυχώς η ανάγκη για αυτά μειώνετε χρόνο με το χρόνο (παρά του ότι κάποιοι “επαναστάτες” τσινάνε και θέλουν καθαρό POSIX λέει λολ). Και όσο άνθρωποι γράφουν προγράμματα θα υπάρχουν bugs.

Το δεύτερο άρθρο στην αναρχική ανάρτηση λέει μια ιστορία.

Ξαναδιαβάστε

1 «Μου αρέσει»

sudo su , sudo su - , sudo su - root , sudo -i και sudo -s

Επίσης βλέπω μερικούς που έχουν ξεκινήσει να χρησιμοποιούν το doas που ήρθε από το OpenBSD.

Δεν υπάρχει καμία ουσιαστική διαφορά μεταξύ τους. Το doas είναι λίγο πιο απλό αλλά έχει και πιο απλές χρήσεις. Η προτίμηση έρχεται από source based διανομές γιατί είναι ευκολότερο να χτιστεί. Το sudo σαν πιο πολύπλοκο, έχεις περισσότερες δυνατότητες για να τα κάνεις σαλάτα. Αλλά και τα δύο ουσιαστικά (όπως και το pkexec) λύνουν το ίδιο πρόβλημα: πως μια ταπεινή διεργασία ανεβαίνει σκαλοπάτια και περνάει στην αριστοκρατία. Και στα δυο απαιτείτε η χρήση του suid bit. Και στα δύο θα έχεις τα γνωστά θέματα με τις διοχετεύσεις.

Είναι καλύτερο να καταργήσεις αυτήν την ανάγκη. Και την λύση την φέρνει το polkit και το dbus. Δεν θέλεις suid bit και δεν μπλέκεις με τα κληρονομικά. Η εργασία που θα εκτελέσει κάτι που θέλει αυξημένες αρμοδιότητες, θα ξεκινήσει με ένα καθαρό και πλήρες ελεγχόμενο παρελθόν.

Το να βάλεις το doas το βλέπω σαν μια προσωπική δήλωση περισσότερο, παρά για κάποια τεχνική ανάγκη.