💾 Archived View for magaz.hellug.gr › 34 › 05_rce3 › index.gmi captured on 2024-02-05 at 09:24:30. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Reverse Engineering σε περιβάλλον Linux, Μέρος 2

Φραντζής Αλέξανδρος (aka Alf) alf82 at freemail dot gr
Σεπ 2003

To άρθρο αυτό είναι το τρίτο της σειράς "Reverse Engineering σε περιβάλλον Linux". Σκοπός της σειράς είναι να εξοικοιώσει τους αναγνώστες με τις βασικές τεχνικές του Reverse Enineering, με έμφαση στο πως αυτές μπορούν να εφαρμοστούν στο Linux, και να τους προσφέρει πιο βαθιές γνώσεις για τη λειτουργία του συστήματος τους. Στο συγκεκριμένο άρθρο θα ασχοληθούμε κυρίως με τη διαδικασία της εκτέλεσης των προγραμμάτων και τις πληροφορίες που μπορούμε να αποκομίσουμε από αυτά.

1. Εισαγωγή

2. Διαδικασία Δημιουργίας και Φόρτωσης Εκτελέσιμου

3. Το ELF

4. Το /proc filesystem

5. Χρήσιμες έως πολύ χρήσιμες πληροφορίες ΙΙ

6. Hands-on Παράδειγμα - Υπό πίεση

7. Πρόκληση

[1. Εισαγωγή]

Καλωσήρθατε στο τρίτο άρθρο (η μέτρηση αρχίζει από το 0) για Reverse Code Engineering σε Linux!

Στο μέρος 2 θα ασχοληθούμε με το πως λειτουργούν τα εκτελέσιμα αρχεία γενικά, ενώ στο 3ο μέρος θα μιλήσουμε συγκεκριμένα για τα ELF αρχεία. Για την καλύτερη κατανόηση των προηγούμενων σας προτείνω να δημιουργήσετε δικά σας μικρά προγράμματα και να δείτε πως ο compiler και ο linker τα μεταμορφώνουν στο τελικό εκτελέσιμο.

Στο μέρος 4 θα ρίξουμε μια σύντομη ματιά στα άδυτα του /proc filesystem και στο 5 θα αναφερθούμε σε δύο πολύ σημαντικά θέματα: το dead-listing και τα packed εκτελέσιμα.

Το μέρος 6 περιέχει το hands on παράδειγμα αυτού του μήνα, όπου θα συναντήσουμε τη Σταχτοπούτα και τη C++ σε όλο της το μεγαλείο :) Εδώ θα εφαρμόσουμε πολλά από όσα ειπώθηκαν στα προηγούμενα μέρη.

Τέλος, όπως πάντα, υπάρχει η λύση του προηγούμενης πρόκλησης, μαζί με το hall of fame και μια καινούργια (και αρκετά διαφορετική από τις προηγούμενες) πρόκληση!

Να σημειωθεί πως όλα τα εκτελέσιμα έχουν δημιουργηθεί με gcc 3.2.2 και επομένως μπορεί να υπάρχουν προβλήματα αν εκτελεστούν σε σύστημα με παλιά έκδοση του gcc (πχ 2.95). Ειδικά στην περίπτωση των C++ προγραμμάτων αυτό είναι σίγουρο, διότι έχει αλλάξει ριζικά το Application Binary Interface (ABI).

Μη διστάσετε να επικοινωνήσετε μαζί μου για οποιαδήποτε διόρθωση, διευκρίνηση ή σχόλιο.

Επίσης είμαι ανοιχτός σε ιδέες για το τι θα θέλατε να περιέχουν τα επόμενα άρθρα (αν υπάρξουν, βέβαια).

[2. Διαδικασία Δημιουργίας και Φόρτωσης Εκτελέσιμου]

[2.1 Εισαγωγή]

Η δημιουργία ενός εκτελέσιμου είναι μια από τις πιο βασικές διαδικασίες σε οποιοδήποτε υπολογιστικό σύστημα. Την εποχή του 1950-1960 τα πράγματα ήταν σχετικά "απλά". Ο προγραμματιστής έγραφε τον αλγόριθμο σε μνημονική γλώσσα assembly και τον μετέφραζε με το χέρι σε γλώσσα μηχανής. Ύστερα τον περνούσε με κάποιο τρόπο (βύσματα, διάτρητες κάρτες) στο σύστημα και προσευχόταν όλα να πάνε καλά!

Η πρώτη προσπάθεια αυτοματοποίησης ήρθε με την δημιουργία των assemblers. Τώρα πια ο ίδιος ο υπολογιστής έκανε την κουραστική δουλειά της μετάφρασης από assembly σε γλώσσα μηχανής. Οι (τεμπέληδες :) )προγραμματιστές, όμως, δεν αρκέστηκαν σε αυτό. Ανέπτυξαν γλώσσες υψηλού επιπέδου και δημιούργησαν compilers οι οποίοι τις μετέφραζαν σε γλώσσα assembly. Οι assemblers που ήδη υπήρχαν ολοκλήρωναν τη διαδικασία αλλά τα πράγματα δε σταμάτησαν ούτε εδώ! Ακολούθησε η χρυσή εποχή του δομημένου προγραμματισμού και των modules. Αποφασίστηκε ότι ήταν σοφό να επαναχρησιμοποιείται ο κώδικας που υπήρχε ήδη και έτσι έπρεπε να βρεθεί ένας τρόπος να μπορούν να συνενώνονται κομμάτια κώδικα (σε δυαδική μορφή) που βρίσκονταν σε διαφορετικά αρχεία.

[2.2 Η πορεία ενός bit: Από την πηγαίο κώδικα στο τελικό εκτελέσιμο]

Υπάρχουν τρία βασικά είδη object αρχείων:

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

[IMG]

Το παραπάνω πρόγραμμα αποτελείται από δύο modules (Relocatable Object File 1 και 2). Επιπλέον, χρησιμοποιεί δύο "βιβλιοθήκες" (Shared Object File 1 και 2). Η πρώτη συνδέεται στατικά στο πρόγραμμα μας, δηλαδή ο κώδικας της συγχωνεύεται στο τελικό object αρχείο. Η δεύτερη συνδέεται δυναμικά. Στην περίπτωση αυτή, στο στάδιο του linking δε γίνεται συγχώνευση κώδικα, αλλά εισάγονται πληροφορίες ώστε όταν φορτωθεί το πρόγραμμα ο dynamic linker να μπορέσει να βρει τις διευθύνσεις των συναρτήσεων και των δεδομένων.

[2.3 Το φόρτωμα του προγράμματος]

Όταν ζητάμε από το λειτουργικό να εκτελέσει ένα πρόγραμμα, γίνονται πολλά περισσότερα από όσα φαίνονται εκ πρώτης όψεως. Σε γενικές γραμμές ακολουθούνται τα εξής βήματα (για τα ELF εκτελέσιμα τα πράγματα διαφέρουν λίγο):

1. Αρχικά το λειτουργικό διαβάζει τον header του εκτελέσιμου για να πάρει απαραίτητες πληροφορίες, όπως:

* Αν όντως πρόκειται για εκτελέσιμο που μπορεί να τρέξει στον υπολογιστή.

* Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα (segment) του εκτελέσιμου ( πχ read-only, executable κτλ).

* Ποια shared objects απαιτεί το εκτελέσιμο.

2. Το λειτουργικό αποδίδει στη διεργασία τη μνήμη που χρειάζεται και φορτώνει τα διάφορα τμήματα στη μνήμη.

3. O dynamic linker φορτώνει στο address space της διεργασίας τις βιβλιοθήκες που χρειάζεται.

4. Γίνεται relocation στο εκτελέσιμο και τις βιβλιοθήκες. Ως μέρος της διαδικασίας του relocation, διορθώνονται οι αναφορές σε συναρτήσεις/δεδομένα των βιβλιοθηκών που φορτώθηκαν. Αυτό είναι το θέμα του επόμενου τμήματος.

5. Τέλος, ο έλεγχος μεταφέρεται στο entry point του προγράμματος. Αυτό αποτελεί τη διεύθυνση της πρώτης εντολής που πρόκειται να εκτελεστεί.

[2.4 Relocation - Εναλλακτικοί τρόποι για τη διόρθωση των αναφορών στο dynamic linking.]

Το relocation (επανατοποθέτηση) είναι η διαδικασία κατά την οποία γίνονται διορθώσεις στην εικόνα ενός προγράμματος επειδή αυτή μπορεί να τοποθετηθεί στη μνήμη σε κάποιο αυθαίρετο σημείο. Αυτό συμβαίνει πάντα για τα shared objects ενώ για τα εκτελέσιμα γίνεται σε πολύ μικρότερο βαθμό. Λόγω της εικονικής μνήμης, μπορούμε να φορτώνουμε το εκτελέσιμο πάντα στο ίδιο σημείο. Για τα shared objects, από την άλλη, δε γίνεται να υποθέσουμε πως δε θα υπάρχει σύγκρουση, διότι πρέπει να μπορούν να συνυπάρχουν με οποιοδήποτε άλλο shared object.

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

H χρήση των shared objects για dynamic linking προσφέρει πολλά πλεονεκτήματα σε σχέση με το static linking. Μερικά από αυτά είναι η μείωση του μεγέθους των εκτελέσιμων, οι αυξημένες δυνατότητες για επαναχρησιμοποίηση κώδικα και η δυνατότητα επέκτασης των εφαρμογών (πχ plug-ins). Όλα αυτά όμως έρχονται με ένα (μικρό, ομολογουμένως) τίμημα. Επειδή οι βιβλιοθήκες φορτώνονται σε κάποιο αυθαίρετο σημείο της εικονικής μνήμης της διεργασίας, οι διευθύνσεις των συναρτήσεων/δεδομένων τους δεν είναι γνωστές από πριν. Έτσι οι κλήσεις/αναφορές σε αυτά δεν είναι ολοκληρωμένες στο εκτελέσιμο.

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

1.

call 0x???????? : Για τη διόρθωση, πρέπει σε κάθε κλήση της συνάρτησης ο dynamic linker να αλλάξει την τιμή στην πραγματική διεύθυνση της συνάρτησης. πχ call 0x40001000. Το πρόβλημα είναι πως αν η συνάρτηση καλείται σε 100 διαφορετικά σημεία, πρέπει καταρχάς το εκτελέσιμο να περιέχει πληροφορίες για όλα αυτά τα σημεία και ο dynamic linker είναι αναγκασμένος να κάνει 100 διορθώσεις.

[IMG]

2.

call [func1_offset] : στη διεύθυνση μνήμης func1_offset (που είναι καθορισμένη από πριν) ο dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Όλες οι κλήσεις προς αυτή τη συνάρτηση διαβάζουν τη διεύθυνση από τη συγκεκριμένη θέση μνήμης και την καλούν έμμεσα (indirect call). Έτσι αποφεύγονται οι πολλαπλές διορθώσεις, με ένα μικρό κόστος στην ταχύτητα εκτέλεσης. Τα PE (Portable Executable) format που χρησιμοποιούν τα MS Windows στηρίζεται σε αυτό το μοντέλο.

[IMG]

3.

call jmp_func : στη διεύθυνση jmp_func βρίσκεται μια εντολή jmp [func_offset]. Στη διεύθυνση func_offset ο dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Γιατί όλη αυτή η ταλαιπωρία; Ο μηχανισμός αυτός προσφ��ρει τη δυνατότητα οι διορθώσεις να γίνονται κατά τη διάρκεια της εκτέλεσης του προγράμματος, όταν υπάρχει ανάγκη, και όχι απαραίτητα όλες μαζί κατά τη φόρτωση του προγράμματος. Το ELF χρησιμοποιεί μια παραλλαγή του μοντέλου αυτού και θα το εξετάσουμε αναλυτικότερα παρακάτω.

[IMG]

[3. Το ELF]

[3.1 Τι είναι το ELF;]

Το Εxecutable and Linking Format (ELF) αποτελεί το format που χρησιμοποιεί το linux για τα object αρχεία του. Υποστηρίζει μια πληθώρα αρχιτεκτονικών και για αυτό είναι ιδανικό για ένα multi-platform λειτουργικό όπως το linux. Παρακάτω θα γίνει μια περιγραφή των βασικών στοιχείων του ELF. Για μια πιο αναλυτική περιγραφή ανατρέξτε στο standard (pdf): ELF 1.1[1], ELF 1.2[2]

1: http://www.nondot.org/sabre/os/files/Executables/ELF.pdf

2: http://x86.ddj.com/ftp/manuals/tools/elf.pdf

Αυτό το ELF δεν έχει καμία σχέση με τον Tolkien :)

[3.2 Βασική δομή του ELF]

Τα βασικά συστατικά που απαρτίζουν κάθε ELF object αρχείο είναι:

Σχηματικά:

[IMG]

Τα δύο views αποτελούν διαφορετικούς τρόπους με τους οποίους το σύστημα βλέπει ένα ELF object αρχείο. Το πρώτο (linking view) χρησιμοποιείται όταν το αρχείο πρόκειται να συνδεθεί για την παραγωγή εκτελέσιμου. Η δομική μονάδα εδώ είναι το section. Το δεύτερο (execution view) χρησιμοποιείται κατά τη φόρτωση-εκτέλεση ενός εκτελέσιμου. Ο loader δε "βλέπει" πια sections αλλά φορτώνει στη μνήμη ολόκληρα segments (ομάδες από sections).

[3.3 Ο ELF Header]

[IMG]

Ο ELF Header περιέχει βασικές πληροφορίες για το object αρχείο. To πρώτο του κομμάτι (16 bytes) είναι το ELF Identification. Αυτό εκτός από τον "μαγικό αριθμό" (υπογραφή) του ELF καθορίζει

Ακολουθούν 9 padding bytes και ύστερα αρχίζει ο κυρίως header.

[3.4 Τα ELF Sections]

Το section είναι ένα τμήμα του object αρχείου το οποίο περιέχει συγκεκριμένες και ομογενείς πληροφορίες. Για παράδειγμα, ένα section μπορεί να περιέχει τον κώδικα του προγράμματος, ένα άλλο τα δεδομένα, ένα τρίτο το string table κτλ. Τα sections ενός ELF αρχείου καθορίζονται στο Section Header Table. Αν και είναι προαιρετικός στο executable object αρχεία, πάντα περιλαμβάνεται (από όσο έχω δει). Το Section Header Table αποτελείται από μια σειρά από περιγραφείς, καθένας από τους οποίους μας δίνει πληροφορίες για ένα section:

[IMG]

Οι πληροφορίες που μας παρέχει η παραπάνω δομή είναι:

Υπάρχουν κάποια sections τα οποία κατά σύμβαση περιέχουν συγκεκριμένες πληροφορίες. Μια πλήρης λίστα μπορεί να βρεθεί στο standard. Τα πιο σημαντικά και κοινά είναι:

String Table Section

Τα τρία sections .strtab, .dynstr και .shstrtab περιέχουν strings τα οποία χρησιμοποιούνται από κάποια άλλα sections. Η δομή τους είναι αρκετά απλή: Το πρώτο byte του section είναι '\0' και από εκεί και πέρα ακολουθεί μια σειρά από null-terminated strings. Τα strings καθορίζονται από το offset τους από την αρχή του section.

[IMG]

Για το παραπάνω table έχουμε:

       index/offset       String
             1            "alf"
             2              "lf"
             5             "tx"
                    κτλ

[3.5 Τα ELF Segments]

Τα segments αποτελούνται από ένα ή περισσότερα sections τα οποία κατά τη φόρτωση του εκτελέσιμου/shared object αρχείου έχουν κοινές ιδιότητες. Για κάθε segment υπάρχει μια καταχώρηση στο Program Header Table:

[IMG]

[3.6 Εκτέλεση ενός ELF executable και dynamic linking]

Φόρτωση - Εκτέλεση

Σε γενικές γραμμές ακολουθούνται τα εξής βήματα:

1.

Αρχικά το λειτουργικό (μέσω του syscall exec()) διαβάζει τον header του εκτελέσιμου για να πάρει απαραίτητες πληροφορίες όπως:

* Αν όντως πρόκειται για εκτελέσιμο που μπορεί να τρέξει στον υπολογιστή

* Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα (segment) του εκτελέσιμου ( πχ read-only, executable κτλ)

2.

To λειτουργικό ελέγχει στο Program Header Table αν το εκτελέσιμο περιέχει ένα segment τύπου PT_INTERP (το οποίο περιέχει μόνο ένα section, το .interp).

Αν δεν υπάρχει τότε:

* Το λειτουργικό αποδίδει στη διεργασία τη μνήμη που χρειάζεται και φορτώνει τα διάφορα τμήματα στη μνήμη.

* Γίνεται relocation (επανατοποθέτηση) αν χρειάζεται.

* Τέλος, ο έλεγχος μεταφέρεται στο entry point του προγράμματος.

Αν υπάρχει (συνήθως υπάρχει):

* Διαβάζει από το segment το path του interpreter και φορτώνει τον interpreter στη μνήμη.

* Το λειτουργικό είτε φορτώνει το εκτελέσιμο στη μνήμη και "περνάει" τη διεύθυνση του στον interpreter, είτε "περνάει" στον interpreter έναν file descriptor για το αρχείο του εκτελέσιμου και τον αφήνει να κάνει τη δουλειά. Σε κάθε περίπτωση, ο έλεγχος περνάει στον interpreter.

* O interpreter, αν είναι ο dynamic linker ld.so (κατά 99% είναι αυτός), φορτώνει στο address space της διεργασίας τις βιβλιοθήκες που χρειάζεται το εκτελέσιμο.

* Γίνεται relocation στο εκτελέσιμο και στις βιβλιοθήκες. Ως μέρος του relocation, διορθώνονται, αν είναι ανάγκη, οι αναφορές σε συναρτήσεις/δεδομένα των βιβλιοθηκών που φορτώθηκαν. Στο ELF αυτό μπορεί να γίνει και στο run-time.

* Δίνεται η δυνατότητα σε κάθε shared object να εκτελέσει κάποιο κώδικα αρχικοποίησης.

* Τέλος, ο έλεγχος μεταφέρεται στο entry point του εκτελέσιμου.

Το dynamic linking στο ELF

Τα sections τα οποία σχετίζονται με το dynamic linking σε ένα εκτελέσιμο είναι τα εξής:

.dynamic

Αυτό περιέχει πληροφορίες για το ποια shared object είναι απαραίτητα για την εκτέλεση, τη διεύθυνση του relocation table, τη διεύθυνση του symbol table που περιέχει τα εξωτερικά σύμβολα κτλ. Ο dynamic linker διαβάζει τα πεδία αυτά, φορτώνει (και επανατοποθετεί) τις βιβλιοθήκες και προσπαθεί να "επιλύσει" (resolve) τις αναφορές στα εξωτερικά ��ύμβολα. Με άλλα λόγια, βρίσκει τις διευθύνσεις των συμβόλων και τις διορθώνει στο εκτελέσιμο. Πρέπει να σημειωθεί πως οι βιβλιοθήκες που φορτώνει ο dynamic linker μπορεί και αυτές να απαιτούν άλλες βιβλιοθήκες, οπότε η όλη διαδικασία επαναλαμβάνεται αναδρομικά. Βέβαια, κάθε βιβλιοθήκη φορτώνεται μόνο μια φορά, άσχετα από το αν χρησιμοποιείται από πολλά ELF objects.

.got (Global Offset Table)

Το section αυτό (αφού φορτωθεί το πρόγραμμα) περιλαμβάνει τις διευθύνσεις των συμβόλων που είναι πιθανό να αλλάξουν από το relocation. Στα εκτελέσιμα περιέχει μόνο τις διευθύνσεις των εξωτερικών συμβόλων (dynamically linked), ενώ για τα shared object περιλαμβάνει τις διευθύνσεις όλων των συμβόλων (εσωτερικών και εξωτερικών). Αυτό συμβαίνει αφού σίγουρα το shared object θα επανατοποθετηθεί. Οι πρώτες τρεις καταχωρίσεις έχουν ειδική σημασία. Η πρώτη (offset 0) περιέχει τη διεύθυνση του .dynamic section. Η τρίτη περιλαμβάνει τη διεύθυνση του dynamic linker. Η τύχη της δεύτερης αγνοείται προς το παρόν.

.plt (Procedure Linkage Table)

Αυτό έχει τη μορφή:
plt_start:
        push got_start+4
        jmp [got_start+8]
        ...
jmp_func1:
        jmp [func1_offset]
        push relocation_offset1
        jmp plt_start

jmp_func2:
        ...

Το func1_offset είναι μια διεύθυνση μέσα στο .got section που τελικά (μετά το relocation) θα περιέχει τη διεύθυνση της συνάρτησης func1. To relocation_offset1 είναι ένα offset που καθορίζει μια καταχώρηση στο relocation table. H καταχώρηση αυτή θα έχει ως offset αλλαγής (η διεύθυνση στην οποία θα γίνει η διόρθωση) το func1_offset.

Αρχικά, η διεύθυνση func1_offset περιέχει τη διεύθυνση της επόμενης εντολής από την jmp [func1_offset]. Έτσι, ο έλεγχος πάει στην push relocation_offset1 και μετά, μέσω της jmp plt_start, στο push got_start+4, jmp [got_start+8].

Το got_start+8 είναι η τρίτη καταχώρηση στο .got section και όπως είπαμε περιλαμβάνει τη διεύθυνση του dynamic linker. Έτσι, ο έλεγχος περνάει στο dynamic linker με δύο παραμέτρους (got_start+4, relocation_offset1). Με αυτές τις πληροφορίες ο dynamic linker μπορεί να βρει τη διεύθυνση της ζητούμενης συνάρτησης και να την τοποθετήσει στη func1_offset. Την επόμενη φορά που θα κληθεί η jmp_func1, η εντολή jmp [func1_offset] θα οδηγήσει στην πραγματική συνάρτηση func1 (και όχι στο αμέσως επόμενο push).

Είναι φανερό πως αυτή η διαδικασία δεν είναι απαραίτητο να γίνει στο φόρτωμα του προγράμματος αλλά την πρώτη φορά που θα χρειαστεί να κληθεί η func1. Αυτή είναι η default συμπεριφορά και ονομάζεται lazy binding. Μπορούμε να την αλλάξουμε δίνοντας στη enviroment μεταβλητή LD_BIND_NOW μία μη null τιμή.

[3.7 Εργαλεία για το ELF]

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

Το πιο κοινό ίσως εργαλείο (αλλά σίγουρα όχι το καλύτερο) είναι το objdump. Το objdump είναι μέρος των binutils και μπορεί να μας δώσει σχεδόν όλες τις πληροφορίες που περιέχει ένα ELF object αρχείο. Η επιλογή των πληροφοριών που θα απεικονιστούν γίνεται με διάφορα flags. Βασικά flags είναι το -x το οποίο δείχνει σχεδόν όλες τις πληροφορίες (εκτός από αυτές που σχετίζονται με δυναμικά σύμβολα), το -Τ που δείχνει τα δυναμικά σύμβολα, το -d που κάνει disassemble τον κώδικα, το -M με το οποίο ρυθμίζουμε τον disassembler (πχ -M intel για intel σύνταξη) και το -C που ενεργοποιεί το demangling. Το κύριο μειονέκτημα του objdump είναι στον τομέα του disassembly. Το listing που παράγεται έχει ελάχιστες πληροφορίες, ειδικά αν δεν υπάρχουν σύμβολα. Παρέα με το objdump (στα binutils) έρχονται το readelf και το nm. Η αλήθεια είναι πως δεν είναι και πολύ χρήσιμα, αφού το objdump κάνει ότι τα προηγούμενα δύο μαζί και ακόμα παραπάνω.

Ένα σαφώς πιο εξελιγμένο εργαλείο είναι το ELFsh (Site: http://elfsh.devhell.org[3]). Πρόκειται για μια scripting γλώσσα που μας επιτρέπει να διαχειριστούμε ένα ELF αρχείο. Οι δυνατότητές του ποικίλουν από μια απλή απεικόνιση των πληροφοριών του header, μέχρι και συγχώνευση δύο object αρχείων! Η "ψυχή" του προγράμματος βρίσκεται σε μια καλοσχεδιασμένη βιβλιοθήκη (libelfsh) και έτσι μπορεί να χρησιμοποιηθεί σε οποιαδήποτε άλλη εφαρμογή.

3: http://elfsh.devhell.org

Μια πιο εύχρηστη λύση είναι o HT Editor (Site: http://hte.sourceforge.net[4]). Παρέχει πλήρη διαχείριση του object αρχείου και έναν αρκετά καλό disassembler. Θα αναφερθούμε λίγο περισσότερο σε αυτόν στο κομμάτι για το dead-listing[5].

4: http://hte.sourceforge.net

5: 05_rce3-5.html#dead-listing

[4. Το /proc filesystem]

[4.1 Τι είναι το /proc filesystem]

To /proc είναι ένα εικονικό σύστημα αρχείων το οποίο μας δίνει τη δυνατότητα να πάρουμε πληροφορίες από τις δομές δεδομένων του πυρήνα. Είναι εικονικό με την έννοια ότι τα αρχεία που βλέπουμε δεν έχουν κάποια φυσική υπόσταση (πχ δεν βρίσκονται σε κάποια συσκευή). Τα περισσότερα αρχεία μπορούν να ανοιχτούν μόνο για ανάγνωση. Με ένα "man proc" θα λάβετε ότι πληροφορίες για το /proc θέλετε και δε θέλετε να μάθετε :)

[4.2 Γενικές πληροφορίες για το σύστημα]

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

[4.3 Πληροφορίες για μια διεργασία]

Αν και το /proc filesystem περιέχει τεράστιο όγκο πληροφοριών, αυτές που είναι πραγματικά χρήσιμες για κάποιον που επιδίδεται στο RCE είναι οι πληροφορίες περί διεργασιών. Αυτές βρίσκονται στον κατάλογο με αριθμό (όνομα) το pid της διεργασίας. Ο κατάλογος περιέχει:

/cwd (directory link): link στον τρέχον κατάλογο της διεργασίας (current working directory).

/fd (directory) : Περιέχει links στα ανοικτά αρχεία της διεργασίας. Πχ το link για το αρχείο με file descriptor 2 είναι fd/2.

/root (directory link): link στο root directory του filesystem στο οποίο εκτελείται η διεργασία (συνήθως "/" ).

cmdline : Η πλήρης γραμμή εντολής με την οποία κλήθηκε η διεργασία.

environ : Λίστα με τις enviroment variables που βλέπει η διεργασία.

exe (link) : link στο εκτελέσιμο από το οποίο δημιουργήθηκε η διεργασία.

maps : Το memory map της διεργασίας. Για παράδειγμα για την εντολή less:

Virtual Address   Perm  Offset  Device  INode        Path
08048000-08060000 r-xp 00000000 03:07 1457441    /usr/bin/less
08060000-08061000 rw-p 00018000 03:07 1457441    /usr/bin/less
08061000-0806a000 rwxp 00000000 00:00 0
40000000-40014000 r-xp 00000000 03:07 615324     /lib/ld-2.3.1.so
40014000-40015000 rw-p 00014000 03:07 615324     /lib/ld-2.3.1.so
40026000-40059000 r-xp 00000000 03:07 615315     /lib/libncurses.so.5.3
40059000-40062000 rw-p 00032000 03:07 615315     /lib/libncurses.so.5.3
40062000-40063000 rw-p 00000000 00:00 0
40063000-4018d000 r-xp 00000000 03:07 615327     /lib/libc-2.3.1.so
4018d000-40192000 rw-p 0012a000 03:07 615327     /lib/libc-2.3.1.so
40192000-40196000 rw-p 00000000 00:00 0
40196000-40396000 r--p 00000000 03:07 745906     /usr/lib/locale/locale-archive
40396000-403c9000 r--p 00507000 03:07 745906     /usr/lib/locale/locale-archive
bfffc000-c0000000 rwxp ffffd000 00:00 0

Η πρώτη στήλη και η δεύτερη δίνουν το πεδίο εικονικών διευθύνσεων που καταλαμβάνει το segment και τα διακαιώματα του, αντίστοιχα. Η τρίτη είναι το offset στο οποίο βρίσκεται το segment στο αρχείο από το οποίο φορτώθηκε. Οι υπόλοιπες στήλες δίνουν το device major:minor number, το ΙNode και το πλήρες path του αρχείο αυτού.

Παρατηρήστε πως όλα τα shared object φορτώνονται στη διεύθυνση 0x40000000 και πάνω. Επιπλέον το τελευταίο segment περιέχει το σωρό. Το γεγονός ότι από default είναι executable (-x- flag) είναι η ρίζα του κακού για τα buffer overflow exploits.

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

mounts : Λίστα με όλα τα mounted directories. (link στο self/mounts)

stat : Η κατάσταση της διεργασίας (αλά ps).

statm : Πληροφορίες για την κατάσταση της μνήμης.

status : Πληροφορίες από τα δύο παραπάνω αρχεία σε πιο ευανάγνωστη μορφή.

Οι πιο χρήσιμες (για RCE) από τις παραπάνω πληροφορίες είναι αυτές που δίνονται από τα fd/, map, cmdline και exe. Ειδικά το τελευταίο έχει μια ενδιαφέρουσα χρήση. Ακόμα και αν διαγράψουμε το εκτελέσιμο αρχείο μιας διεργασίας ενώ εκτελείται, μπορούμε να έχουμε πρόσβαση σε αυτό μέσω του exe!

[5. Χρήσιμες έως πολύ χρήσιμες πληροφορίες ΙΙ]

[]{#dead-listing} [5.1 Dead Listing και Εργαλεία]

Τι είναι το dead listing

Dead listing είναι η παρουσίαση του κώδικα ενός εκτελέσιμου σε στατική μορφή. Κύριο πλεονέκτημα αυτού του τρόπου μελέτης του κώδικα είναι πως δίνει μια πιο ολοκληρωμένη άποψη για τη λειτουργία του, και, ανάλογα με την ποιότητα του disassembler, ένα πλήθος πληροφοριών (function calls, strings κτλ). Το βασικό μειονέκτημα είναι ότι δε μπορεί να μας δώσει χρήσιμα αποτελέσματα αν ο κώδικας αλλάζει κατά τη διάρκεια της εκτέλεσης (πχ self-modifying code, συμπιεσμένα/κρυπτογραφημένα εκτελέσιμα) ή αν υπάρχουν πολλά έμμεσα jmp και calls (πχ jmp [eax]), διότι δεν μπορούμε να ακολουθήσουμε (έστω και νοητικά) την πορεία της εκτέλεσης. Το καλύτερο που έχουμε να κάνουμε σε τέτοιες περιπτώσεις, είναι να φέρουμε το εκτελέσιμο σε μια μορφή που να μπορεί να διαβάσει ο disassembler, ώστε να εκμεταλλευτούμε ��ις ευκολίες που μας προσφέρει.

[]{#objdump-weakness} Δυστυχώς, οι disassembly δυνατότητες του objdump δεν είναι αρκετά ικανοποιητικές (μη ξεχνάμε βέβαια πως το objdump δε σχεδιάστηκε για αυτό). Εκτός από ο γεγονός ότι δε φροντίζει να παρουσιάζει τα cross-references (πχ αν μια διεύθυνση μνήμης είναι στόχος μιας jump ή call εντολής), το disassembly του είναι γραμμικό και δε γίνεται καμία προσπάθεια να ακολουθηθεί η ροή του προγράμματος. Για παράδειγμα το παρακάτω κομμάτι κώδικα ο ΗΤEditor το κάνει disassemble σωστά...

8048080 !
....... ! entrypoint:
....... !   jmp     loc_8048083
8048082     db      86h
8048083 !
....... ! loc_8048083:                    ;xref j8048080
....... !   cmp     eax, ebx
8048085 !   ret

...ενώ το objdump δεν καταλαβαίνει ότι ο έλεγχος θα πάει στη διεύθυνση 0x8048083 και συνεχίζει το disassembly με το επόμενο byte (dummy byte), με αποτέλεσμα να αποσυγχρονιστεί το listing από εκεί και κάτω.

08048080 <_start>:
8048080:       eb 01                   jmp    8048083 <_start+0x3>
8048082:       86 39                   xchg   BYTE PTR [ecx],bh
8048084:       d8 c3                   fadd   %st,st(3)

Μπορεί τα "κανονικά" προγράμματα να μην έχουν κώδικα αυτής της μορφής αλλά πρόκειται για μια πολύ κοινή τεχνική για προστασία έναντι των "χαζών" disassemblers.

Εργαλεία

Παρακάτω παρουσιάζονται συνοπτικά μερικοί disassemblers και το disassembly που παράγουν για το γνωστό :) πρόγραμμα.

--------------------------------------------------------------------------------
#include <stdio.h>

int main(int argc, char **argv)
{
int num;

if (argc<2) {
printf("Usage: %s <number>\n",argv[0]);
exit(1);
}

num=alf(argv[1]);

if (num>10)
printf("Ok!\n");
else
printf("Failed!\n");

return 1;
}

int alf(char *s)
{
return atoi(s);
}
--------------------------------------------------------------------------------

IDA

Ένα από τα καλύτερα εργαλεία για disassemby είναι ο IDA (Interactive DisAssembler). Υπάρχει μια freeware έκδοση που δουλεύει σε DOS, ενώ η εμπορική υπάρχει και για περιβάλλον Windows. Υποστηρίζει πλήθος αρχιτεκτονικών, εκτελέσιμων και compilers, ενώ περιέχει μια script γλώσσα για διάφορες αυτοματοποιήσεις. Στο linux μπορεί να εκτελεστεί μέσω wine. Το επίσημο site είναι http://www.datarescue.com[6], ενώ τη freeware εκδοση μπορείτε να τη βρείτε με μια αναζήτηση στο google.

6: http://www.datarescue.com

.text:0804838C ; ??????????????? S U B R O U T I N E ???????????????????????????????????????
.text:0804838C
.text:0804838C ; Attributes: bp-based frame
.text:0804838C
.text:0804838C                 public main
.text:0804838C main            proc near               ; DATA XREF: _start+17 o
.text:0804838C
.text:0804838C var_4           = dword ptr -4
.text:0804838C arg_0           = dword ptr  8
.text:0804838C arg_4           = dword ptr  0Ch
.text:0804838C
.text:0804838C                 push    ebp
.text:0804838D                 mov     ebp, esp
.text:0804838F                 sub     esp, 8
.text:08048392                 and     esp, 0FFFFFFF0h
.text:08048395                 mov     eax, 0
.text:0804839A                 sub     esp, eax
.text:0804839C                 cmp     [ebp+arg_0], 1
.text:080483A0                 jg      short loc_80483C1
.text:080483A2                 sub     esp, 8
.text:080483A5                 mov     eax, [ebp+arg_4]
.text:080483A8                 push    dword ptr [eax]
.text:080483AA                 push    offset aUsageSNumber ; "Usage: %s <number>\n"
.text:080483AF                 call    _printf
.text:080483B4                 add     esp, 10h
.text:080483B7                 sub     esp, 0Ch
.text:080483BA                 push    1
.text:080483BC                 call    _exit
.text:080483C1
.text:080483C1 loc_80483C1:                            ; CODE XREF: main+14 j
.text:080483C1                 sub     esp, 0Ch
.text:080483C4                 mov     eax, [ebp+arg_4]
.text:080483C7                 add     eax, 4
.text:080483CA                 push    dword ptr [eax]
.text:080483CC                 call    alf
.text:080483D1                 add     esp, 10h
.text:080483D4                 mov     [ebp+var_4], eax
.text:080483D7                 cmp     [ebp+var_4], 0Ah
.text:080483DB                 jle     short loc_80483EF
.text:080483DD                 sub     esp, 0Ch
.text:080483E0                 push    offset aOk      ; "Ok!\n"
.text:080483E5                 call    _printf
.text:080483EA                 add     esp, 10h
.text:080483ED                 jmp     short loc_80483FF
.text:080483EF ; ---------------------------------------------------------------------------
.text:080483EF
.text:080483EF loc_80483EF:                            ; CODE XREF: main+4F j
.text:080483EF                 sub     esp, 0Ch
.text:080483F2                 push    offset aFailed  ; "Failed!\n"
.text:080483F7                 call    _printf
.text:080483FC                 add     esp, 10h
.text:080483FF
.text:080483FF loc_80483FF:                            ; CODE XREF: main+61 j
.text:080483FF                 mov     eax, 1
.text:08048404                 leave
.text:08048405                 retn
.text:08048405 main            endp
.text:08048405
.text:08048406
.text:08048406 ; ??????????????? S U B R O U T I N E ???????????????????????????????????????
.text:08048406
.text:08048406 ; Attributes: bp-based frame
.text:08048406
.text:08048406                 public alf
.text:08048406 alf             proc near               ; CODE XREF: main+40 p
.text:08048406
.text:08048406 arg_0           = dword ptr  8
.text:08048406
.text:08048406                 push    ebp
.text:08048407                 mov     ebp, esp
.text:08048409                 sub     esp, 8
.text:0804840C                 sub     esp, 0Ch
.text:0804840F                 push    [ebp+arg_0]
.text:08048412                 call    _atoi
.text:08048417                 add     esp, 10h
.text:0804841A                 leave
.text:0804841B                 retn
.text:0804841B alf             endp
.text:0804841B
.text:0804841C

Bastard Disassembly Enviroment

Στον κόσμο του open source τώρα, στο sourceforge θα βρείτε projects που υπόσχονται πολλά αλλά δυστυχώς είναι σε πρώιμο στάδιο. Ένα από αυτά είναι το Bastard Disassembly Enviroment. Πρακτικά πρόκειται για μια scripting γλώσσα και το αντίστοιχο interpreter shell. Site: http://bastard.sourceforge.net[7].

7: http://bastard.sourceforge.net

main:
0804838C 55                             push    ebp
0804838D 89 E5                          mov     ebp , esp
0804838F 83 EC 08                       sub     esp , 0x8
08048392 83 E4 F0                       and     esp , 0xF0
08048395 B8 00 00 00 00                 mov     eax , 0x0
0804839A 29 C4                          sub     esp , eax
0804839C 83 7D 08 01                    cmp     [ebp+0x08] , 0x1
080483A0 7F 1F                          jg      loc_080483C1  ;(0x80483C1 was +31) ;  xrefs: >080483C1[x]
080483A2 83 EC 08                       sub     esp , 0x8
080483A5 8B 45 0C                       mov     eax , [ebp+0x0C]
080483A8 FF 30                          push    [eax]
080483AA 68 64 84 04 08                 push    0x8048464
080483AF E8 F8 FE FF FF                 call    printf  ;(0x80482AC was -264) ;  xrefs: >080482AC[x]
080483B4 83 C4 10                       add     esp , 0x10
080483B7 83 EC 0C                       sub     esp , 0xC
080483BA 6A 01                          push    0x1
080483BC E8 FB FE FF FF                 call    exit  ;(0x80482BC was -261) ;  xrefs: >080482BC[x]
loc_080483C1:
080483C1 83 EC 0C                       sub     esp , 0xC  ;  xrefs: <080483A0[x]
080483C4 8B 45 0C                       mov     eax , [ebp+0x0C]
080483C7 83 C0 04                       add     eax , 0x4
080483CA FF 30                          push    [eax]
080483CC E8 35 00 00 00                 call    alf  ;(0x8048406 was +53) ;  xrefs: >08048406[x]
080483D1 83 C4 10                       add     esp , 0x10
080483D4 89 45 FC                       mov     [ebp-0x04] , eax
080483D7 83 7D FC 0A                    cmp     [ebp-0x04] , 0xA
080483DB 7E 12                          jle     loc_080483EF  ;(0x80483EF was +18) ;  xrefs: >080483EF[x]
080483DD 83 EC 0C                       sub     esp , 0xC
080483E0 68 78 84 04 08                 push    0x8048478
080483E5 E8 C2 FE FF FF                 call    printf  ;(0x80482AC was -318) ;  xrefs: >080482AC[x]
080483EA 83 C4 10                       add     esp , 0x10
080483ED EB 10                          jmp     loc_080483FF  ;(0x80483FF was +16) ;  xrefs: >080483FF[x]
loc_080483EF:
080483EF 83 EC 0C                       sub     esp , 0xC  ;  xrefs: <080483DB[x]
080483F2 68 7D 84 04 08                 push    0x804847D
080483F7 E8 B0 FE FF FF                 call    printf  ;(0x80482AC was -336) ;  xrefs: >080482AC[x]
080483FC 83 C4 10                       add     esp , 0x10
loc_080483FF:
080483FF B8 01 00 00 00                 mov     eax , 0x1  ;  xrefs: <080483ED[x]
08048404 C9                             leave
08048405 C3                             ret
alf:
08048406 55                             push    ebp   ;  xrefs: <080483CC[x]
08048407 89 E5                          mov     ebp , esp
08048409 83 EC 08                       sub     esp , 0x8
0804840C 83 EC 0C                       sub     esp , 0xC
0804840F FF 75 08                       push    [ebp+0x08]
08048412 E8 B5 FE FF FF                 call    atoi  ;(0x80482CC was -331) ;  xrefs: >080482CC[x]
08048417 83 C4 10                       add     esp , 0x10
0804841A C9                             leave
0804841B C3                             ret

HT Editor

Πρόκειται για ένα ιδιαίτερα χρήσιμο και καλοφτιαγμένο εργαλείο. Ο HT Editor είναι ένας editor με έμφαση στα εκτελέσιμα αρχεία. Τα file formats που υποστηρίζει είναι τα COFF, ELF, LE, NE, PE, MZ, και Java Class. Εκτός από τη δυνατότητα για εύκολη επεξεργασία των headers, sections, symbols κτλ των αρχείων, προσφέρει έναν αρκετά καλό disassembler. Το μόνο πρόβλημα που υπάρχει (στην έκδοση 0.7.3 τουλάχιστον), είναι ότι ο C++ demangler δεν υποστηρίζει ακόμα το gnu-V3 mangling, οπότε αν κάποιο πρόγραμμα έχει γίνει compile με g++ 3 τα σύμβολα θα είναι ακαταλαβίστικα. Site: http://hte.sourceforge.net[8].

8: http://hte.sourceforge.net

....... ! ;****************************************************
....... ! ; function main (global)
....... ! ;****************************************************
....... ! main:                           ;xref o80482f3
....... !   push    ebp
804838d !   mov     ebp, esp
804838f !   sub     esp, 8
8048392 !   and     esp, 0fffffff0h
8048395 !   mov     eax, 0
804839a !   sub     esp, eax
804839c !   cmp     dword ptr [ebp+8], 1
80483a0 !   jg      loc_80483c1
80483a2 !   sub     esp, 8
80483a5 !   mov     eax, [ebp+0ch]
80483a8 !   push    dword ptr [eax]
80483aa !   push    strz_Usage:__s__number___8048464
80483af !   call    printf@@GLIBC_2.0
80483b4 !   add     esp, 10h
80483b7 !   sub     esp, 0ch
80483ba !   push    1
80483bc !   call    exit@@GLIBC_2.0
80483c1 !
....... ! loc_80483c1:                    ;xref j80483a0
....... !   sub     esp, 0ch
80483c4 !   mov     eax, [ebp+0ch]
80483c7 !   add     eax, 4
80483ca !   push    dword ptr [eax]
80483cc !   call    alf
80483d1 !   add     esp, 10h
80483d4 !   mov     [ebp-4], eax
80483d4 !   mov     [ebp-4], eax
80483d7 !   cmp     dword ptr [ebp-4], 0ah
80483db !   jng     loc_80483ef
80483dd !   sub     esp, 0ch
80483e0 !   push    data_8048478
80483e5 !   call    printf@@GLIBC_2.0
80483ea !   add     esp, 10h
80483ed !   jmp     loc_80483ff
80483ef !
....... ! loc_80483ef:                    ;xref j80483db
....... !   sub     esp, 0ch
80483f2 !   push    strz_Failed___804847d
80483f7 !   call    printf@@GLIBC_2.0
80483fc !   add     esp, 10h
80483ff !
....... ! loc_80483ff:                    ;xref j80483ed
....... !   mov     eax, 1
8048404 !   leave
8048405 !   ret
8048406 !
....... ! ;****************************************************
....... ! ; function alf (global)
....... ! ;****************************************************
....... ! alf:                            ;xref c80483cc
....... !   push    ebp
8048407 !   mov     ebp, esp
8048409 !   sub     esp, 8
804840c !   sub     esp, 0ch
804840f !   push    dword ptr [ebp+8]
8048412 !   call    atoi@@GLIBC_2.0
8048417 !   add     esp, 10h
804841a !   leave
804841b !   ret

LDasm

Το ldasm είναι ένα πρόγραμμα που χρησιμοποιεί και επεκτείνει την έξοδο του objdump. Χρησιμοποιεί perl/Tk για να δώσει ένα οπτικό αποτέλεσμα παρόμοιο με το W32Dasm που υπάρχει για Windows. Δυστυχώς, ο δημιουργός του το έχει παρατήσει και έτσι έχει ξεμείνει στην έκδοση 0.04.53 (!). Πάντως, έχει τις στοιχειώδεις δυνατότητες που χρειαζόμαστε, αν και αφού χρησιμοποιεί το objdump, έχει και τα ίδια αδύνατα σημεία. Site: http://Feedface.com/projects/ldasm[9].

9: http://Feedface.com/projects/ldasm

Exported fn(): main
:0804838c 55                     push ebp
:0804838d 89e5                   mov  ebp, esp
:0804838f 83ec08                 sub  esp, 8
:08048392 83e4f0                 and  esp, -16
:08048395 b800000000             mov  eax, 0
:0804839a 29c4                   sub  esp, eax
:0804839c 837d0801               cmpl ptr [ebp+8], 1
:080483a0 7f1f                   jg   080483c1
:080483a2 83ec08                 sub  esp, 8
:080483a5 8b450c                 mov  eax, ptr [ebp]
:080483a8 ff30                   pushl (eax)


|
:080483aa 6864840408             push 8048464


|
:080483af e8f8feffff             call 080482ac
:080483b4 83c410                 add  esp, 10
:080483b7 83ec0c                 sub  esp, c
:080483ba 6a01                   push 1


|
:080483bc e8fbfeffff             call 080482bc

Referenced by a (U)nconditional or (C)onditional Jump at Address:
| :080483a0
|
:080483c1 83ec0c                 sub  esp, c
:080483c4 8b450c                 mov  eax, ptr [ebp]
:080483c7 83c004                 add  eax, 4
:080483ca ff30                   pushl (eax)


|
:080483cc e835000000             call 08048406
:080483d1 83c410                 add  esp, 10
:080483d4 8945fc                 mov  ptr [ebp-4], eax
:080483d7 837dfc0a               cmpl ptr [ebp-4], a
:080483db 7e12                   jle  080483ef
:080483dd 83ec0c                 sub  esp, c


|
:080483e0 6878840408             push 8048478


|
:080483e5 e8c2feffff             call 080482ac
:080483ea 83c410                 add  esp, 10
:080483ed eb10                   jmp  080483ff

Referenced by a (U)nconditional or (C)onditional Jump at Address:
| :080483db
|
:080483ef 83ec0c                 sub  esp, c


|
:080483f2 687d840408             push 804847d


|
:080483f7 e8b0feffff             call 080482ac
:080483fc 83c410                 add  esp, 10

Referenced by a (U)nconditional or (C)onditional Jump at Address:
| :080483ed
|
:080483ff b801000000             mov  eax, 1
:08048404 c9                     leave
:08048405 c3                     ret

Referenced by a Call at Address:
| :080483cc
|
Exported fn(): alf
:08048406 55                     push ebp
:08048407 89e5                   mov  ebp, esp
:08048409 83ec08                 sub  esp, 8
:0804840c 83ec0c                 sub  esp, c
:0804840f ff7508                 pushl ptr [ebp+8]


|
:08048412 e8b5feffff             call 080482cc
:08048417 83c410                 add  esp, 10
:0804841a c9                     leave
:0804841b c3                     ret

[]{#packing}[5.2 Packed Executables - Συμπιεσμένα Εκτελέσιμα]

Τι είναι;

Τα εκτελέσιμα αρχεία, όπως και όλα τα αρχεία, περιέχουν μέσα τους επαναλήψεις που καθιστούν δυνατή τη συμπίεση τους. Η συμπίεση στα εκτελέσιμα, εφόσον η αποσυμπίεση μπορεί να εκτελεστεί αρκετά γρήγορα ώστε να μη γίνεται αισθητή, είναι σίγουρα επιθυμητή. Τα αρχεία καταλαμβάνουν λιγότερο χώρο και επίσης είναι πιο δύσκολο να ερευνηθούν και να αλλαχτούν (βέβαια, για όσους κάνουμε RCE αυτό είναι μεγάλο πρόβλημα). Μάλιστα, πολλά προγράμματα συμπίεσης εκτελέσιμων εφαρμόζουν και άλλες τεχνικές, όπως κρυπτογράφηση και CRC ελέγχους. Είναι σαφές πως δε βολεύει απλώς να συμπιεστεί το αρχείο με κάποια παραδοσιακή μέθοδο (πχ με το gzip). Αυτό συμβαίνει, διότι εκτός από το ότι το εκτελέσιμο δεν είναι πια εκτελέσιμο (η κατάσταση διορθώνεται με χρήση scripts για αυτόματη αποσυμπίεση, το utility gzexe λειτουργεί έτσι), χάνεται η προστασία από το RCE, αφού τελικά το εκτελέσιμο θα βρεθεί στην αρχική του μορφή πριν εκτελεστεί. Για αυτό, έχουν αναπτυχθεί διάφορες άλλες τεχνικές για συμπίεση προγραμμάτων.

Βασικές Τεχνικές packing

Καταρχάς θα δούμε την απλή packing τεχνική που αναφέραμε στην εισαγωγή . Η βασική της λειτουργία φαίνεται στο παρακάτω σχήμα:

[IMG]

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

Μια πιo εξελιγμένη τεχνική :

[IMG]

Εδώ τα πράγματα είναι πιο ενδιαφέροντα. Δε συμπιέζεται όλο το εκτελέσιμο αλλά μόνο τα segments του. Το καινούργιο εκτελέσιμο περιέχει τα συμπιεσμένα δεδομένα και τον κώδικα για το unpacking (κατά προτίμηση μετά τα δεδομένα).

[IMG]

Κατά την εκτέλεση φορτώνονται τα συμπιεσμένα segments στη μνήμη, με τέτοιο τρόπο, ώστε όταν αποσυμπιεστούν να έχουν τις αρχικές διευθύνσεις τους. Επίσης, ο unpacker πρέπει να κάνει και κάτι άλλο που δεν είναι φανερό με την πρώτη ματιά. Αν το αυθεντικό εκτελέσιμο χρησιμοποιούσε shared objects (βιβλιοθήκες), τότε μέσα στο αρχείο υπήρχαν οι πληροφορίες, ώστε ο dynamic linker να τα φορτώσει. Όμως το συμπιεσμένο εκτελέσιμο έχει διαφορετικές πληροφορίες και έτσι τα shared objects δε φορτώνονται. Θα πρέπει ο unpacker να επωμιστεί αυτό το βάρος και επιπλέον να διορθώσει τις αναφορές στα εξωτερικά σύμβολα. Αυτό γίνεται με χρήση των συναρτήσεων dlopen() και dlsym() που χρησιμοποιούνται για να φορτώνουν shared objects στο run-time (δείτε manpages). Ο unpacker, αφού ολοκληρώσει όλες τις εργασίες του, θα μεταφέρει τον έλεγχο στο OEP (original entry point) και έτσι θα αρχίσει το κυρίως πρόγραμμα.

Βασικές Τεχνικές Unpacking

Στην πιο απλή περίπτωση, όπου ο unpacker αποσυμπιέζει το αρχικό αρχείο στο δίσκο και το εκτελεί από εκεί, τα πράγματα είναι εύκολα. Αρκεί να βρούμε ποιο προσωρινό αρχείο χρησιμοποιείται και τελειώσαμε. Το /proc filesystem είναι ιδιαίτερα χρήσιμο, όπως θα φανεί και στο hands-on παράδειγμα που ακολουθεί.

Αν ο packer είναι εξελιγμένος, τότε θα πρέπει να φερθούμε και εμείς πιο έξυπνα. Η μέθοδος αυτή είναι σχετικά επίπονη αλλά μπορεί να εφαρμοστεί ακόμα και στις πιο δύσκολες περιπτώσεις. Σκοπός είναι να κάνουμε dump από τη μνήμη την εικόνα του εκτελέσιμου σε ένα αρχείο. Αυτό, βέβαια, δε θα είναι το πλήρες, αυθεντικό αρχείο αλλά συχνά είναι ότι καλύτερο μπορούμε να κάνουμε. Εξάλλου, μετά μπορούμε να κάνουμε διάφορες επεμβάσεις για να το "βελτιώσουμε".

Η διαδικασία αποτελείται από τα παρακάτω βήματα:

1. Ανάκτηση του OEP (original entry point) : Εδώ προσπαθούμε να εντοπίσουμε τη διεύθυνση της πρώτης εντολής του αυθεντικού προγράμματος στην εικόνα του εκτελέσιμου μετά την αποσυμπίεση. Αυτό το βήμα είναι που απαιτεί την περισσότερη εμπειρία. Σημάδια πως τελειώνει ο κώδικας του unpacker και περνάμε στο αρχικό εκτελέσιμο είναι:

* Unconditional jump ( άμεσο πχ jmp 0x8048954 ή έμμεσο πχ jmp eax ) σε κάποια "μακρινή" διεύθυνση.

* Αλλαγή του ύφους του προγράμματος. Οι unpackers είναι συνήθως γραμμένοι σε assembly με το χέρι και το στυλ τους διαφέρει από τον κώδικα που έχει παραχθεί από compilers.

* Χρήση των popa/popad (pop all registers). Επειδή η unpackers αλλοιώνουν το context της διεργασίας (πχ τιμές των registers) τους σώζουν όλους πριν αρχίσουν (με pusha/pushad) και τους επαναφέρουν πριν περάσουν τον έλεγχο στο αρχικό πρόγραμμα.

2. Dump της εικόνας (image) του εκτελέσιμου σε ένα αρχείο : Κάνουμε dump την εικόνα του εκτελέσιμου, όπως είναι αυτή, ακριβώς πριν περάσει ο έλεγχος στο αυθεντικό εκτελέσιμο. Για αυτό χρειαζόμαστε το OEP που μας δινει το προηγούμενο βήμα. Αφου, λοιπόν, το έχουμε, αναγκάζουμε τη διεργασία να εκτελεστεί μέχρι το OEP (έτσι είμαστε σίγουροι πως όλα είναι unpacked όπως πρέπει) και τότε κάνουμε dump την εικόνα του εκτελέσιμου. Μπορούμε να χρησιμοποιήσουμε την εντολή dump του gdb. Η σύνταξη της είναι dump memory <file> <start> <stop>. Για να αποφασίσουμε ποιες διευθύνσεις θα κάνουμε dump μπορούμε να συμβουλευτούμε το map από το /proc/<pid>/. Για τα εκτελέσιμα στο linux, αρκούν συνήθως τα δύο πρώτα segments που είναι το code και data segment αντίστοιχα .

3. Διόρθωση του ELF Header : Διορθώνουμε τον ELF Header του dumped αρχείου, ώστε να μπορεί να εξεταστεί και να εκτελεστεί. Συγκεκριμένα θέτουμε το entry point του εκτελέσιμου στο OEP, διορθώνουμε τα στοιχεία των segments και αναδημιουργούμε το DYNAMIC segment. Το τελευταίο είναι αρκετά επίπονο και όχι απαραίτητο αν απλώς θέλουμε να εξετάσουμε τον κώδικα (dissasembly). Αν δε γίνει αυτή η διόρθωση, τότε οι διευθύνσεις των συναρτήσεων των shared objects θα έχουν παντα τις τιμές που είχαν την στιγμή που κάναμε dump, διότι ο dynamic linker δε θα ενεργοποιηθεί. Αν χρησιμοποιούμε το dumped εκτελέσιμο μόνο στο δικό μας σύστημα και με τι ίδιες ακριβώς βιβλιοθήκες, ίσως να τη γλιτώσουμε.

[6. Hands-on Παράδειγμα - Υπό πίεση]

[6.1 Πράξη 1η]

Αυτή τη φορά θα ασχοληθούμε με το demo του εκπληκτικού προγράμματος για πράξεις μη αρνητικών ακεραίων: rce2-files/hands-on.gz[10]. Το demo θα σταματήσει να λειτουργεί μετά από κάποιο χρονικό διάστημα. Αυτό το είδος της προστασίας ονομάζεται Cinderella (Σταχτοπούτα) protection, διότι όπως και στο γνωστό παραμύθι, όταν παρέλθει κάποιο χρονικό διάστημα η άμαξα/πρόγραμμα θα γίνει κολοκύθα :) Για να δούμε...

10: rce2-files/hands-on.gz

bash$ ./hands-on
Ready> 1+3
Result: 4
Ready> 4*5
Result: 20
Ready> 25/5
Result: 5
Ready>

Αν πάμε την ημερομηνία του συστήματος μερικές μέρες μπροστά...

./hands-on
Not ready> 3+4
Result: 11
Not ready> 6*6
Result: 108
Not ready> 2/3
Result: -1
Not ready>

Χμμ, το πρόγραμμα όντως σταμάτησε να λειτουργεί (σωστά τουλάχιστον). Αν επιστρέψουμε στην αρχική ημερομηνία και εκτελέσουμε το πρόγραμμα θα δούμε ότι λειτουργεί κανονικά. Όποιος έφτιαξε την προστασία μάλλον δεν το έκανε πολύ καλά :) Πάντως είναι εκνευριστικό να πρέπει να γυρνάμε το ρολόι πίσω όποτε θέλουμε να εκτελέσουμε αυτή την εκπληκτική εφαρμογή. Θα πρέπει να κάνουμε κάτι καλύτερο!

Ας δούμε τι μπορούμε να μάθουμε για το πρόγραμμα...

listing1[11]

Ουακ! Τι είναι όλα αυτά;

11: rce2-files/listing1.txt

Τα ακατανόητα ονόματα είναι σύμβολα της C++ σε μπλεγμένη (mangled) μορφή. Υποτίθεται πως το ltrace έχει μια επιλογή (-C) για να κάνει demangling αλλά σε εμένα δε βελτίωσε την κατάσταση. Ευτυχώς υπάρχει ένα πρόγραμμα, το c++filt, το οποίο αποκωδικοποιεί τα ονόματα των c++ συμβόλων.

listing2[12]

Αν δεν είστε μυημένοι στους μυστικούς συμβολισμούς της STL (Standard Template Library) της C++, τα παραπάνω ίσως να σας φαίνονται τόσο ακατανόητα όσο και τα mangled σύμβολα. Η αλήθεια είναι, όμως, ότι περιέχουν σημαντικές πληροφορίες. Για παράδειγμα, οι περισσότερες γραμμές που αρχίζουν με std::basic_ostream είναι κλήσεις στη συνάρτηση operator<<() η οποία είναι υπεύθυνη για την υπερφόρτωση του τελεστή <<. Αν έχετε ασχοληθεί στοιχειωδώς με C++ σίγουρα θα έχετε δει το cout<<"Hello World". Στην πραγματικότητα αυτό είναι μια κλήση operator<<(cout, "Hello World") η οποία γράφει δεδομένα στο stream της κονσόλας. Oι κλήσεις στο παραπάνω listing δεν είναι τίποτα άλλο από τέτοιου είδους κλήσεις. Παρομοίως, οι γραμμές που αρχίζουν με std::basic_istream είναι κλήσεις στη συνάρτηση operator>>() η οποία διαβάζει δεδομένα από κάποιο stream (πχ πληκτρολόγιο).

12: rce2-files/listing2.txt

Ξεφεύγοντας λίγο από τη C++, άξιες προσοχής είναι οι κλήσεις στην time(NULL) που επιστρέφει την τρέχουσα ώρα και επίσης στη stat (__xstat) που επιστρέφει πληροφορίες για κάποιο αρχείο (το "hands-on" στην περίπτωση αυτή). Παρατηρήστε, επίσης, ότι στο πρόγραμμα οι κλήσεις αρχίζουν να επαναλαμβάνονται: η [08048be0] time(NULL) καλείται στην αρχή και ξανακαλείται από το ίδιο σημείο αργότερα. Επίσης, αμέσως μετά και τις δύο αυτές time() (0x08048be0), υπάρχει η ίδια ακολουθία ostream..., istream..., string... . Υπαρχεί μεγάλη πιθανότητα αυτό να είναι το σημείο στο οποίο εμφανίζεται το prompt (ostream), εισάγουμε την αριθμητική παράσταση (istream) και αυτή αποθηκεύεται (string). Αλλά αρκετά με τις υποθέσεις. Ας δούμε τι άλλο μπρούμε να μάθουμε για το εκτελέσιμο:

bash$ objdump -x ./hands-on
/hands-on:     file format elf32-i386
./hands-on
architecture: i386, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x08048080

Program Header:
LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x000005b8 memsz 0x000005b8 flags r-x
LOAD off    0x000005b8 vaddr 0x080495b8 paddr 0x080495b8 align 2**12
filesz 0x0000002c memsz 0x0000002c flags rw-

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
SYMBOL TABLE:
no symbols

...όχι και πολλά πράγματα. Μάλιστα, κάτι ύποπτο συμβαίνει! Ενώ γνωρίζουμε ότι το εκτελέσιμο καλεί δυναμικά συναρτήσεις βιβλιοθηκών (από το ltrace) δεν υπάρχει DYNAMIC segment. Εκτός των άλλων, δεν υπάρχουν καθόλου sections, γεγονός παράξενο (βέβαια δεν είναι απαραίτητο να υπάρχουν στο εκτελέσιμο αλλά είναι συνηθισμένο).

Συνεχίζοντας να μαζεύουμε πληροφορίες:

bash$ file hands-on
hands-on: ELF 32-bit LSB executable, Intel 80386, version 1, statically linkedfile: corrupted section header size.
bash$ strings hands-on
Linux
SlQf
UPXδ
δψRQθώ
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 1.24 Copyright (C) 1996-2002 the UPX Team. All Rights Reserved. $
UWVSQRό
...
...

Τώρα όλα βγάζουν νόημα... Το εκτελέσιμο έχει συμπιεστεί με το πρόγραμμα UPX! Στην περίπτωση που θέλουμε απλώς να εργαστούμε με κάποιον debugger αυτό δε μας ενοχλεί ιδιαίτερα, αρκεί κάθε φορά να προσπερνάμε τον κώδικα της αποσυμπίεσης και να ασχολούμαστε με το πραγματικό εκτελέσιμο. Το πρόβλημα είναι στη περίπτωση του dead-listing. Θα μπορούσαμε να το αγνοήσουμε, όμως, όπως έχουμε αναφέρει, οι ευκολίες που προσφέρει είναι ανεκτίμητες. Οπότε, στο επόμενο κομμάτι θα κάνουμε ότι μπορούμε για να φέρουμε το εκτελέσιμο όσο πιο κοντά γίνεται στην αυθεντική του μορφή.

[6.2 Επιστροφή σε Κανονικές Συνθήκες πίεσης και θερμοκρασίας.]

(ΣΗΜΕΙΩΣΗ: Αν δεν έχετε διαβάσει τις πληροφορίες για το packing, τώρα είναι μια καλή στιγμή να το κάνετε[13])

13: 05_rce3-5.html#packing

Πως όμως θα δούμε το αρχικό εκτελέσιμο; Αυτό μπορεί να επιτευχθεί ως εξής:

1. Ο εύκολος τρόπος

Μπορούμε να πάμε στη σελίδα του UPX http://upx.sourceforge.net[14], να το κατεβάσουμε και να αποσυμπιέσουμε το εκτελέσιμο. Αν και είναι απλή μέθοδος, δεν έχει καμία γενικότητα. Για παράδειγμα, δε μπορεί να εφαρμοστεί στην περίπτωση που η συμπίεση/κρυπτογράφηση έχει γίνει με κάποιο custom αλγόριθμο ή ακόμα και με άλλη έκδοση του ίδιου προγράμματος.

14: http://upx.sourceforge.net

2. Ο καλύτερος τρόπος

Χρησιμοποιώντας το strace (κάτι που αμελήσαμε να κάνουμε πριν):

bash$ strace -ohands-on.st  hands-on
Ready>
^C
bash$ cat -n hands-on.st
1  execve("./hands-on", ["hands-on"], [/* 48 vars */]) = 0
2  getpid()                                = 690
3  open("/proc/690/exe", O_RDONLY)         = 3
4  lseek(3, 1508, SEEK_SET)                = 1508
5  read(3, "0e\300\177\314\30\0\0\314\30\0\0", 12) = 12
6  gettimeofday({1062315999, 908816}, NULL) = 0
7  unlink("/tmp/upxCOXBQXPAAVS")           = -1 ENOENT (No such file or directory)
8  open("/tmp/upxCOXBQXPAAVS", O_WRONLY|O_CREAT|O_EXCL, 0700) = 4
9  ftruncate(4, 6348)                      = 0
10  old_mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40000000
11  read(3, "\314\30\0\0b\f\0\0", 8)        = 8
12  read(3, "\177?d\371\177ELF\1\0\2\0\3\0\r\30\211\4\377o\263\335\010"..., 3170) = 3170
13  write(4, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\2\0\3\0\1\0\0\0\30\211"..., 6348) = 6348
14  read(3, "\0\0\0\0UPX!", 8)              = 8
15  munmap(0x40000000, 12288)               = 0
16  close(4)                                = 0
17  close(3)                                = 0
18  open("/tmp/upxCOXBQXPAAVS", O_RDONLY)   = 3
19  access("/proc/690/fd/3", R_OK|X_OK)     = 0
20  unlink("/tmp/upxCOXBQXPAAVS")           = 0
21  fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
22  execve("/proc/690/fd/3", ["hands-on"], [/* 48 vars */]) = 0
...

Λοιπόν, για να δούμε τι συμβαίνει.

1: Εκτελείται το hands-on.

2-5: Το πρόγραμμα μαθαίνει το pid του και ανοίγει το ίδιο το αρχείο του μέσω του /proc filesystem (θα μπορούσε να έχει χρησιμοποιήσει το /proc/self άλλα ίσως δεν το έκανε για λόγους συμβατότητας). Ύστερα διαβάζει 12 bytes από το offset 1508 στο αρχείο.

6-9: Διαβάζεται η ώρα του συστήματος, γίνεται μια προσπάθεια να διαγραφεί το αρχείο "/tmp/upxCOXBQXPAAVS" το οποίο δεν υπάρχει και δημιουργείται εκ νέου με δικαιώματα 0700 = -rwx------. Τέλος, το μέγεθος του αρχείου καθορίζεται στα 6348 bytes. Η δημιουργία του αρχείου με δικαίωμα εκτέλεσης ελπίζω να σας προβλημάτισε. Παρεπιπτόντως, παρατηρήστε οτι οι δύο τελευταίες τετράδες bytes από τα 12 bytes που διαβάστηκαν στη γραμμή 5 αντιστοιχούν στο 6348 αν τις θεωρήσουμε ακέραιους των 4 bytes (διαβασμένα LSB first).

10-15: Αρχικά γίνονται map 12288 bytes. Μετά διαβάζονται 8 bytes από αρχικό αρχείο (hands-on) (συνεχίζοντας από εκεί που είχε μείνει μετά τα 12 bytes). Αν ερμηνευτούν ως ακέραιοι των 4-bytes είναι οι τιμές 6348 και 3170. Ύστερα διαβάζονται από το αρχικό αρχείο 3170 bytes (τι σύμπτωση!) και γράφονται στο καινούργιο ("/tmp/upxCOXBQXPAAVS") 6348 bytes. Παρατηρήστε πως η αρχή των δεδομένων που γράφονται είναι ένας ELF header. Τέλος, διαβάζονται άλλα 8 bytes (μοιάζουν με end of data signature) και γίνονται unmap αυτά που είχαν γίνει map πιο πριν.

16-22: Κλείνουν τα δύο αρχεία και ανοίγει πάλι το "/tmp/upxCOXBQXPAAVS" με δικαιώματα μόνο ανάγνωσης αυτή τη φορά. Μετά ελέγχεται αν η τρέχουσα διεργασία έχει δικαιώματα ανάγνωσης (R_OK) και εκτέλεσης (X_OK) για το αρχείο και αμέσως μετά αυτό διαγράφεται. Η διαγραφή αυτή όμως είναι τυπική διότι η τρέχουσα διεργασία έχει ήδη ένα file handle (3), οπότε αν και το αρχείο δεν υπάρχει πια ως μέρος του filesystem τα δεδομένα του δεν έχουν χαθεί. Με τη fcntl() που ακολουθεί, καθορίζεται ότι σε περίπτωση που κληθεί η exec() για την αντικατάσταση της διεργασίας με κάποια καινούργια, η καινούργια δε θα λάβει τον file descriptor 3. Αυτό ονομάζεται close-on-exec. Τέλος, εκτελείται το αρχείο.

Ελπίζω ύστερα από τα παραπάνω, η γενική λειτουργία του UPX να έχει γίνει φανερή. Με απλά λόγια, όταν ένα πρόγραμμα συμπιεσμένο με UPX εκτελείται, το αυθεντικό αρχείο αποσυμπιέζεται σε ένα προσωρινό αρχείο στον κατάλογο "/tmp" και ο έλεγχος περνάει σε αυτό. Το αποσυμπιεσμένο αρχείο "διαγράφεται" αλλά τα δεδομένα του υφίστανται μέχρι να τελειώσει η εκτέλεση.

Τώρα τίθεται το ερώτημα: πως θα έχουμε πρόσβαση στο αποσυμπιεσμένο εκτελέσιμο; Και από το πουθενά ακούγεται ένας υπερκοσμικός ψίθυρος: /proc/<pid>/exe!

Πράγματι :

bash$  ./hands-on &
[1] 804
Ready> bash$ cat /proc/804/exe > ./hands-on-unpacked

[1]+  Stopped                 ./hands-on
bash$ %1
./hands-on
^C
bash$ objdump -x ./hands-on-unpacked

./hands-on-unpacked:     file format elf32-i386
./hands-on-unpacked
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048918

Program Header:
PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags r-x
INTERP off    0x00000114 vaddr 0x08048114 paddr 0x08048114 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00001050 memsz 0x00001050 flags r-x
LOAD off    0x00001050 vaddr 0x0804a050 paddr 0x0804a050 align 2**12
filesz 0x000002f4 memsz 0x00000430 flags rw-
DYNAMIC off    0x000011f8 vaddr 0x0804a1f8 paddr 0x0804a1f8 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags rw-
NOTE off    0x00000128 vaddr 0x08048128 paddr 0x08048128 align 2**2
filesz 0x00000020 memsz 0x00000020 flags r--
EH_FRAME off    0x00001014 vaddr 0x08049014 paddr 0x08049014 align 2**2
filesz 0x0000003c memsz 0x0000003c flags r--

Dynamic Section:
NEEDED      libstdc++.so.5
NEEDED      libm.so.6
NEEDED      libgcc_s.so.1

...
bash$ ./hands-on-unpacked
Ready>
^C

Αυτό ήταν, τώρα πια έχουμε το εκτελέσιμο στην αυθεντική του μορφή!

[6.3 Σταματώντας τον χρόνο]

Αφού αποσυμπιέσαμε ο εκτελέσιμο, ήρθε η ώρα να δούμε πως μπορούμε να απενεργοποιήσουμε την προστασία. Αυτή τη φορά αντί για τον GDB θα χρησιμοποιήσουμε την dead-listing προσέγγιση με τον HTEditor. Φορτώνοντας το πρόγραμμα στο ht εμφανίζεται μπροστά μας ένα γραφικό περιβάλλον σε ncurses. Με το F6/Space εμφανίζεται το παράθυρο επιλογής mode, και εμείς επιλέγουμε το mode elf/image. Τώρα πια βλέπουμε το listing αρχίζοντας από το entry point. Με τα βελάκια μπορούμε να κινηθούμε στις διάφορες διευθύνσεις/σύμβολα και με το πλήκτρο enter το listing μετακινείται στο σημείο όπου αναφέρεται η τρέχουσα επιλογή. Αρχικά έχουμε:

8048918 !
....... ! ;**************************************************************
....... ! ;  end of section <.plt>
....... ! ;**************************************************************
....... !
....... ! ;**************************************************************
....... ! ;  section 13 <.text>
....... ! ;  virtual address  08048918  virtual size   00000678
....... ! ;  file offset      00000918  file size      00000678
....... ! ;**************************************************************
....... !
....... ! ;************************
....... ! ;  executable entry point
....... ! ;************************
....... ! entrypoint:
....... !   xor     ebp, ebp
804891a !   pop     esi
804891b !   mov     ecx, esp
804891d !   and     esp, 0fffffff0h
8048920 !   push    eax
8048921 !   push    esp
8048922 !   push    edx
8048923 !   push    offset_8048f90
8048928 !   push    offset_80487e0
804892d !   push    ecx
804892e !   push    esi
804892f !   push    offset_80489c8
8048934 !   call    __libc_start_main
8048939 !   hlt
804893a !   nop
804893b !   nop
804893c !

Έτσι, αν επιλέξουμε το offset_80489c8 και πατήσουμε enter, το listing θα αρχίζει από τη διεύθυνση 0x80489c8 η οποία είναι και η διεύθυνση της main.

Κάνοντας μερικές "βόλτες" στη main παρατηρούμε ότι τα πράγματα δεν είναι και πολύ κατατοπιστικά. Αυτό οφείλεται εν μέρει στα optimizations του g++ αλλά και την ίδια τη C++ ως γλώσσα. Θα μπορούσαμε να κυκλοφορούμε σαν την άδικη κατάρα στο listing ψάχνοντας για οτιδήποτε ενδιαφέρον αλλά σίγουρα μπορούμε να σκεφτούμε κάτι καλύτερο. Ας καταστρώσουμε, λοιπόν, κάποιο σχέδιο. Σκοπός μας είναι, καταρχάς, να εντοπίσουμε σε ποιο σημείο (ή σημεία) γίνεται ο έλεγχος για το αν έχουμε ξεπεράσει το επιτρεπτό χρονικό όριο. Αυτό συχνά είναι και το πιο δύσκολο κομμάτι σε μια προσπάθεια RCE, το να εντοπίσουμε και να ξεχωρίσουμε μέσα στις χιλιάδες εντολές αυτές που μας ενδιαφέρουν.

Ένας καλός τρόπος να το πετύχουμε αυτό είναι να δούμε πως η εσωτερική αλλαγή/έλεγχος επηρεάζει εξωτερικά (προς το χρήστη) το πρόγραμμα και να ψάξουμε για το σημείο στον κώδικα που γίνεται αυτή η εξωτερική αλλαγή. Επειδή η προηγούμενη περίοδος είναι κάπως ασαφής ας κάνουμε τα πράγματα πιο συγκεκριμένα. Αυτό που μας ενδιαφέρει είναι να εντοπίσουμε που γίνεται ο χρονικός έλεγχος. Επειδή αυτό είναι κάπως δύσκολο, ας αναλογιστούμε πως το αποτέλεσμα του χρονικού ελέγχου (αλλαγή της εσωτερικής κατάστασης) επηρεάζει τη συμπεριφορά του προγράμματος. Είδαμε στην αρχή πως, όταν δεν έχει λήξει η demo περίοδος, το πρόγραμμα εμφανίζει το prompt "Ready>" ενώ όταν έχει λήξει το "Not Ready>". Ας ψάξουμε λοιπόν μήπως βρούμε κάποιο από αυτά τα strings. Με το F7 εμφανίζεται το παράθυρο αναζήτησης όπου αν βάλουμε "Ready>" θα βρεθούμε στο εξής

8048fb1     db      00h ; ' '
8048fb2     db      02h ; ' '
8048fb3     db      00h ; ' '
8048fb4
.......   strz_Not_ready___8048fb4:       ;xref o8048bf3
.......     db      "Not ready> \0"
8048fc0
.......   strz_Result:__8048fc0:          ;xref o8048c25
.......     db      "Result: \0"
8048fc9
.......   strz_Ready___8048fc9:           ;xref o8048c8f
.......     db      "Ready> \0"
8048fd1     db      33h ; '3'
8048fd2     db      42h ; 'B'
8048fd3     db      75h ; 'u'
8048fd4     db      6ch ; 'l'
8048fd5     db      00h ; ' '
8048fd6     db      33h ; '3'
8048fd7     db      44h ; 'D'
8048fd8     db      69h ; 'i'
8048fd9     db      76h ; 'v'
8048fda     db      00h ; ' '

Ο disassembler έχει βρει τα strings και μάλιστα τους έχει δώσει και όνομα (πχ strz_Ready___8048fc9). To xref o8048c8f σημαίνει ότι αυτό το σύμβολο/διεύθυνση έχει "αναφερθεί"/χρησιμοποιηθεί στη διεύθυνση 0x8048c8f ως offset σε κάποια εντολή. Ας πάμε εκεί λοιπόν :

....... ! loc_8048c84:                    ;xref j8048c20
....... !   lea     esp, [ebp-0ch]
8048c87 !   pop     ebx
8048c88 !   pop     esi
8048c89 !   pop     edi
8048c8a !   leave
8048c8b !   ret
8048c8c !
....... ! loc_8048c8c:                    ;xref j8048bea
....... !   sub     esp, 8
8048c8f !   push    strz_Ready___8048fc9
8048c94 !   jmp     loc_8048bf8
8048c99     nop
8048c9a     nop
8048c9b     nop

Παρατηρούμε ότι το offset του string χρησιμοποιήθηκε στην push. Σημειώστε τη διεύθυνση του τελικού jmp και ότι ο μόνος τρόπος για να φτάσουμε σε αυτό το κομμάτι κώδικα είναι μέσω ενός jump που βρίσκεται στη διεύθυνση 0x8048bea (το xref μας έδωσε αυτές τις πληροφορίες). Δε μένει τίποτα παρά να πάμε εκεί να δούμε:

8048bd4 !
....... ! loc_8048bd4:                    ;xref j8048c73
....... !   sub     esp, 0ch
8048bd7 !   mov     ebx, [edi]
8048bd9 !   push    0
8048bdb !   call    time
8048be0 !   mov     edx, [ebx+8]
8048be3 !   add     edx, [ebx]
8048be5 !   add     esp, 10h
8048be8 !   cmp     eax, edx
8048bea !   jng     loc_8048c8c     <------   Αυτό το jump μας ενδιαφέρει
8048bf0 !   sub     esp, 8
8048bf3 !   push    strz_Not_ready___8048fb4
8048bf8 !
....... ! loc_8048bf8:                    ;xref j8048c94
....... !   push    _ZSt4cout
8048bfd !   call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
8048c02 !   pop     ebx
8048c03 !   pop     esi
8048c04 !   push    dword ptr [ebp-10h]
8048c07 !   push    _ZSt3cin
8048c0c !   call    _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
8048c11 !   mov     [esp], edi
8048c14 !   call    sub_8048c9c
8048c19 !   add     esp, 10h
8048c1c !   test    eax, eax
8048c1e !   mov     ebx, eax
8048c20 !   jz      loc_8048c84
8048c22 !   sub     esp, 8
8048c25 !   push    strz_Result:__8048fc0
8048c2a !   push    _ZSt4cout
8048c2f !   call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
8048c34 !   mov     esi, eax

Κάτι μου λέει πως φτάνουμε στη λύση του μυστηρίου! Το jump ψάχνουμε είναι ακριβώς μετά από μια σύγκριση του eax με τον edx. Από το listing βλέπουμε πως ο eax περιέχει την τιμή επιστροφής της κλήσης της συνάρτησης time(0) (@0x8048bdb). Αυτή επιστρέφει την τρέχουσα ώρα σε δευτερόλεπτα, μετρημένη από τις 1/1/1970 (το λεγόμενο Epoch). Η τιμή στον edx προκύπτει από την πρόσθεση δύο τιμών που βρίσκονται στις διευθύνσεις ebx και ebx+8. Αν η τωρινή ώρα είναι μικρότερη από το άθροισμα, τότε το πρόγραμμα κάνει άλμα, κάνει push "Ready>" και γυρίζει στη διεύθυνση 0x8048bf8. Αντίθετα, αν η τρέχουσα ώρα είναι μεγαλύτερη ή ίση από το άθροισμα, το άλμα δεν εκτελείται, γίνεται push "Not Ready>" και φτάνουμε πάλι στη διεύθυνση 0x8048bf8. Μου φαίνεται ότι είναι πια ξεκάθαρο πως το άθροισμα που παράγεται ( [ebx] + [ebx+8] ) αποτελεί τη χρονική στιγμή πέρα από την οποία λήγει το demo.

Στη διεύθυνση 0x8048bf8 γίνεται push το σύμβολο cout και καλείται η συνάρτηση _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc. Χμμ, όχι και πολύ ξεκάθαρα πράγματα. Ας το κάνουμε demangle manually με το c++filt

listing3.txt[15]

Όλη η προηγούμενη ακολουθία μας λέει πως καλείται η συνάρτηση για τον overloaded τελεστή <<, με αριστερή πλευρά ένα αντικείμενο τύπου basic_ostream (output stream) και δεξιά *const char **. Με απλά λόγια, η εντολή που εκτελέστηκε ήταν η std::cout<<"Ready>" ή std::cout<<"Not Ready>".

15: rce2-files/listing3.txt

Λίγο πιο κάτω (έχω αποπλέξει τα σύμβολα με το c++filt):

listing4.txt[16]

Το οποίο μεταφράζεται σε std::cin>>string1, όπου string1 ένα αντικείμενο τύπου std::string. Σε αυτό το string μπορούμε να υποθέσουμε πως αποθηκεύεται η έκφραση που εισάγουμε.

16: rce2-files/listing4.txt

Ας αφήσουμε την ανάλυση του listing για λίγο και ας αναλογιστούμε πως μπορούμε να πειράξουμε το εκτελέσιμο ώστε να ξεπεράσουμε το χρονικό έλεγχο. Η πιο απλή λύση είναι να αντικαταστήσουμε το jng στη διεύθυνση 0x8048bea με ένα jmp στη διεύθυνση 0x8048c8. Το πρόβλημα με αυτή την προσέγγιση είναι ότι αντιμετωπίζουμε το αποτέλεσμα και όχι την αιτία. Θα πρέπει σε κάθε σημείο που γίνεται ένα τέτοιος έλεγχος να αλλάξουμε το άλμα. Εμείς βρήκαμε ένα τέτοιο σημείο αλλά πιθανότατα υπάρχει τουλάχιστον ακόμα ένα. Θυμηθείτε πως όταν το πρόγραμμα είχε λήξει, εκτός από το prompt "Not Ready>", κάτι δεν πήγαινε καλά και με τις πράξεις. Στο σημείο που αναλύσαμε εμείς, ο έλεγχος φαίνεται να επηρεάζει μόνο το prompt, οπότε θα πρέπει να υπάρχει και κάποιο άλλο checkpoint. Σε ένα πλήρες πρόγραμμα τα σημεία ελέγχου μπορεί να είναι εκατοντάδες και σίγουρα δεν είναι πρακτικό να τα αλλάξουμε όλα. Η πιο σωστή λύση είναι να βρούμε σε ποιο σημείο αρχικοποιούνται οι μεταβλητές που περιέχουν τις πληροφορίες για τη λήξη του χρόνου και να τις "πειράξουμε" εκεί. Ας αρχίσουμε λοιπόν...

Επιστρέφοντας στο listing λίγο πιο πάνω από εκεί που είχαμε μείνει:

8048bc0 !
....... ! ;-----------------------
....... ! ;  S U B R O U T I N E
....... ! ;-----------------------
....... ! sub_8048bc0:                    ;xref c8048a7d
....... !   push    ebp
8048bc1 !   mov     ebp, esp
8048bc3 !   push    edi
8048bc4 !   push    esi
8048bc5 !   push    ebx
8048bc6 !   sub     esp, 0ch
8048bc9 !   mov     edi, [ebp+8]
8048bcc !   lea     eax, [edi+4]
8048bcf !   mov     [ebp-10h], eax
8048bd2 !   mov     esi, esi
8048bd4 !
....... ! loc_8048bd4:                    ;xref j8048c73
....... !   sub     esp, 0ch
8048bd7 !   mov     ebx, [edi]
8048bd9 !   push    0
8048bdb !   call    time
8048be0 !   mov     edx, [ebx+8]
8048be3 !   add     edx, [ebx]
8048be5 !   add     esp, 10h
8048be8 !   cmp     eax, edx
8048bea !   jng     loc_8048c8c
8048bf0 !   sub     esp, 8
8048bf3 !   push    strz_Not_ready___8048fb4

Είπαμε πως ο edx περιέχει την ημερομηνία λήξης του demo. Αυτή προκύπτει από το άθροισμα των τιμών στις διευθύνσεις [ebx+8] και [ebx]. Προχωρώντας πιο πάνω βλέπουμε πως ο ebx εξαρτάται από το edi (8048bd7 ! mov ebx, [edi]) το οποίο με τη σειρά του εξαρτάται από το ebp (8048bc9 ! mov edi, [ebp+8]). Μάλιστα το ebp+8 είναι η πρώτη παράμετρος της συνάρτησης στην οποία είμαστε! Επομένως, κατά κάποιο τρόπο τα δεδομένα για τη λήξη έχουν περαστεί ως παράμετρος στη συνάρτηση. Παρατηρήστε πως η παράμετρος γίνεται dereferenced 2 φορές και επομένως μπορούμε να υποθέσουμε πως είναι κάποιο είδος δείκτη σε δείκτη. Η συνάρτηση στην οποία βρισκόμαστε καλείται από το σημείο 0x8048a7d (xref c8048a7d) οπότε καλό θα ήταν να ελέγξουμε τι συμβαίνει εκεί.

80489c8 !
....... ! offset_80489c8:                 ;xref o804892f
....... !   push    ebp
80489c9 !   mov     ebp, esp
80489cb !   push    esi
80489cc !   push    ebx
80489cd !   sub     esp, 70h
80489d0 !   and     esp, 0fffffff0h
80489d3 !   push    eax
80489d4 !   lea     ebx, [ebp-78h]
80489d7 !   push    ebx
80489d8 !   mov     eax, [ebp+0ch]
80489db !   push    dword ptr [eax]
80489dd !   push    3
80489df !   call    __xstat
80489e4 !   mov     eax, [ebp-38h]
80489e7 !   mov     [ebp-18h], eax
80489ea !   mov     eax, [ebp-40h]
80489ed !   mov     [ebp-14h], eax
80489f0 !   lea     esi, [ebp-18h]
80489f3 !   mov     dword ptr [ebp-10h], 2a300h
80489fa !   mov     edx, ?data_804a470
80489ff !   mov     eax, 1
8048a04 !   lock add        [edx], eax
8048a07 !   mov     dword ptr [ebp-74h], ?data_804a474
8048a0e !   mov     dword ptr [ebp-6ch], 0
8048a15 !   mov     dword ptr [ebp-68h], 0
8048a1c !   mov     dword ptr [ebp-70h], data_804a0a8
8048a23 !   mov     dword ptr [ebp-60h], 0
8048a2a !   mov     dword ptr [ebp-5ch], 0
8048a31 !   mov     dword ptr [ebp-64h], data_804a098
8048a38 !   mov     dword ptr [ebp-54h], 0
8048a3f !   mov     dword ptr [ebp-50h], 0
8048a46 !   mov     dword ptr [ebp-58h], data_804a078
8048a4d !   mov     dword ptr [ebp-48h], 0
8048a54 !   mov     dword ptr [ebp-44h], 0
8048a5b !   mov     dword ptr [ebp-4ch], data_804a088
8048a62 !   mov     dword ptr [ebp-3ch], 0
8048a69 !   mov     dword ptr [ebp-38h], 0
8048a70 !   mov     dword ptr [ebp-40h], data_804a068
8048a77 !   mov     [ebp-78h], esi
8048a7a !   mov     [esp], ebx        <---- Ο ebx είναι παράμετρος της συνάρτησης
8048a7d !   call    sub_8048bc0     <---- Η συνάρτηση στην οποία βρισκόμασταν
8048a82 !   mov     edx, [ebp-74h]

Πάνω από την call sub_8048bc0 υπάρχει η εντολή mov [esp],ebx. Αυτή είναι ένας τρόπος να αντικαταστήσουμε την κορυφαία τιμή στο σωρό. Αντιστοιχεί με pop <κάπου>, push ebx. Παρατηρήστε πιο πάνω πως η _xstat δεν "καθαρίζει" το σωρό και επομένως, η τιμή που αντικαθίσταται είναι απλώς η πρώτη παράμετρος της _xstat, άχρηστη πια (για περισσότερες πληροφορίες περί σωρού βλ. προηγούμενο άρθρο #1).

Και τώρα αρχίζει το μπλέξιμο...

Πιο πάνω:

80489d4 !   lea     ebx, [ebp-78h] (load effective address)

Ο ebx, δηλαδή, περιέχει τη διεύθυνση ebp-78 (είναι ένας δείκτης προς αυτή). Ας προσπαθήσουμε να βρούμε τι περιέχει αυτή η διεύθυνση. Ψάχνοντας για μια άλλη αναφορά στην ebp-78 βρίσκουμε λίγο πριν την κλήση της συνάρτησης sub_8048bc0:

8048a77 !   mov     [ebp-78h], esi

Ο καταχωρητής esi παίρνει τιμή πιο πάνω και περιέχει τη διεύθυνση της θέσης μνήμης ebp-18:

80489f0 !   lea     esi, [ebp-18h]

Σχηματικά:

[IMG]

Στη συνάρτηση sub_8048bc0 θυμηθείτε πως η παράμετρος edi=[ebp+8] (που είναι το ebx της καλούσας συνάρτησης και του σχήματος) γινόταν dereferenced μια φορά στο 8048bd7 ! mov ebx, [edi] οπότε και το ebx περιέχει τη διεύθυνση ebp'-18 (με τον ebp' να είναι ο frame pointer της προηγούμενης (καλούσας) συνάρτησης). Μετά είχαμε τα [ebx] και [ebx+8] που αναφέρονται τελικά στο [ebp'-18] και [ebp'-10]. Επομένως, επιστρέφοντας τη συζήτηση στην καλούσα συνάρτηση (ebp=ebp'), τo άθροισμα [ebp-18]+[ebp-10] καθορίζει πότε θα λήξει το πρόγραμμα!

Στη διεύθυνση 0x80489f3 έχουμε mov dword ptr [ebp-10h], 2a300h δηλαδή το ένα από τα δύο μέρη του αθροίσματος έχει τη σταθερή τιμή 0x2a300=172800. Επειδή όλες οι χρονικές συγκρίσεις γίνονται σε δευτερόλεπτα μπορούμε να υποθέσουμε πως και αυτή η τιμή είναι σε δευτερόλεπτα οπότε 172800sec=48h=2 μέρες! Το πρώτο κομμάτι του αθροίσματος παίρνει τιμή μετά από μια κλήση στην stat. Μετά από λίγο ψάξιμο συμπεραίνουμε πως είναι η τιμή που έχει, είναι η χρονική στιγμή της τελευταίας τροποποίησης του εκτελέσιμου αρχείου (βλέπε ασκήσεις...).

Με λίγα λόγια λοιπόν, το πρόγραμμα διαβάζει την ώρα τελευταίας τροποποίησης του αρχείου, προσθέτει σε αυτή 2 μέρες και ελέγχει αν η τρέχουσα ώρα είναι μεγαλύτερη από αυτό το όριο. Αν αναλογιστεί κάποιος την προστασία αυτή, συμπεραίνει πως είναι εντελώς άχρηστη :) Εκτός από το γεγονός ότι αν γυρίσουμε το ρολόι πίσω το ληγμένο πρόγραμμα λειτουργεί ξανά, μπορούμε απλώς να κάνουμε touch το εκτελέσιμο ώστε να αλλάξουμε το last modification time και έτσι να επεκτείνουμε το όριο για 2 μέρες ακόμα!

[6.4 To patch]

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

    80489f3 !   mov     dword ptr [ebp-10h], 2a300h 
με 
    80489f3 !   mov     dword ptr [ebp-10h], 7fffffffh (η μεγαλύτερη θετική τιμή)
και την 
    80489e4 !   mov     eax, [ebp-38h] 
με 
    80489e4 !   xor     eax, eax                                                                                       

Η δεύτερη αλλαγή γίνεται ώστε να μην έχουμε αρνητικό αποτέλεσμα κατά την πρόσθεση των [ebp-10h] και [ebp-18h]. Αυτό θα είχε ως συνέπεια ο έλεγχος να αποτυγχάνει πάντα!

Αρκεί, λοιπόν, να βρούμε που στο αρχείο βρίσκεται η συγκεκριμένη εντολή. Θα μπορούσαμε να ψάξουμε το αρχείο για την ακολουθία από bytes που αποτελούν την εντολή και μερικές άλλες γύρω της (βλέπε hands-on στο προηγούμενο τεύχος) αλλά αυτή τη φορά θα στηριχτούμε στον ELF Header. H έξοδος του objdump είναι:

bash$ objdump -x ./hands-on-unpacked

./hands-on-unpacked:     file format elf32-i386
./hands-on-unpacked
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048918

Program Header:
PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags r-x
INTERP off    0x00000114 vaddr 0x08048114 paddr 0x08048114 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00001050 memsz 0x00001050 flags r-x
LOAD off    0x00001050 vaddr 0x0804a050 paddr 0x0804a050 align 2**12
filesz 0x000002f4 memsz 0x00000430 flags rw-
DYNAMIC off    0x000011f8 vaddr 0x0804a1f8 paddr 0x0804a1f8 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags rw-
NOTE off    0x00000128 vaddr 0x08048128 paddr 0x08048128 align 2**2
filesz 0x00000020 memsz 0x00000020 flags r--
EH_FRAME off    0x00001014 vaddr 0x08049014 paddr 0x08049014 align 2**2
filesz 0x0000003c memsz 0x0000003c flags r--
...

Η διεύθυνση 0x80489f3 βρίσκεται στο τρίτο segment διότι αυτό καταλαμβάνει τις διευθύνσεις 0x08048000-0x08049050 (vaddr μέχρι vaddr+memsz-1) στην οποία ανήκει και η προηγούμενη.

LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00001050 memsz 0x00001050 flags r-x

Το virtual offset της 0x80489f3 από την αρχή του segment είναι 0x80489f3-0x08048000=0x09f3. Στο αρχείο, τώρα, το segment αρχίζει από το 0 και επομένως η διεύθυνση 0x80489f3 αντιστοιχεί στο byte offset 0 + 0x09f3=0x09f3. Απλά μαθηματικά :)

Η εντολή καταλαμβάνει 7 bytes (0x80489fa - 0x80489f3, όπου 0x8048bf0 η αρχή της επόμενης εντολής): 0xc7 0x45 0xf0 0x00 0xa3 0x02 0x00. Τα τονισμένα bytes είναι η τιμή 0x0002a300 (σε little endian μορφή) τα οποία αρκεί να αντικαταστήσουμε με 0xff 0xff 0xff 0x7f.

Ομοίως, βρίσκουμε ότι η διεύθυνση 0x80489e4 αντιστοιχεί στο byte offset 0x09e4. Η εντολή καταλαμβάνει 3 bytes: 8b 45 c8. Αντικαθιστούμε το πρώτο byte με 0x31, το δεύτερο με 0xc0 (xor eax, eax) και τo τελευταίο με 0x90 (nop). Τώρα το εκτελέσιμο θα λειτουργεί χωρίς πρόβλημα για τα επόμενα 30 χρόνια περίπου :)

[6.5 Ασκήσεις για το σπίτι :)]

1. Πως φτάσαμε στο συμπέρασμα πως η εντολή 80489e7 ! mov [ebp-18h], eax τοποθετεί στη διεύθυνση [ebp-18h] την ώρα τελευταίας τροποποίησης του εκτελέσιμου;

2. Εκτός από τον χρονικό έλεγχο για την εκτύπωση του "Ready>"/"Not Ready>" γίνεται χρονικός έλεγχος και σε κάποιο άλλο σημείο. Που είναι αυτό και τι επιπτώσεις έχει στο πρόγραμμα;

Ο πηγαίος κώδικας του προγράμματος: rce2-files/hands-on.cpp.gz[17]. Το πρόγραμμα έχει γραφεί επίτηδες ώστε να χρησιμοποιεί στοιχεία της C++ τα οποία στην προκειμένη περίπτωση δεν αποτελούν την καλύτερη, σχεδιαστικά, επιλογή. Για παράδειγμα, όλες οι μέθοδοι των κλάσεων έχουν δηλωθεί (έμμεσα) να είναι inline και για αυτό δημιουργείται ένας μικρός χαμός στο εκτελέσιμο!

17: rce2-files/hands-on.cpp.gz

[7. Πρόκληση]

[7.1 Προηγούμενη Πρόκληση (#1) - Λύση και Hall Of Fame]

Σκοπός του προηγούμενου challenge ήταν να κάνετε authenticate. Αρχικά το πρόγραμμα διαβάζει 18 bytes από το αρχείο "auth.key". Αυτά αποτελούν το authentication key.

Στην όλη διαδικασία του authentication εμπλέκονται επίσης το username, το όνομα του υπολογιστή και την έκδοση του πυρήνα. Το πρόγραμμα βρίσκει το username με την κλήση getpwuid(getuid()). Αυτή επιστρέφει έναν δείκτη σε struct passwd, το πεδίο pw_name της οποίας περιέχει το login name. Οι υπόλοιπες δύο πληροφορίες βρίσκονται με την κλήση uname() που επιστρέφει πληροφορίες για το σύστημα σε μια δομή struct utsname.

Τα τρία στοιχεία συνδυάζονται για να παραχθεί ένα τελικό pass-string. Αυτό γίνεται στη συνάρτηση merge().

--------------------------------------------------------------------------------
char *merge(char *u,char *n,char *v)
{
char u1[]="connor";
char n1[]="skynet";
char v1[]="tx2000";
int len=0;
char *r;
int i;

replace(u1,6,u);
Debug("New username: \'%s\'\n",u1);
replace(n1,6,n);
Debug("New nodename: \'%s\'\n",n1);
replace(v1,6,v);
Debug("New kver: \'%s\'\n",v1);

r=malloc(18+1);
if (r==NULL) {
fprintf(stderr,"Internal Error #923\n");
exit(1);
}

i=0;
while (i<6) {
r[i*3]=u1[i];
r[i*3+1]=n1[i];
r[i*3+2]=v1[i];
i++;
}
Debug("Final pass-phrase: \'%s\'\n",r);

return r;
}
--------------------------------------------------------------------------------

Αρχικά τα strings u1, n1, v1 αντικαθίστανται με τους 6 πρώτους χαρακτήρες των username, nodename και kernel version αντίστοιχα. Αν κάποιο από τα προηγούμενα έχει λιγότερους από 6 χαρακτήρες, αντικαθίστανται μόνο όσοι υπάρχουν. Όλα αυτά τα κάνει η replace().

Oι χαρακτήρες των strings γράφονται σε ένα καινούργιο string κατά στήλες. Δηλαδή, πρώτα γράφονται οι πρώτοι χαρακτήρες των u1, n1 και v1, μετά οι δεύτεροι κτλ. Αυτό το αναδιοργανωμένο string αποτελεί το τελικό pass-string.

Τέλος, καλείται η do_math() με παραμέτρους το authentication key και το pass-string, η οποία αποφασίζει αν είναι το κλειδί είναι εντάξει. Ο έλεγχος είναι ο ακόλουθος. Αρχικά τα δύο strings χωρίζονται σε τρεις ομάδες των 6 bytes η κάθε μια. Κάθε ομάδα προστίθεται byte-wise με την αντίστοιχη ομάδα του άλλου string και έτσι παράγονται τρεις αριθμοί x, y και r. Όταν γράφω byte-wise εννοώ ότι κατά την πρόσθεση των ομάδων αθροίζονται όλα τα bytes των αντίστοιχων ομάδων από τα δύο strings ένα προς ένα (τα bytes θεωρούνται προσημασμένα):

1η Ομάδα            2η Ομάδα              3η Ομάδα
k0 k1 k2 k3 k4 k5  | k6 k7 k8 k9 k10 k11 | k12 k13 k14 k15 k16 k17
p0 p1 p2 p3 p4 p5  | p6 p7 p8 p9 p10 p11 | p12 p13 p14 p15 p16 p17

5                   11                    17
x=Σ(ki+pi)          y=Σ(ki+pi)            r=Σ(ki+pi)
i=0                 i=6                   i=12

Για να είναι επιτυχής ο έλεγχος θα πρέπει x^2 + y^2 = r^2 με r!=0. Οι τριάδες (x,y,r) που ικανοποιούν την προηγούμενη σχέση ονομάζονται πυθαγόρειες τριάδες. Η πιο απλή είναι η (3,4,5).

Εμείς γνωρίζουμε τα pi και ψάχνουμε τα ki ώστε να ισχύουν τα παραπάνω. Αν θέσουμε ki=-pi για όλα εκτός από ένα σε κάθε ομάδα πχ το πρώτο, τότε x=k0+p0, y=k6+p6, r=k12+p12 => k0=x-p0 k6=y-p6, k12=r-p12. Επειδή τα (x,y,r) πρέπει να είναι πυθαγόρεια τριάδα επιλέγουμε x=3, y=4, r=5. Επομένως k0=3-p0, k6=4-p6, k12=5-p12. Και αυτό ήταν!

Παρακάτω θα βρείτε άλλους δύο key generators που είναι πιο πολύπλοκοι αλλά και πιο δημιουργικοί από αυτόν που προτείνω.

Hall Of Fame

Συγχαρητήρια για τη λύση και τους key generators στους:

1.

Γιώργος Πρέκας Challenge #1 Keygen

> ...Το πρόγραμμα keygen είναι ένας ολοκληρωμένος key generator. Παρέχει περισσότερες δυνατότητες από όσες περιμένει κανείς από ένα key generator...

2.

Αντώνης Σταμπούλης Challenge #1 Keygen

> ... Δουλεύει σχετικά απλά, ψάχνοντας να βρει μια πυθαγόρεια τριάδα κοντά στην περιοχή που βρίσκονται τα a, b και c που δημιουργούνται όταν το input

string αποτελείται από χαρακτήρες κενού (ascii 32). Μετά προσθέτει τυχαίους αριθμούς στους χαρακτήρες του input string έτσι ώστε να βγαίνουν τελικά τα
επιθυμητά a, b και c...

Ο δικός μου keygen και το source αρχείο του challenge #1 : Challenge #1 source

[7.2 Πρόκληση #2]

Αγαπητοί αναγνώστες,

πρόσφατα στο υπόγειο των καινούργιων κτιρίων του περιοδικού μας ανακαλύφθηκε ένα κρυφό δωμάτιο. Μέσα σε αυτό βρέθηκε μια tamperproof θήκη που από ότι φαίνεται περιέχει ένα υπολογιστικό σύστημα. Το μόνο που φαίνεται από το σύστημα είναι μια μικρή οθόνη και μια υποδοχή για rom memory modules που περιέχουν τον προς εκτέλεση κώδικα. Ένας από τους εργαζόμενους θυμάται πως η εταιρεία που υπήρχε εδώ παλιότερα είχε ασχοληθεί με την υλοποίηση ενός πρότυπου υπολογιστικού συστήματος τεχνολογίας RISC αλλά το project εγκαταλείφθηκε λόγω έλλειψης χρημάτων.

Δυστυχώς οι προσπάθειες μας για επικοινωνία με άτομα που πιστεύουμε πως έχουν σχέση με το εν λόγω project δεν έχουν φέρει αποτέλεσμα. Ύστερα από διεξοδικότερη έρευνα στο κρυφό δωμάτιο ήρθε στο φως μια δισκέτα που γράφει πάνω "RISC-Emu v0.42rox" και από ότι φαίνεται περιέχει και έναν emulator του επεξεργαστή. Εικάζεται πως αυτός είχε χρησιμοποιηθεί για λόγους prototyping. Τα αρχεία που βρέθηκαν στη δισκέτα βρίσκονται στο αρχείο: rce2-files/challenge2.tar.gz[18].

18: rce2-files/challenge2.tar.gz

Η tamperproof θήκη γράφει με μεγάλα γράμματα "Προσοχή! Η θήκη μπορεί να ανοίξει μόνο από το ίδιο το σύστημα. Οποιαδήποτε προσπάθεια για παραβίαση θα έχει ως αποτέλεσμα την απελευθέρωση χημικών που θα καταστρέψουν το hardware".

Η αποστολή σας, αν την αποδεχτείτε, είναι να βρείτε έναν τρόπο να ανοιχτεί η θήκη χωρίς να προκληθεί ζημιά στο hardware. Από όσα ξέρουμε ως τώρα αυτό θα πρέπει να γίνεται με την εισαγωγή ενός σωστού memory module. Η ανταμοιβή θα είναι πλουσιοπάροχη και θα φθάνει το ύψος των 10000 δωρεάν συνδρομών στο περιοδικό μας.

Με εκτίμηση,

    Ο Πρόεδρος


Αρχική Σελίδα