Το προφίλ μας στο Google Plus
0

Ανάλυση κρίσιμου προβλήματος ασφαλείας στη glibc

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

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

Η ενασχόλησή μας με το θέμα ξεκίνησε ένα ήσυχο πρωινό, όταν ο γράφων άνοιξε το email στη δουλειά και βρήκε μια ντουζίνα ερωτήσεων κι απαντήσεων, γύρω από ένα ζήτημα ασφαλείας. Ένας από τους professional services architects της εταιρείας είχε διαβάσει ένα άκρως ανησυχητικό άρθρο και είχε πανικοβληθεί. Με το δίκιο του ο άνθρωπος, αφού η αδυναμία για την οποία έκανε λόγο το άρθρο αφορούσε σε κάθε εφαρμογή που κάνει χρήση του DNS και βασίζεται στη βιβλιοθήκη glibc. Όλα τα σχετικά emails, λοιπόν, απευθύνονταν στους μηχανικούς που είχαν υλοποιήσει τέτοιες εφαρμογές και τους προέτρεπαν να τσεκάρουν κατά πόσο το bug επηρέαζε τα δικά τους προγράμματα.

Βάσεις αδυναμιών
Κατά πάσα πιθανότητα, κάθε μεγάλη εταιρεία που αναπτύσσει εφαρμογές ή μια ολόκληρη διανομή Linux, θα συντηρεί και κάποιο σύστημα για την έγκαιρη αντιμετώπιση των κενών ασφαλείας. Αυτός είναι ο λόγος που όταν δημοσιεύεται κάποια αδυναμία, περιγράφεται με κάποιο περίεργο ακρωνύμιο και έναν αριθμό. Για παράδειγμα, πολύ συχνά συναντάμε αυτά τα μυστήρια CVE και RHSA. Ας δούμε τι σημαίνουν.

  • CVE, Common Vulnerability Exposure. To CVE project συντηρείται από το MITRE Corporation, έναν μη κερδοσκοπικό οργανισμό που εκτελεί εργασίες έρευνας κι ανάπτυξης χρηματοδοτούμενες από την κυβέρνηση των ΗΠΑ.
  • RHSA, RedHat Security Advisories. Συμβουλευτικά Δελτία Ασφαλείας από τη RedHat. Η εταιρεία διατηρεί μια πολύ οργανωμένη βάση δεδομένων στην οποία συνδέει τα διάφορα CVEs με τα προϊόντα της. Το CVE για τη glibc βρίσκεται στο RedHat Security Advisories. Στην ιστοσελίδα εξηγείται ποιο ακριβώς είναι το πρόβλημα και πώς λύνεται στα προγράμματα της εταιρείας. Επιπροσθέτως, αναφέρει αναλυτικά τα πακέτα που πρέπει να ενημερωθούν άμεσα.

Η αδυναμία ανακαλύφθηκε από έναν μηχανικό της Google, ο οποίος παρατήρησε ότι ο SSH client “έσκαγε” κάθε φορά που προσπαθούσε να συνδεθεί σ’ ένα συγκεκριμένο σύστημα. Ο ίδιος μηχανικός καταχώρησε ένα ticket στο σχετικό σύστημα της εταιρείας και ξεκίνησε να διερευνά το πρόβλημα. Αυτό μπορεί να σας φαίνεται απίστευτο, αφού στις ελληνικές εταιρίες λογισμικού είναι αδιανόητο ότι κάποιος μηχανικός θα ασχοληθεί έστω και πέντε λεπτά με κάποιο θέμα που αφορά την παγκόσμια κοινότητα. Στις εταιρίες του Silicon Valley όμως τα πράγματα δεν είναι έτσι. Ειδικά στην Google, το 20% του χρόνου των μηχανικών αφιερώνεται σε projects ανοικτού λογισμικού. Τελικά, μετά από ενδελεχή έρευνα, οι μηχανικοί της Google που ασχολήθηκαν με το συγκεκριμένο θέμα ανακάλυψαν ότι το πρόβλημα βρίσκεται στη glibc κι όχι στο SSH. Μιλάμε για μια βασική βιβλιοθήκη (GNU libc) του Linux, που χρησιμοποιείται σε αμέτρητα προγράμματα. Η συγκεκριμένη ευπάθεια υπό συνθήκες επέτρεπε την απομακρυσμένη εκτέλεση κώδικα (remote code execution) στο σύστημα με την ευπαθή εφαρμογή. Περιττό να πούμε ότι μετά τον αρχικό εντοπισμό ξεκίνησε μια λεπτομερής ανάλυση του προβλήματος κι άρχισαν να γράφονται οι πιθανές λύσεις. Οι μηχανικοί της Google αντιμετώπισαν την όλη προσπάθεια σαν μια σπουδαία πρόκληση, καθώς η ευπάθεια επηρέαζε ένα τεράστιο πλήθος συστημάτων. Ξέρετε ποιο ήταν ένα από τα πρώτα πράγματα που ανακάλυψαν; Οι μηχανικοί που συντηρούσαν την glibc είχαν εντοπίσει την ευπάθεια κι είχαν καταχωρήσει ένα σχετικό bug από τον Ιούλιο του 2015! Συνεχίζοντας την έρευνα, οι μηχανικοί της Google διαπίστωσαν ότι το πρόβλημα είχε εντοπιστεί κι από δύο μηχανικούς της Red Hat, οι οποίοι είχαν αρχίσει να ερευνούν το θέμα ανεξάρτητα. Κάπως έτσι οι δύο ομάδες ξεκίνησαν τη συνεργασία και κατέληξαν στην έκδοση μιας αναλυτικής έκθεσης, που περιέγραφε το πρόβλημα και την αντιμετώπιση (δείτε εδώ).

Το πρόβλημα
Οι αρχικές δοκιμές έδειξαν ότι η ευπάθεια υπήρχε σε όλες τις εκδόσεις της glibc, από τη 2.9 και μετά. Ένα τμήμα του κώδικα της βιβλιοθήκης, που σχετίζεται με το χειρισμό του DNS, είναι ευάλωτο και επιτρέπει την πραγματοποίηση επιθέσεων μέσω της υπερχείλισης στοίβας (stack overflow). Συγκεκριμένα, ο τρωτός κώδικας βρίσκεται στη συνάρτηση getaddrinfo(). Τι σημαίνει αυτό στην πράξη; Τα προγράμματα που χρησιμοποιούν την εν λόγω συνάρτηση και απευθύνουν DNS queries προς διάφορους name servers, θα μπορούσαν να πέσουν θύματα ενός χάκερ που ελέγχει έναν τέτοιο server. Η ευπάθεια σχετίζεται με το μέγεθος της απάντησης (response) στο ερώτημα και εκδηλώνεται ανεξάρτητα από το αν χρησιμοποιείται το πρωτόκολλο UDP ή το TCP. Αν η απάντηση ξεπερνά σε μέγεθος τα 2048 bytes, προκαλείται υπερχείλιση στοίβας! Η προτεινόμενη λύση είναι ο περιορισμός του μεγέθους της απάντησης, είτε με το DNSMasq είτε με άλλα παρεμφερή προγράμματα. Επιπρόσθετα, πρέπει να φροντίζουμε ώστε τα ερωτήματα DNS να στέλνονται αποκλειστικά σε DNS servers που περιορίζουν το μέγεθος του response και χρησιμοποιούν και το truncation bit.

Αναγκαία ορολογία
Εσείς γνωρίζετε τους όρους stack και heap; Ο πρώτος μεταφράζεται σαν στοίβα και ο δεύτερος σαν σωρός, ενώ αναφέρονται και οι δύο σε περιοχές της μνήμης. Ωστόσο, η ακριβής ερμηνεία του καθενός δεν είναι γνωστή σε όλους. Για να κατανοήσουμε σε βάθος την ευπάθεια στη glibc, πρέπει να ξεδιαλύνουμε τους δύο όρους.

Με τον όρο stack αναφερόμαστε σε μια ειδική περιοχή μνήμης, όπου αποθηκεύονται οι μεταβλητές κάθε συνάρτησης. Κι όταν λέμε κάθε συνάρτηση, αναφερόμαστε ακόμα και στη main(). Η στοίβα αποτελεί μια δομή δεδομένων τύπου LIFO (Last In, First Out), την οποία διαχειρίζεται το λειτουργικό και ο επεξεργαστής. Κάθε φορά που ξεκινά η εκτέλεση μιας συνάρτησης, δεσμεύεται χώρος για τη δημιουργία μιας αντίστοιχης στοίβας. Η στοίβα μπορεί να μην έχει σταθερό μέγεθος, αλλά εκτείνεται σε μια περιοχή που έχει συγκεκριμένη έκταση. Στη στοίβα μιας συνάρτησης τοποθετούνται οι μεταβλητές που δημιουργούνται εντός της συγκεκριμένης συνάρτησης. Αντίστοιχα, όταν τερματίζεται μια συνάρτηση, η περιοχή της μνήμης που είχε δεσμευτεί για τη στοίβα απελευθερώνεται κι αυτό σημαίνει ότι όλες οι μεταβλητές της συνάρτησης διαγράφονται/χάνονται. Η χρήση της stack για την αποθήκευση μεταβλητών προσφέρει αρκετά πλεονεκτήματα. Το σημαντικότερο είναι ότι ο εκάστοτε προγραμματιστής δεν χρειάζεται να εκτιμήσει τον όγκο των δεδομένων και να δεσμεύει το αντίστοιχο ποσό μνήμης, ούτε να θυμηθεί να αποδεσμεύσει το σχετικό χώρο όταν δεν θα χρειάζεται τη μεταβλητή. Εξάλλου, η εγγραφή και η ανάγνωση μεταβλητών στη στοίβα πραγματοποιείται ταχύτατα. Τελικά, αυτά που πρέπει να θυμάται κανείς για τη στοίβα συνοψίζονται στα εξής:

  • Η στοίβα μεγαλώνει καθώς μια συνάρτηση ορίζει νέες μεταβλητές.
  • Η στοίβα μπορεί να επεκταθεί μέχρι ενός ορίου, αφού εκτείνεται σε μια περιοχή συγκεκριμένου μεγέθους.
  • Ο προγραμματιστής δεν χρειάζεται να μπλέκει με τη διαχείριση μνήμης και αυτή η δουλειά πραγματοποιείται από το λειτουργικό σύστημα.
  • Οι μεταβλητές της στοίβας υπάρχουν μόνο όσο για όσο εκτελείται η συνάρτηση που τις δημιούργησε.

Με τον όρο heap αναφερόμαστε σ’ ένα κομμάτι μνήμης που διαχειρίζεται αποκλειστικά ο προγραμματιστής. Αυτή η περιοχή είναι αρκετά μεγαλύτερη από τη στοίβα. Στη γλώσσα C, για να δεσμεύσουμε ένα τμήμα του σωρού μπορούμε να χρησιμοποιήσουμε τις συναρτήσεις malloc() και calloc(). Αφού δεσμεύσουμε μνήμη μέσα στο heap και τη χρησιμοποιήσουμε για όσο απαιτεί η εφαρμογή μας, πρέπει να θυμηθούμε να την αποδεσμεύσουμε. Κάτι τέτοιο, στη γλώσσα C πάντα, επιτυγχάνεται με τη συνάρτηση free(). Αν παραλείψουμε αυτό το στάδιο, καθώς το πρόγραμμά μας θα συνεχίζει να εκτελείται, η διαθέσιμη μνήμη στο σωρό θα εξαντλείται. Τότε λέμε ότι το πρόγραμμα παρουσιάζει διαρροή μνήμης (memory leak). Σε αντίθεση με τη stack, το heap δεν έχει περιορισμούς στο μέγεθος των μεταβλητών. Η πρόσβαση στο σωρό είναι πιο αργή απ’ ό,τι στη στοίβα, αφού ο προγραμματιστής είναι υποχρεωμένος να χρησιμοποιεί δείκτες για να προσπελάσει τα αποθηκευμένα δεδομένα. Επίσης, σε αντίθεση με τις μεταβλητές που βρίσκονται στη στοίβα, οι μεταβλητές που δημιουργούνται στο heap είναι προσπελάσιμες από όλες τις συναρτήσεις του προγράμματος — είναι καθολικής έκτασης. Συνοψίζοντας, για το σωρό πρέπει να έχουμε υπόψη τα εξής:

  • Η διαχείριση του σωρού αποτελεί ευθύνη του προγραμματιστή.
  • Ο σωρός είναι μεγαλύτερος από τη στοίβα, αλλά ενδέχεται να σπαταλείται άσκοπα, όταν τα τμήματα που δεσμεύονται δεν απελευθερώνονται ποτέ.
  • Οι μεταβλητές που τοποθετούνται στο heap είναι προσβάσιμες απ’ όλο το πρόγραμμα.
  • Σε γενικές γραμμές, δεν υπάρχει όριο στην έκταση του σωρού.

Περισσότερα είδη μνήμης
Το λειτουργικό σύστημα δεν δεσμεύει χώρο μόνο για τη στοίβα και το σωρό. Η έκταση της μνήμης που καταλαμβάνει κάθε εφαρμογή είναι μεγαλύτερη και χωρίζεται σε τμήματα που ονομάζονται segments. Αναλυτικότερα, έχουμε:

  • Code segment. Σε αυτή την περιοχή της μνήμης τοποθετείται ο εκτελέσιμος κώδικας του προγράμματος. Συνήθως, το code segment επιτρέπει μόνο την ανάγνωση (read-only). Είναι γνωστό και σαν text segment.
  • Data segment. Εκεί αποθηκεύονται οι μεταβλητές που έχουν δηλωθεί σαν global και οι οποίες είναι αρχικοποιημένες. Με άλλα λόγια, οι μεταβλητές που δηλώνονται στον κύριο κορμό του προγράμματος (έξω από οποιαδήποτε συνάρτηση), για τις οποίες έχουμε προσδιορίσει μια αρχική τιμή. Γι’ αυτό το λόγο, το συγκεκριμένο τμήμα μνήμης ονομάζεται και initialized data segment.
  • BSS segment. Εδώ τοποθετούνται οι μεταβλητές global, για τις οποίες δεν έχουμε ορίσει αρχική τιμή. Η συγκεκριμένη περιοχή ονομάζεται και uninitialized data segment.

Χειρισμός στην πράξη
Όταν στη C++ χρησιμοποιούμε τον τελεστή new για να δεσμεύσουμε μνήμη ο σχετικός χώρος καταλαμβάνεται από το σωρό (heap segment), που είναι γνωστός και σαν “free store”.

int *ptr = new int;
int *array = new int[10];

Στην πρώτη γραμμή δηλώνουμε έναν δείκτη προς ακεραίους (int) και στη συνέχεια, με τη βοήθεια του new, δεσμεύουμε το χώρο που απαιτεί ένας ακέραιος (4 bytes). Στη δεύτερη γραμμή δηλώνουμε και πάλι έναν δείκτη προς ακεραίους. Αυτή τη φορά όμως δεσμεύουμε χώρο για δέκα ακεραίους (40 bytes). Εδώ αξίζει να σημειώσουμε το εξής: Ο χώρος που δεσμεύει μία εμφάνιση του τελεστή new είναι συνεχόμενος. Αυτό δεν ισχύει για τις ξεχωριστές εμφανίσεις του τελεστή. Στο παράδειγμα που είδαμε προηγουμένως, οι δέκα ακέραιοι θα βρίσκονται σε διαδοχικές θέσεις. Ωστόσο, ο χώρος για τον μεμονωμένο ακέραιο δεν είναι υποχρεωτικό ότι θα συνορεύει με την περιοχή της δεκάδας. Ας αυτό σας μπέρδεψε, σκεφτείτε την εξής (απλούστερη) περίπτωση:

int *ptr1 = new int;
int *ptr2 = new int;

Οι διευθύνσεις μνήμης στις οποίες παραπέμπουν οι δύο δείκτες μπορεί να είναι γειτονικές, αλλά μπορεί και να μην είναι.

Η δραστηριότητα μέσα στη στοίβα είναι αρκετά πιο έντονη απ’ αυτή που υποψιαζόμαστε. Ας δούμε όσα συμβαίνουν στο σύστημα όταν καλείται μια συνάρτηση:

  • Κατασκευάζεται ένα stack frame. Μπορείτε να το φαντάζεστε σαν μια νέα στοίβα, μέσα στη “μεγάλη” στοίβα του προγράμματος. Αρχικά, το stack frame περιλαμβάνει τα εξής:
    • Τη διεύθυνση μνήμης της εντολής, που ακολουθεί την κλήση της συνάρτησης. Ονομάζεται και return address, αφού εκεί θα επιστρέψει η ροή του προγράμματος όταν ολοκληρωθεί η συνάρτηση.
    • Τις τιμές ορισμένων καταχωριστών που ενδέχεται να τροποποιηθούν κατά την εκτέλεση της συνάρτησης και οι οποίοι αργότερα θα πρέπει να επανέλθουν στην αρχική τους κατάσταση.
    • Τις παραμέτρους που μεταφέρονται στη συνάρτηση.
  • Η ροή του προγράμματος μεταφέρεται στο σημείο εκκίνησης της συνάρτησης και η εκτέλεσή της ξεκινά.
  • Κάθε μεταβλητή που δηλώνεται και χρησιμοποιείται χωρίς να έχει δεσμευτεί χώρος χειροκίνητα (από τον προγραμματιστή), τοποθετείται στο stack frame της συνάρτησης.
  • Όταν η συνάρτηση ολοκληρωθεί πραγματοποιούντα τα εξής:
    • Οι τοπικές μεταβλητές (όσες βρίσκονται στο stack frame) καθώς και οι παράμετροι που είχαν μεταφερθεί στη συνάρτηση, καταστρέφονται.
    • Οι καταχωριστές που ενδέχεται να είχαν αποθηκευτεί, ανακτούν τις αρχικές τους τιμές.
    • Η ροή του προγράμματος μεταφέρεται εκεί που υποδεικνύει το return address.

Οι τιμές που επιστρέφει μια συνάρτηση μεταφέρονται με διάφορους τρόπους. Σε ορισμένες περιπτώσεις αποθηκεύονται μέσα στο stack frame, ενώ σε κάποιες άλλες τοποθετούνται σε καταχωριστές του επεξεργαστή. Φυσικά, αυτές οι λεπτομέρειες δεν είναι κρίσιμες. Αρκεί να έχουμε υπόψη το γενικό τρόπο λειτουργίας της στοίβας και να θυμόμαστε ότι εκεί δεν τοποθετούνται μόνο μεταβλητές, αλλά και διευθύνσεις μνήμης που παραπέμπουν σε σημεία του κώδικα (return address).

Υπερχείλιση στοίβας
Όπως αναφέραμε νωρίτερα η στοίβα είναι φραγμένη — δεν μπορεί να εκτείνεται απεριόριστα. Για παράδειγμα, στα Windows το μέγεθος της στοίβας δεν μπορεί να ξεπερνά το 1MB, ενώ σε κάποια συστήματα Unix μπορεί να φτάνει έως τα 8MB. Όταν ένα πρόγραμμα προσπαθεί να τοποθετήσει στη στοίβα περισσότερα δεδομένα, συμβαίνει αυτό που ονομάζουμε υπερχείλιση στοίβας. Στην πράξη, τα πλεονάζοντα δεδομένα καταλήγουν σε περιοχές της μνήμης που προορίζονταν για άλλα, άσχετα δεδομένα. Κάτι τέτοιο, τουλάχιστον όταν συμβαίνει κατά λάθος, έχει απρόβλεπτα αποτελέσματα στη συμπεριφορά ενός προγράμματος και συνήθως οδηγεί στο “κρέμασμα”. Υπερχείλιση στοίβας μπορούμε να προκαλέσουμε τεχνητά, με διάφορους τρόπους.

int main()
{
    int my_array[100000000];
    return 0;
}

Στο παραπάνω παράδειγμα επιχειρούμε να δεσμεύσουμε χώρο για πάρα πολλές μεταβλητές. Είναι πασιφανές ότι η στοίβα δεν μπορεί να χωρέσει όλα αυτά τα δεδομένα και το πρόγραμμα θα σκάσει αμέσως μόλις προσπαθήσουμε να το τρέξουμε. Δείτε κι αυτή την περίπτωση:

void foo() 
{
    foo();
}

int main()
{
    foo();
    return 0;
}

Εδώ η συνάρτηση foo() καλείται αναδρομικά (δηλαδή από τον εαυτό της) χωρίς κάποιον περιορισμό. Ως αποτέλεσμα, θα αρχίσουν να δημιουργούνται αλλεπάλληλα stack frames, το ένα μέσα στο άλλο. Με αυτόν τον τρόπο ο διαθέσιμος χώρος για τη στοίβα θα εξαντληθεί πολύ γρήγορα. Γενικότερα, το παραπάνω πρόγραμμα θα καταναλώσει όλη τη διαθέσιμη μνήμη, μέχρι το πρόγραμμα να τερματιστεί.

Κρίσιμες λεπτομέρειες
Ας επιστρέψουμε στο θέμα μας τώρα. Η glibc, μέσα στη συνάρτηση _nss_dns_gethostbyname4_r() δεσμεύει 2048 bytes για την απάντηση στο εκάστοτε DNS query. Αυτός ο χώρος βρίσκεται μέσα στη στοίβα. Αντιθέτως, στις συναρτήσεις send_dg() και send_vc(), ο χώρος για την απάντηση στο εκάστοτε ερώτημα DNS δεσμεύεται δυναμικά, από το σωρό. Αυτό σημαίνει ότι δεσμεύονται ακριβώς τόσα bytes, όσα χρειάζονται για να χωρέσει η απάντηση. Αν το μέγεθός της υπερβαίνει τα 2048 bytes, θα δεσμευτεί περισσότερος χώρος *χωρίς* να προκύψει κάποιο πρόβλημα. Όλα αυτά μοιάζουν αδιάφορα, μόνο που οι παραπάνω συναρτήσεις χρησιμοποιούνται συνδυασμένα και μάλιστα σε πάρα πολλά προγράμματα. Υπό συγκεκριμένες συνθήκες, λοιπόν, μπορεί να συμβεί το εξής: Ο χώρος που έχει δεσμευτεί στο σωρό να είναι μεγαλύτερος από τον χώρο που έχει δεσμευτεί στη στοίβα. Αντιλαμβάνεστε τι σημαίνει αυτό; Μόλις μεταφερθούν τα δεδομένα από το σωρό στη στοίβα, θα πραγματοποιηθεί μια υπερχείλιση! Τα δεδομένα μετά το 2048 byte θα τοποθετηθούν σε άσχετες περιοχές της στοίβας — ή και εκτός αυτής.

Σενάρια επίθεσης
Η ευπάθεια που εξετάζουμε, θεωρητικά τουλάχιστον, επιτρέπει ακόμα και την εκτέλεση απομακρυσμένου κώδικα. Βέβαια κάνουμε λόγο για έναν θεωρητικό κίνδυνο, αφού στην πράξη είναι εξαιρετικά δύσκολο να συμβεί κάτι τέτοιο. Οι μηχανικοί της Google έχουν κατασκευάσει δύο προγράμματα που επιδεικνύουν την επίθεση μέσω αυτής της ευπάθειας και αποτελούν αυτό που ονομάζουμε “proof of concept”. Πρόκειται για δύο προγράμματα, από τα οποία το ένα λειτουργεί σαν client και το άλλο σαν ένας υποτυπώδης name server. Το δεύτερο εκμεταλλεύεται την ευπάθεια, ενώ το πρώτο αποτελεί το θύμα. Με τη βοήθειά τους μπορούμε να τσεκάρουμε αν το σύστημά μας αντιμετωπίζει το –θεωρητικό τουλάχιστον– κίνδυνο. Μπορείτε να τα κατεβάσετε από εδώ.

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων