Control Flow Attacks Control Flow Integrity (CFI) Αντρέας Ιωάννου Στο Control flow integrity(cfi) υπάρχουν διάφορα είδη επιθέσεων που έχουν σας απώτερο σκοπό να εκμεταλλευτούν το πρόγραμμα μέσω κάποιου bug και να το κάνουν να εκτελέσει μια μη καθορισμένη απο το δημιουργό ροή. Κάποιες επιθέσεις είναι: Code injection: ο attacker με ένα δικό του input, εκμεταλλέυεται ενα bug ενός προγράμματος και τροποποιά κάποια control data (π.χ function addresses, v table pointers) με σκοπό να αποκτήσει τον έλεγχο του προγράμματος και να βάλει και να εκτελέσει τον δικό του κώδικα(shellcode) για να καλέσει ένα system call η να κάνει κάποια κακά πράγματα. Code reuse: Ο attacker χρησιμοποιά ήδη υπάρχοντα κομμάτια κώδικα (συνήθως απο libraries) τα οποία λέγονται gadgets, στην συνέχεια τα συνδέει και έχει σαν σκοπό πάλι να αποκτήσει τον έλεγχο του προγράμματος και να το κάνει να κάνει πράγματα που θέλει. Control Flow Defences Για να αποτρέψουμε τις πιο πάνω επιθέσεις υπάρχουν κάποιες άμυνες, όπως: Nx bit: Μια τεχνική όπου χρησιμοποιείτε στα CPUs για να μαρκάρουμε σελίδες της μνήμης σαν μη εκτελέσιμες έτσι ώστε να μην μπορεί ο attacker να βάλει και να εκτελέσει τον κώδικα του. Stack canaries: Μέθοδος όπου βάλει ένα αριθμό, ο οποίος καθορίζεται τυχαία στην αρχή του προγράμματος μέσα στο code memory, για να αποτρέψει τον attacker να κάνει άμεσα inject το κώδικα του, ή και να ανιχνέυσει τυχόν code injection. Address Space Layout Randomization (ASLR) : Μέθοδος όπου κάνουμε τυχαιοποίηση κάποιες διευθύνσεις σελίδων κώδικα (συνήθως απο libraries) έτσι ώστε να δυσκολευτεί ο attacker να βρεί gadgets για να τα χρησιμοποιήσει. Enforce Control Flow Integrity Control flow integrity Τι είναι; To Control Flow Integrity ή CFI είναι μια τεχνική η οποία έχει σαν σκοπό να μας προστατέυσει απο το να εκτελέσει το πρόγραμμα ροές ελέγχου τις οποίες δέν είχε πρόθεση και δεν ήταν καθορισμένες απο τον δημιουργό του για να εκτελεστούν. Αυτο επιτυγχάνεται με τον συνδυασμό στατικής επαλήθευσης και δυναμικών ελέγχων. Για την στατική επαλήθευση πρέπει να βεβαιωθούμε ότι η ροή ελέγχου παραμένει σε ένα γράφο Ροής ελέγχου ο οποίος δημιουργείτε στην μεταγλώττιση, και για τους δυναμικούς ελέγχους κάνουμε ελέγχους για Ids με machine code rewriting.
Στατική επαλήθευση με χρήση γράφου ροής ελέγχου- Control Flow Graph(CFG) Η στατική επαλήθευση του CFI επιτυγχάνεται με το να βεβαιωθούμε ότι η ροή παραμένει μέσα στον γράφο ροής ελέγχου. -Ο Γράφος ροής ελέγχου ή CGF είναι ένας γράφος ο οποίος καθορίζεται με static binary analysis και μας δείχνει όλες τις πιθανές αλλαγές ροής που πρέπει να έχει ενα πρόγραμμα κατα την εκτέλεση του. Κάθε κόμβος είναι ένα block απο εντολές και κάθε ακμή είναι αλλαγή ροής ελέγχου μεταξύ των block(κόμβων), η οποία επιτυγχάνεται με jumps. -Υπάρχουν 2 είδη jumps: 1)Direct Jumps: Είναι γνωστό απο την αρχή του προγράμματος που θα κατευθύνουν την ροή, επομένως δέν χρειάζεται έλεγχος. Παράδειγμα είναι τα branches, ή κάλεσμα συναρτήσεων οι οποίες δεν μπορούν να έχουν διαφορετικούς τύπους παραμέτρων. 2)Indirect Jumps: Δεν είναι γνωστό που θα μεταφερθεί η ροή ελέγχου διότι εξαρτάται απο μια τιμή καταχωρητή ή παράμετρο συνάρτησης που δεν μπορεί να καθοριστεί απο την αρχή του προγράμματος και συνεχώς αλλάζει. Επομένως πρέπει να γίνονται έλεγχοι πριν αυτά τα jumps. Παράδειγμα : κάλεσμα συναρτήσεων με function pointer( sort( a, len, lt), η οποία θα πάρει σαν function pointer το lt, επομένως θα καλέσει την less than συνάρτηση (lt), είτε αν έχουμε το κάλεσμα sort( a,len, gt) η sort θα πάρει σαν function pointer το gt, επομένως θα καλέσει την greater than συνάρτηση (gt) Άρα δέν μπορούμε να γνωρίζουμε απο την αρχή του προγράμματος είτε απο το compile time πιο function pointer θα έχει σαν παράμετρο η συνάρτηση sort[c++] ). Επίσης όταν έχουμε jump σε τιμή καταχωρητή (π.χ jump eax δέν ξέρουμε τη τιμή ή τι διέυθυνση θα έχει ο καταχωρητής διότι αλλάζει συνέχεια κατα την διάρκεια του προγράμματος). -Παράδειγμα CFG Εξήγηση κώδικα: Έχουμε την συνάρτηση sort2 η οποία θα καλέσει την συνάρτηση sort 2 φορές, με 2 διαφορετικούς function pointers, τον lt(less than ) την πρώτη φορά και τον gt(greater than ) την δέυτερη όπου θα ταξινομήσει με βάση φθίνουσα σειρά και με βάση άυξουσα σειρά αντιστοίχως τα στοιχεία του πίνακα α[].
Εξήγηση σχήματος: Οι κόμβοι είναι όλα τα πιθανά labels (block απο εντολές ) και τα βέλη( ακμές) αλλαγές ροής μεταξύ των block(κόμβων). Τα διακεκομμένα βέλη είναι direct calls, τα συμπαγή βέλη είναι indirect calls και τα βέλη απο πάυλες ακμές επιστροφής(return). Άρα βλέπουμε ότι απο την συνάρτηση sort2 θα μεταφερθούμε με direct call στην sort 2 φορές, αφου η sort2 καλεί 2 φορές την sort με 2 διαφορετικούς παραμέτρους (lt και gt) και απο την sort μπορούμε να πάμε με indirect call είτε στην lt είτε στην gt αναλόγως με τον παράμετρο που πήρε σε κάθε κλήση η sort(o οποίος παράμετρος είναι function pointer). Στην συνέχεια βλέπουμε ότι απο όποια συνάρτηση (lt ή gt ) να πάει η ροή μετά απο την sort επιστρέφει στο ίδιο τόπο (label 23) και το ίδιο με την sort και την sort2. Δυναμική επαλήθευση με χρήση machine code rewriting Όπως έχουμε αναφέρει για να επιτυγχάνει δυναμική επαλήθευση, το cfi πρέπει να ελέγχει προορισμούς σε runtime διότι δεν είναι καθορισμένοι απο την αρχή του προγράμματος, αυτο επιτυγχάνεται με το να κάνει machine code rewriting για να ελέγχει προορισμούς στο runtime χρησιμοποιώντας 3 νέες εντολές. -Νέες εντολές : 1)label ID: Εντολή που δηλώνει ότι μπαίνουμε σε ένα καινούργιο block εντολών όπου η μεταφορά σε αυτο το block(label) επιτυγχάνεται με indirect call. 2) call ID, dst: Εντολή όπου μας επιτρέπει να μεταφέρουμε την ροή ελέγχου απο εκέι που ήμασταν στην διέυθυνση που περιέχει ο καταχωρητης προορισμού dst(destination) μόνο άν το label (id) της διέθυνσης προορισμού είναι το ίδιο με το Id. Π.χ : call 1234567,0x00800 επιτρέπεται μόνο άν έχουμε «label 1234567» στην διέυθυνση 0x00800. 3)ret ID: εντολή η οποία μας επιτρέπει να επιστέψουμε στο συγκεκριμένο ID -Παράδειγμα Instrumentation στο CFI:
Η μετάφραση του call[ebx+8] που έχει σαν σκοπό να μεταφέρουμε ροή στο περιεχόμενο του καταχωρητή ebx + 8, είναι ότι θα μεταφέρουμε στον καταχωρητή eax την διέυθυνση που θέλουμε να πάμε (mov,eax,[ebx+8] ), έπειτα θα ελέξουμε άν το label( id) αυτής της διέυθυνσης (νέο block εντολών) είναι valid και συγκεκριμένα ίσο με το ID 12345678h(για το παράδειγμα cmp [eax+4],12345678h) και άν είναι ίσα τότε θα μεταφέρουμε την ροή στην διέυθυνση που θέλουμε (call eax), ειδαλλιώς θα μεταφερθούμε σε ένα error label(jne error_label) που θα ειδοποιήσουμε ότι έχουμε αλλοίωση ροής ή όχι valid προορισμό και τότε πιθανό το πρόγραμμα να κάνει crush). Παρόμοια επιτυγχάνεται και το return. Προϋποθέσεις κάτω απο τις οποίες μπορεί να επιτευχθεί το CFI Για να μπορεί να επιτευχθεί το cfi πρέπει να τηρούνται κάποιες προϋποθέσεις : 1) Unique IDs :Τα μοτίβα των bit που επιλέγονται για IDs δέν πρέπει αν υπάρχουν πουθένα μέσα στο code memory παρα μόνο σε IDs και IDs-checks. Αυτο επιτυγχάνεται με το να κάνουμε το space των IDs πολύ μεγάλο και να επιλέγουμε IDs που να μήν έχουν σύγκρουση με τα opcodes του λογισμικού. 2) Non executable data(nxd): Δεν πρέπει να γίνεται ο attacker να εκτελέσει τον κώδικα του επομένως πρέπει να ισχύει το Nx bit 3) Non-writable Code(NWC): Δεν πρέπει να επιτρέπετε γράψιμο πάνω σε κώδικα διότι ο attacker μπορεί να κάνει έυκολα inject τον κώδικα του είτε να αλλάξει τιμές IDs και να μεταφέρει την ροή σε δικό του κώδικα με έγκυρα ID. Προκλήσεις του CFI Οι προκλήσεις και αδυναμίες που αντιμετωπίζει το cfi είναι: 1) Χρειάζεται ένα πλήρες και ακριβές CFG το οποίο είναι σχεδόν αδύνατο να έχουμε, λόγω των πολλών μεταφορών που μπορουν να υπάρχουν για indirect jumps at runtime. 2) Υπάρχει ένα μή-αμελητέο κόστος απόδοσης το οποίο προέρχεται απο τους ελέγχους που κάνει το cfi πρίν τα indirect jumps για να ελέγξει αν είναι έγκυρα τα Ids των προορισμών Βελτιώσεις για τις προκλήσεις -Κάποιες βελτιώσεις που μπορούν να γίνουν είναι να 1)μειώσουμε το αριθμό των Ids έτσι ώστε να γίνονται πιο λίγοι έλεγχοι για βελτίωση απόδοσης 2)να έχουμε ένα πιο επιτρεπτό(χαλαρό CGF) για να έχουμε παραπάνω επιλογές για μεταφορές το οποίο επιτυγχάνεται με το να συγχωνέυσουμε IDs. -Παράδειγμα μείωσης αριθμού Ids:
Στο πιό πάνω σχήμα βλέπουμε ότι η συνάρτηση sort καλείτε απο την συνάρτηση sort1 και sort2 και στην συνέχεια αναλόγως του καλέσματος της η sort θα καλέσει με indirect call την less_than ή την greater_than. Αυτό που μπορεί να γίνει για να βελτιωθεί η απόδοση και να μειωθεί το κόστος ελέγχων είναι για παράδειγμα στο σημείο της sort όπου υπάρχει «check ID41 ID51» να έχουμε ένα κοινό Id(π.χ ID 40) και να έχουμε αντιστοίχως στην less_than και greater_than ID40 στην αρχή του block, διότι ξέρουμε ότι πρέπει να καλέσει είτε την μία είτε την άλλη. Με αυτό γλιτώνουμε για παράδειγμα 1 έλεγχο που θα γινόταν για ελέγξουμε τα 2 διαφορετικά IDs των συναρτήσεων,και αυτο θα μπορούσαμε να το κάνουμε επίσης στο σημείο της sort «check ID11 ID21» να γίνει «check ID 20». -Παράδειγμα συγχώνευσης IDs
Στο πιό πάνω σχήμα βλέπουμε ότι συγχωνέυσαμε όλα τα Ids με αποτέλεσμα να έχουμε μόνο 2 διαφορετικά IDs(ID1,ID2). Αυτό μας επιτρέπει να έχουμε ένα πιο χαλαρό και επιτρεπτό γράφο για να έχουμε πιο ολοκληρωμένο γράφο. Μορφές CFI Στον πιο κάτω πίνακα φαίνονται κάποιες μορφές cfi με βάση του αριθμού των IDs που χρησιμοποιούν. Αδυναμίες CFI Οι αδυναμίες γενικά του cfi είναι ότι με το να προσπαθήσουμε να μειώσουμε το κόστος των ελέγχων που γίνονται πριν απο κάθε indirect jump, μειώνουμε την ακρίβεια και την ασφάλεια του συστήματος.
Επομένως έχουμε το trade off μεταξύ ασφάλειας και απόδοσης, αφου όσο πιο πολλούς ελέγχους έχουμε τόσο πιο πολύ ασφάλεια έχουμε, αλλα είναι πιο χαμηλή η απόδοση, ενώ όσο πιο λίγους ελέγχους έχουμε τοσο πιο αποδοτικό είναι το σύστημα, αλλα μειώνεται η ασφάλεια του. Πετυχημένη επίθεση σε σύστημα που υποστήριζε CFI Σε αυτήν την ενότητα θα δούμε μια πετυχημένη επίθεση η οποία έγινε εκμεταλλέυοντας το Internet Explorer 8 σε Windows 7 σε ένα σύστημα το οποίο υποστηριζόταν απο μια πρόσφατη μορφή cfi, ASLR και DEP(NX bit). CCFIR Το Compact Control Flow Integrity and Randomization(CCFIR) είναι ένας πρόσφατος σχεδιασμός του παραδοσιακού cfi όπου έχει πιο αυστηρούς κανόνες απο τα χαλαρά cfi, υποστηρίζει 3 Ids και ένα ακόμη επίπεδο τυχαιοποίησης Springboard Section). -Springboard Section Το springboard section είναι μια νέα ενότητα κώδικα για κάθε τμήμα του συστήματος, όπου όλα τα indirect jumps, calls και returns περνούν μέσω αυτού. Στο πιό πάνω σχήμα έχουμε ένα παράδειγμα με springboard section. Έστω ότι βρισκόμαστε στο σημείο 1 του προγράμματος και μεταφερόμαστε με direct call στο σημείο 2. Στην συνέχεια θέλουμε να μεταφερθούμε με indirect jump(καλώντας τον eax) στο σημείο 5. Αυτό επιτυγχάνεται και μεταφράζεται σέ ένα σύστημα που περιέχει CFFIR στο δεξιό σχημα, όπου απο το σημέιο 2 θα
καλέσουμε το αντίστοιχο σημέιο του 2 στο springboard με direct call, στην συνέχεια θα μεταφερθούμε στο αντίστοιχο σημείο του 5 μέσα στο springboard με indirect call πρωτού μεταφερθούμε στο 5, και στην συνέχεια θα μεταφέρουμε με direct call την ροή στο σημείο 5. Με αυτό επιτυγχάνεται ένα επίπεδο παραπάνω ασφάλειας και τυχαιοποίησης διότι ο attacker θα πρέπει να βρεί και τις διευθύνσεις των αντίστοιχων σημείων μέσα στο springboard section για κάθε κομμάτι του κώδικα για να επιτραπέι το indirect jump που θα επιχειρήσει. Είδη gadgets που αφορούν την επίθεση Η πετυχημένη επίθεση χρησιμοποίησε 2 νέα είδη gadgets. 1) Call-site(CS) gadgets: Αυτό το είδος gadget είναι block απο εντολές οι οποίες είναι μετά απο εντολή call και τερματίζονται με return. Αυτά τα gadgets μπορούν να υπάρχουν μέσα στη μέση κώδικα η συνάρτησης. 2) Entry-Point (EP) gadgets: Αυτό το είδος gadget είναι block απο εντολές οι οποίες ξεκινούν απο την αρχή μίας συνάρτησης και τερματίζονται απο indirect call ή jump. Επομένως με την χρησιμοποίηση και την ένωση αυτών των 2 ειδών gadgets οι attackers έχουν περισσότερη ευκαμψία για να χρησιμοποιήσουν μικρά κομμάτια κώδικα και να κάμουν πιο έυκολο το έργο τους. Η Eπίθεση -Η πετυχημένη επίθεση είναι μια διαδικασία 2 σταδίων: 1)Θα επιχειρήσουμε πρώτα να μάθουμε κάποιες πληροφορίες για το σχήμα του target προγράμματος, όπως τις διευθύνσεις των gadgets που βρίσκονται μέσα στα libraries που θα χρειαστούμε, το οποίο επιτυγχάνεται με memory disclosure bug(αποκάλυψη μνήμης) 2)Θα χρησιμοποιήσουμε αυτές τις πληροφορίες που έχουμε για να βάλουμε σωστά τα gadgets μας με την σειρά που πρέπει στο payload.
-Στο δέυτερο στάδιο έχουμε σαν απώτερο σκοπό να κάνουμε inject τον κώδικα μας, επομένως θα χρειαστούμε μια διαδικασία όπου θα πρέπει να πάμε απο το code reuse, στο code injection. Για να πετύχουμε αυτό το σκοπό θα πρέπει γενικά με το code reuse payload να κάνουμε code Injection κάνοντας bypass τα WX (non-writable, non-executable) semantics. Τα βήματα που θα ακολουθήσουμε για να πετύχουμε να κάνουμε bypass το WX semantics είναι: 1) Συνδέουμε τα gadgets έτσι ώστε να καλέσουμε ενα API, ή system call του λειτουργικού συστήματος(π.χ VirtualProtect για Windows) με το οποίο μπορούμε να αλλάξουμε τα permissions σελίδων μνήμης 2)Με βάση αυτό το system call να τροποποιήσουμε το non executable bit κάποιων σελίδων μνήμης, και να τις κάνουμε executable 3) Να κάνουμε overwrite αυτες τις σελίδες με τον κώδικα μας (shellcode) 4) Τέλος να μεταφέρουμε ξανά ροή σε αυτές τις σελίδες για να εκτελεστεί ο κώδικας μας(shellcode) -Η πετυχημένη επίθεση ήταν ένα heap overflow στο internet explorer το οποίο έδινε έλεγχο σε indirect jump. Η αστάθεια(vulnerability) γινόταν triggered με το να έχουμε πρόσβαση στα span και width attributes μιας στήλης ενός HTML table μέσω JavaScript. Μετά κάνουμε overwrite ένα virtual function table (VFT) pointer σε ένα button object, που τελικά θα μας οδηγήσει στο να έχουμε έλεγχο πάνω στο target μιας indirect jump εντολής. Έπειτα χρησιμοποιούμε το ίδιο overflow για να κάνουμε overwrite το string object s size. Επίσης έχουμε αναφορά σε αυτό το object, έτσι μπορούμε να χειραγωγήσουμε το string s size για να επιτύχουμε memory disclosure attack, δηλαδή να αποκαλύπτουμε πληροφορίες μνήμης (όπως gadgets addresses) όποτε θέλουμε. -Διαδικασία επίθεσης A.Exploiation Requirements Για να επιτύχει η επίθεση έπρεπε να κάνουμε «heap spraying» και «heap feng shui», δηλαδή με λίγα λόγια να βάλουμε με την σωστή σειρά τα vulnerable buffer, string object, and the button object έτσι ώστε όταν το vulnerable buffer is overflowed την πρώτη φορά, τότε το string object s size property θα γίνει overwritten για να κτίσουμε το memory disclosure interface. Όταν τότε το vulnerable buffer γίνει overflowed την δέυτερη φορά, το button object s VFT pointer θα γίνει overwrite. Στην συνέχεια κάνουμε spray ( πολλά copies ) τον κώδικα μας στην μνήμη έτσι ώστε μια φάση που θα δείξει σε λάθος τόπο στην μνήμη να κτυπήσει τόπο που έχουμε τον κώδικα για να ξεκινήσει η διαδικασία του να κάνουμε guide το gadget-chaining process απο την αρχική indirect transfer εντολή σε code injection. Β.Αποκάλυψη μνήμης. Το σύστημα υποστηρίζεται απο ASLR και εποιδή είναι CCFIR υπάρχει και το πρόβλημα του springboard section επομένως πρέπει να κάνουμε την διαδικασία του memory
dislcosure(αποκάλυψη μνήμης) 2 φορές, μία να βρούμε τα base addresess των gadgets που θέλουμε να χρησιμοποιείσουμε τα οποία βρίσκονται στα libraries(dlls) και «προστατέυονται» απο ASLR και, μετά να μπούμε μέσα στο springboard section για να βρούμε τις πραγματικές διευθύνσεις για function calls και return stubs που αντιστοιχούν στα gadgets μας. H αστάθεια που χρησιμοποιούμε μας επιτρέπει να κάνουμε leak memory με το να κάνουμε overwrite το size field of a string object και να διαβάσουμε τιμές πέρα απο τα αρχικά όρια (boundary) ενός object. C.Gadget chaining Αυτή η φάση η οποία είναι και η τελική της επίθεσης αποτελείτε απο 3 άλλες φάσεις: 1)Μετατρέπουμε το indirect jump που ελέγξαμε σε return instruction. 2) Κάνουμε stack-pivoting. Δημιουργούμε μια ψέυτικη στοίβα μέσα στο heap όπου βάλουμε τα gadget μας με την σωστή σειρά και τα κάνουμε chain, και κάνουμε τον αρχικό esp(stack pointer) να δείχνει στο heap(νέα ψέυτικη στοίβα) κάνοντας το σύστημα να νομίζει ότι αυτή είναι τώρα η στοίβα του προγράμματος. 3)Αλλάζουμε τα memory permissions κάποιων σελίδων μνήμης μέσω καλέσματος system calls(π.χ VirtualProtect για Windows ) έτσι ώστε να κάνουμε executable αυτές τις σελίδες,να κάνουμε inject τον κώδικα μας(shellcode) και στην συνέχεια να μεταφέρουμε ροή σε αυτές τις σελίδες για να τον εκτελέσουμε. Πιθανές άμυνες που μπορούν να υπάρξουν για να αποφευχθούν τέτοιες επιθέσεις και να κάνουν το CFI πιο δυνατό 1) Control-Flow Locking Αυτή η τεχνική χρησιμοποιά locks για να διατηρήσει το integrity του CFG. Υλοποία μια λειτουργία lock πρίν απο indirect control-flow transfers και μια λειτουργία unlock πρίν απο valid targets. 2) Shadow call stack Σύνοψη Το shadow call stack διατηρείτε με ασφάλεια κατά το runtime για να κάνει πιο δυνατό το cfi.to shadow stack κρατά ένα αντίγραφο των return addresses που αποθηκέυονται στη στοίβα του προγράμματος με ένα τρόπο όπου ένας υποψήφιος attacker να μήν μπορεί να κάνει overwrite. Το CFI είναι μια δυνατή και καλά υποσχόμενη άμυνα η οποία όμως όπως έχουμε δεί και με την πετυχημένη επίθεση χρειάζεται περισσότερη δουλεία και μελέτη για να γίνει πιο δυνατή και να αποτρέπει όλες τις επιθέσεις.