Δημοσιεύτηκε: 06 Σεπ 2011, 03:49
από migf1
Το buffer-overflow ως έννοια δεν είναι δύσκολο στην κατανόησή του, το να το βρει και να το κάνει exploit όμως κανείς είναι.

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

Αν λοιπόν έχουμε ορίσει έναν πίνακα να καταλαμβάνει π.χ. 10 bytes στη μνήμη...
Κώδικας: Επιλογή όλων
    char string[10] = { 0 };

τότε αν έστω και κατά λάθος αρχίζουμε να γράφουμε δεδομένα μετά το τέλος του...
Κώδικας: Επιλογή όλων
    strcpy(string, "this is a string with a lot more than 10 characters");

τότε δημιουργούμε buffer-overflow!

Τα έξτρα αυτά δεδομένα πάνε και αντικαθιστούν μερικώς ή ολικώς δεδομένα που βρίσκονται δίπλα από το buffer μας και προφανώς ανήκουν σε άλλες μεταβλητές του προγράμματος μας, ή σε άλλα τμήματά του, ανάλογα με το αν ο υπερχειλισμένος πίνακας είχε οριστεί στατικά (άρα βρίσκεται στο stack) ή δυναμικά (άρα βρίσκεται στο heap).

Είτε έτσι, είτε αλλιώς, όταν το πρόγραμμα πάει να διαχειριστεί το συγκεκριμένο τμήμα της μνήμης βρίσκει "άλλα αντί άλλων" κι ενίοτε παραπέμπεται σε άλλα τμήματα μνήμης, στα οποία όμως δεν του επιτρέπεται η πρόσβαση. Οπότε το λειτουργικό εγείρει exception και στέλνει το αντίστοιχο σήμα στη διεργασία που το προκάλεσε (σε POSIX περιβάλλοντα το signal του segmentation-fault είναι το SIGSEGV).

Άλλη περίπτωση είναι τα έξτρα δεδομένα να είναι τόσα πολλά, ώστε να φτάνουν απευθείας σε read-only τμήματα της μνήμης, οπότε εκεί έχουμε άμεση δημιουργία του exception και αποστολή του σήματος.

Υπάρχουν αρκετοί τρόποι να εκμεταλλευτεί κανείς τα κενά ασφαλείας που δημιουργεί το buffer-overflow, και ποικίλουν από ενσωμάτωση κακόβουλου κώδικα (ή παραπομπής σε κακόβουλο κώδικα) σε μεταβλητές που βρίσκονται δίπλα στο υπερχειλισμένο buffer, μέχρι παρεμβολή, παραπομπή ή και αντικατάσταση της σελίδας που εμπλέκεται στη δημιουργία του exception σε λειτουργικά που χρησιμοποιούν σελιδοποιημένη εικονική μνήμη (τα περισσότερα).

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

Προγραμματιστικά, οι πιο επιρρεπείς γλώσσες για δημιουργία buffer-overflow είναι οι C και C++ (που μαζί με τη Java είναι οι 3 πιο δημοφιλείς γλώσσες) επειδή αφήνουν τον προγραμματιστή να έχει πρόσβαση σε οποιοδήποτε τμήμα της μνήμης, χωρίς να παρέχουν εξαναγκαστικά μηχανισμούς ελέγχου για την εγκυρότητα αυτής της πρόσβασης. Υπάρχουν όμως εργαλεία που βοηθάνε στην καταπολέμηση του φαινομένου (η C++ παρέχει αρκετά, αρκεί να ξέρει κάποιος να τα χρησιμοποιήσει, ενώ για τη C είναι εξωτερικά προγράμματα που κάνουν συνεχώς monitoring για τις πιο κοινές περιπτώσεις). Αντίθετα, γλώσσες όπως η Java, η Python και η συντριπτική πλειοψηφία των interpreted γλωσσών παρέχουν boundary checking (να μην ξεπερνιούνται δηλαδή τα όρια των πινάκων) είτε στο run-time, είτε στο compilation.

Επίσης οι περισσότερες από αυτές τις γλώσσες στερούνται πολλών low level δυνατοτήτων στον βωμό αυτής της προστασίας, με χαρακτηριστικό παράδειγμα την έλλειψη δεικτών και αριθμητικής δεικτών.

Στα χέρια μη έμπειρων προγραμματιστών και σε ελαστικές γλώσσες (όπως η C και η C++), οι δείκτες είναι μια ακόμα πηγή παραγωγής segmentation faults, που όπως είπαμε δημιουργεί κενά ασφαλείας.

Πιο συγκεκριμένα, exception που εγείρονται όταν πάμε να χρησιμοποιούμε μη-αρχικοποιημένους δείκτες, ή δείκτες που είναι NULL ή δείκτες που έχουν γίνει free. Για αυτό πρέπει να είναι κανείς πολύ προσεχτικός όταν γράφει κώδικα με δείκτες.

Μερικές πρακτικές που μειώνουν το πρόβλημα (αλλά δεν το λύνουν από μόνες τους) είναι:

  • να αρχικοποιούμε πάντα τους δείκτες όταν τους ορίζουμε, ιδανικά με απευθείας δέσμευση της μνήμης που θα χρησιμοποιήσουν αν προορίζονται για δυναμική διαχείριση μνήμης, ή σε NULL αν πρόκειται για απλούς δείκτες...
    Κώδικας: Επιλογή όλων
        char *p1 = calloc(10, sizeof(char) );
        char *p2 = NULL;

  • να ελέγχουμε να μην είναι NULL ο δείκτης πριν τον χρησιμοποιήσουμε, ΕΙΔΙΚΑ όταν περνιέται ως όρισμα σε συνάρτηση...
    Κώδικας: Επιλογή όλων
    // ---------------------------------------------------
    char *string_ncopy( char *dst, const char *src, size_t n )
    {
          if ( !dst )
                return NULL;
          if ( !src )
                return dst;

          strncpy( dst, src, n );
          return dst;
    }

  • να ελέγχουμε να μην είναι NULL ο δείκτης πριν τον απελευθερώσουμε, κι αμέσως μετά να τον κάνουμε NULL...
    Κώδικας: Επιλογή όλων
        if ( p1 ) {
            free( p1 );
            p1 = NULL;
        }

        if ( p2 ) {
            free( p2 );
            p2 = NULL;
        }
Μερικά από τα παραπάνω γίνονται αυτόματα σε compilers που υποστηρίζουν την αναθεώρηση C99 της C (π.χ. o έλεγχος πριν την απελευθέρωση, ή ακόμα και η ίδια η απελευθέρωση πριν τον τερματισμό του προγράμματος) αλλά αφενός δεν υπάρχει ακόμα compiler που να υποστηρίζει πλήρως όλα τα έξτρα στάνταρ που θέσπισε η αναθεώρηση C99 κι αφετέρου δεν υποστηρίζουν όλοι οι compilers τα ίδια έξτρα στάνταρ.

Οπότε το καλύτερα να τα κάνουμε μόνοι μας, για να έχουμε το κεφάλι μας ήσυχο :)

@Star_Light: btw, αυτοί είναι οι έλεγχοι για τους οποίους ήθελα να σας μιλήσω στο τόπικ της C ;)