💾 Archived View for magaz.hellug.gr › 34 › 05_rce3 › index.gmi captured on 2024-08-18 at 17:37:31. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2024-02-05)
-=-=-=-=-=-=-
Φραντζής Αλέξανδρος (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. Πρόκληση
Καλωσήρθατε στο τρίτο άρθρο (η μέτρηση αρχίζει από το 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).
Μη διστάσετε να επικοινωνήσετε μαζί μου για οποιαδήποτε διόρθωση, διευκρίνηση ή σχόλιο.
Επίσης είμαι ανοιχτός σε ιδέες για το τι θα θέλατε να περιέχουν τα επόμενα άρθρα (αν υπάρξουν, βέβαια).
Η δημιουργία ενός εκτελέσιμου είναι μια από τις πιο βασικές διαδικασίες σε οποιοδήποτε υπολογιστικό σύστημα. Την εποχή του 1950-1960 τα πράγματα ήταν σχετικά "απλά". Ο προγραμματιστής έγραφε τον αλγόριθμο σε μνημονική γλώσσα assembly και τον μετέφραζε με το χέρι σε γλώσσα μηχανής. Ύστερα τον περνούσε με κάποιο τρόπο (βύσματα, διάτρητες κάρτες) στο σύστημα και προσευχόταν όλα να πάνε καλά!
Η πρώτη προσπάθεια αυτοματοποίησης ήρθε με την δημιουργία των assemblers. Τώρα πια ο ίδιος ο υπολογιστής έκανε την κουραστική δουλειά της μετάφρασης από assembly σε γλώσσα μηχανής. Οι (τεμπέληδες :) )προγραμματιστές, όμως, δεν αρκέστηκαν σε αυτό. Ανέπτυξαν γλώσσες υψηλού επιπέδου και δημιούργησαν compilers οι οποίοι τις μετέφραζαν σε γλώσσα assembly. Οι assemblers που ήδη υπήρχαν ολοκλήρωναν τη διαδικασία αλλά τα πράγματα δε σταμάτησαν ούτε εδώ! Ακολούθησε η χρυσή εποχή του δομημένου προγραμματισμού και των modules. Αποφασίστηκε ότι ήταν σοφό να επαναχρησιμοποιείται ο κώδικας που υπήρχε ήδη και έτσι έπρεπε να βρεθεί ένας τρόπος να μπορούν να συνενώνονται κομμάτια κώδικα (σε δυαδική μορφή) που βρίσκονταν σε διαφορετικά αρχεία.
Υπάρχουν τρία βασικά είδη object αρχείων:
Το παρακάτω σχήμα δείχνει συνοπτικά τα στάδια που περνάει ένα πρόγραμμα από τη στιγμή της δημιουργίας του μέχρι την εκτέλεση.
Το παραπάνω πρόγραμμα αποτελείται από δύο modules (Relocatable Object File 1 και 2). Επιπλέον, χρησιμοποιεί δύο "βιβλιοθήκες" (Shared Object File 1 και 2). Η πρώτη συνδέεται στατικά στο πρόγραμμα μας, δηλαδή ο κώδικας της συγχωνεύεται στο τελικό object αρχείο. Η δεύτερη συνδέεται δυναμικά. Στην περίπτωση αυτή, στο στάδιο του linking δε γίνεται συγχώνευση κώδικα, αλλά εισάγονται πληροφορίες ώστε όταν φορτωθεί το πρόγραμμα ο dynamic linker να μπορέσει να βρει τις διευθύνσεις των συναρτήσεων και των δεδομένων.
Όταν ζητάμε από το λειτουργικό να εκτελέσει ένα πρόγραμμα, γίνονται πολλά περισσότερα από όσα φαίνονται εκ πρώτης όψεως. Σε γενικές γραμμές ακολουθούνται τα εξής βήματα (για τα ELF εκτελέσιμα τα πράγματα διαφέρουν λίγο):
1. Αρχικά το λειτουργικό διαβάζει τον header του εκτελέσιμου για να πάρει απαραίτητες πληροφορίες, όπως:
* Αν όντως πρόκειται για εκτελέσιμο που μπορεί να τρέξει στον υπολογιστή.
* Πόση μνήμη απαιτεί και τι ιδιότητες έχει κάθε τμήμα (segment) του εκτελέσιμου ( πχ read-only, executable κτλ).
* Ποια shared objects απαιτεί το εκτελέσιμο.
2. Το λειτουργικό αποδίδει στη διεργασία τη μνήμη που χρειάζεται και φορτώνει τα διάφορα τμήματα στη μνήμη.
3. O dynamic linker φορτώνει στο address space της διεργασίας τις βιβλιοθήκες που χρειάζεται.
4. Γίνεται relocation στο εκτελέσιμο και τις βιβλιοθήκες. Ως μέρος της διαδικασίας του relocation, διορθώνονται οι αναφορές σε συναρτήσεις/δεδομένα των βιβλιοθηκών που φορτώθηκαν. Αυτό είναι το θέμα του επόμενου τμήματος.
5. Τέλος, ο έλεγχος μεταφέρεται στο entry point του προγράμματος. Αυτό αποτελεί τη διεύθυνση της πρώτης εντολής που πρόκειται να εκτελεστεί.
Το 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 διορθώσεις.
2.
call [func1_offset] : στη διεύθυνση μνήμης func1_offset (που είναι καθορισμένη από πριν) ο dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Όλες οι κλήσεις προς αυτή τη συνάρτηση διαβάζουν τη διεύθυνση από τη συγκεκριμένη θέση μνήμης και την καλούν έμμεσα (indirect call). Έτσι αποφεύγονται οι πολλαπλές διορθώσεις, με ένα μικρό κόστος στην ταχύτητα εκτέλεσης. Τα PE (Portable Executable) format που χρησιμοποιούν τα MS Windows στηρίζεται σε αυτό το μοντέλο.
3.
call jmp_func : στη διεύθυνση jmp_func βρίσκεται μια εντολή jmp [func_offset]. Στη διεύθυνση func_offset ο dynamic linker τοποθετεί τη διεύθυνση της επιθυμητής συνάρτησης. Γιατί όλη αυτή η ταλαιπωρία; Ο μηχανισμός αυτός προσφ��ρει τη δυνατότητα οι διορθώσεις να γίνονται κατά τη διάρκεια της εκτέλεσης του προγράμματος, όταν υπάρχει ανάγκη, και όχι απαραίτητα όλες μαζί κατά τη φόρτωση του προγράμματος. Το 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 :)
Τα βασικά συστατικά που απαρτίζουν κάθε ELF object αρχείο είναι:
Σχηματικά:
Τα δύο views αποτελούν διαφορετικούς τρόπους με τους οποίους το σύστημα βλέπει ένα ELF object αρχείο. Το πρώτο (linking view) χρησιμοποιείται όταν το αρχείο πρόκειται να συνδεθεί για την παραγωγή εκτελέσιμου. Η δομική μονάδα εδώ είναι το section. Το δεύτερο (execution view) χρησιμοποιείται κατά τη φόρτωση-εκτέλεση ενός εκτελέσιμου. Ο loader δε "βλέπει" πια sections αλλά φορτώνει στη μνήμη ολόκληρα segments (ομάδες από sections).
Ο ELF Header περιέχει βασικές πληροφορίες για το object αρχείο. To πρώτο του κομμάτι (16 bytes) είναι το ELF Identification. Αυτό εκτός από τον "μαγικό αριθμό" (υπογραφή) του ELF καθορίζει
Ακολουθούν 9 padding bytes και ύστερα αρχίζει ο κυρίως header.
Το section είναι ένα τμήμα του object αρχείου το οποίο περιέχει συγκεκριμένες και ομογενείς πληροφορίες. Για παράδειγμα, ένα section μπορεί να περιέχει τον κώδικα του προγράμματος, ένα άλλο τα δεδομένα, ένα τρίτο το string table κτλ. Τα sections ενός ELF αρχείου καθορίζονται στο Section Header Table. Αν και είναι προαιρετικός στο executable object αρχεία, πάντα περιλαμβάνεται (από όσο έχω δει). Το Section Header Table αποτελείται από μια σειρά από περιγραφείς, καθένας από τους οποίους μας δίνει πληροφορίες για ένα section:
Οι πληροφορίες που μας παρέχει η παραπάνω δομή είναι:
Υπάρχουν κάποια sections τα οποία κατά σύμβαση περιέχουν συγκεκριμένες πληροφορίες. Μια πλήρης λίστα μπορεί να βρεθεί στο standard. Τα πιο σημαντικά και κοινά είναι:
Τα τρία sections .strtab, .dynstr και .shstrtab περιέχουν strings τα οποία χρησιμοποιούνται από κάποια άλλα sections. Η δομή τους είναι αρκετά απλή: Το πρώτο byte του section είναι '\0' και από εκεί και πέρα ακολουθεί μια σειρά από null-terminated strings. Τα strings καθορίζονται από το offset τους από την αρχή του section.
Για το παραπάνω table έχουμε:
index/offset String 1 "alf" 2 "lf" 5 "tx" κτλ
Τα segments αποτελούνται από ένα ή περισσότερα sections τα οποία κατά τη φόρτωση του εκτελέσιμου/shared object αρχείου έχουν κοινές ιδιότητες. Για κάθε segment υπάρχει μια καταχώρηση στο Program Header Table:
Σε γενικές γραμμές ακολουθούνται τα εξής βήματα:
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 του εκτελέσιμου.
Τα 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 τιμή.
Η δομή του 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) και έτσι μπορεί να χρησιμοποιηθεί σε οποιαδήποτε άλλη εφαρμογή.
Μια πιο εύχρηστη λύση είναι o HT Editor (Site: http://hte.sourceforge.net[4]). Παρέχει πλήρη διαχείριση του object αρχείου και έναν αρκετά καλό disassembler. Θα αναφερθούμε λίγο περισσότερο σε αυτόν στο κομμάτι για το dead-listing[5].
5: 05_rce3-5.html#dead-listing
To /proc είναι ένα εικονικό σύστημα αρχείων το οποίο μας δίνει τη δυνατότητα να πάρουμε πληροφορίες από τις δομές δεδομένων του πυρήνα. Είναι εικονικό με την έννοια ότι τα αρχεία που βλέπουμε δεν έχουν κάποια φυσική υπόσταση (πχ δεν βρίσκονται σε κάποια συσκευή). Τα περισσότερα αρχεία μπορούν να ανοιχτούν μόνο για ανάγνωση. Με ένα "man proc" θα λάβετε ότι πληροφορίες για το /proc θέλετε και δε θέλετε να μάθετε :)
Στο βασικό κατάλογο /proc υπάρχει ένα πλήθος από αρχεία και καταλόγους. Κάποια από αυτά περιέχουν ολόκληρες δομές πληροφοριών, ενώ άλλα απλώς την τιμή μιας συγκεκριμένης μεταβλητής του πυρήνα. Τα περισσότερα αρχεία έχουν ονόματα που αυτοεξηγούνται. Κάποια κύρια είναι:
Αν και το /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!
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); }
--------------------------------------------------------------------------------
Ένα από τα καλύτερα εργαλεία για disassemby είναι ο IDA (Interactive DisAssembler). Υπάρχει μια freeware έκδοση που δουλεύει σε DOS, ενώ η εμπορική υπάρχει και για περιβάλλον Windows. Υποστηρίζει πλήθος αρχιτεκτονικών, εκτελέσιμων και compilers, ενώ περιέχει μια script γλώσσα για διάφορες αυτοματοποιήσεις. Στο linux μπορεί να εκτελεστεί μέσω wine. Το επίσημο site είναι http://www.datarescue.com[6], ενώ τη freeware εκδοση μπορείτε να τη βρείτε με μια αναζήτηση στο google.
.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
Στον κόσμο του 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 είναι ένας 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].
....... ! ;**************************************************** ....... ! ; 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 είναι ένα πρόγραμμα που χρησιμοποιεί και επεκτείνει την έξοδο του 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)
Τα εκτελέσιμα αρχεία, όπως και όλα τα αρχεία, περιέχουν μέσα τους επαναλήψεις που καθιστούν δυνατή τη συμπίεση τους. Η συμπίεση στα εκτελέσιμα, εφόσον η αποσυμπίεση μπορεί να εκτελεστεί αρκετά γρήγορα ώστε να μη γίνεται αισθητή, είναι σίγουρα επιθυμητή. Τα αρχεία καταλαμβάνουν λιγότερο χώρο και επίσης είναι πιο δύσκολο να ερευνηθούν και να αλλαχτούν (βέβαια, για όσους κάνουμε RCE αυτό είναι μεγάλο πρόβλημα). Μάλιστα, πολλά προγράμματα συμπίεσης εκτελέσιμων εφαρμόζουν και άλλες τεχνικές, όπως κρυπτογράφηση και CRC ελέγχους. Είναι σαφές πως δε βολεύει απλώς να συμπιεστεί το αρχείο με κάποια παραδοσιακή μέθοδο (πχ με το gzip). Αυτό συμβαίνει, διότι εκτός από το ότι το εκτελέσιμο δεν είναι πια εκτελέσιμο (η κατάσταση διορθώνεται με χρήση scripts για αυτόματη αποσυμπίεση, το utility gzexe λειτουργεί έτσι), χάνεται η προστασία από το RCE, αφού τελικά το εκτελέσιμο θα βρεθεί στην αρχική του μορφή πριν εκτελεστεί. Για αυτό, έχουν αναπτυχθεί διάφορες άλλες τεχνικές για συμπίεση προγραμμάτων.
Καταρχάς θα δούμε την απλή packing τεχνική που αναφέραμε στην εισαγωγή . Η βασική της λειτουργία φαίνεται στο παρακάτω σχήμα:
Ο packer συμπιέζει όλο το εκτελέσιμο και δημιουργεί ένα καινούργιο που περιλαμβάνει τα συμπιεσμένα δεδομένα και τον κώδικα αποσυμπίεσης. Ο κώδικα αποσυμπιέζει τα συμπιεσμένα δεδομένα (αρχικό εκτελέσιμο), τα σώζει σε ένα προσωρινό αρχείο, και μετά το εκτελεί συνήθως με exec(). Η αδυναμία του βρίσκεται στο γεγονός πως εμφανίζεται το αυθεντικό εκτελέσιμο στο δίσκο, και επομένως δεν παρέχει ιδιαίτερη προστασία. Το θετικό στοιχείο του είναι η απλότητα.
Μια πιo εξελιγμένη τεχνική :
Εδώ τα πράγματα είναι πιο ενδιαφέροντα. Δε συμπιέζεται όλο το εκτελέσιμο αλλά μόνο τα segments του. Το καινούργιο εκτελέσιμο περιέχει τα συμπιεσμένα δεδομένα και τον κώδικα για το unpacking (κατά προτίμηση μετά τα δεδομένα).
Κατά την εκτέλεση φορτώνονται τα συμπιεσμένα segments στη μνήμη, με τέτοιο τρόπο, ώστε όταν αποσυμπιεστούν να έχουν τις αρχικές διευθύνσεις τους. Επίσης, ο unpacker πρέπει να κάνει και κάτι άλλο που δεν είναι φανερό με την πρώτη ματιά. Αν το αυθεντικό εκτελέσιμο χρησιμοποιούσε shared objects (βιβλιοθήκες), τότε μέσα στο αρχείο υπήρχαν οι πληροφορίες, ώστε ο dynamic linker να τα φορτώσει. Όμως το συμπιεσμένο εκτελέσιμο έχει διαφορετικές πληροφορίες και έτσι τα shared objects δε φορτώνονται. Θα πρέπει ο unpacker να επωμιστεί αυτό το βάρος και επιπλέον να διορθώσει τις αναφορές στα εξωτερικά σύμβολα. Αυτό γίνεται με χρήση των συναρτήσεων dlopen() και dlsym() που χρησιμοποιούνται για να φορτώνουν shared objects στο run-time (δείτε manpages). Ο unpacker, αφού ολοκληρώσει όλες τις εργασίες του, θα μεταφέρει τον έλεγχο στο OEP (original entry point) και έτσι θα αρχίσει το κυρίως πρόγραμμα.
Στην πιο απλή περίπτωση, όπου ο 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 εκτελέσιμο μόνο στο δικό μας σύστημα και με τι ίδιες ακριβώς βιβλιοθήκες, ίσως να τη γλιτώσουμε.
Αυτή τη φορά θα ασχοληθούμε με το demo του εκπληκτικού προγράμματος για πράξεις μη αρνητικών ακεραίων: rce2-files/hands-on.gz[10]. Το demo θα σταματήσει να λειτουργεί μετά από κάποιο χρονικό διάστημα. Αυτό το είδος της προστασίας ονομάζεται Cinderella (Σταχτοπούτα) protection, διότι όπως και στο γνωστό παραμύθι, όταν παρέλθει κάποιο χρονικό διάστημα η άμαξα/πρόγραμμα θα γίνει κολοκύθα :) Για να δούμε...
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]
Ουακ! Τι είναι όλα αυτά;
Τα ακατανόητα ονόματα είναι σύμβολα της 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 (πχ πληκτρολόγιο).
Ξεφεύγοντας λίγο από τη 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. Θα μπορούσαμε να το αγνοήσουμε, όμως, όπως έχουμε αναφέρει, οι ευκολίες που προσφέρει είναι ανεκτίμητες. Οπότε, στο επόμενο κομμάτι θα κάνουμε ότι μπορούμε για να φέρουμε το εκτελέσιμο όσο πιο κοντά γίνεται στην αυθεντική του μορφή.
(ΣΗΜΕΙΩΣΗ: Αν δεν έχετε διαβάσει τις πληροφορίες για το packing, τώρα είναι μια καλή στιγμή να το κάνετε[13])
Πως όμως θα δούμε το αρχικό εκτελέσιμο; Αυτό μπορεί να επιτευχθεί ως εξής:
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
Αυτό ήταν, τώρα πια έχουμε το εκτελέσιμο στην αυθεντική του μορφή!
Αφού αποσυμπιέσαμε ο εκτελέσιμο, ήρθε η ώρα να δούμε πως μπορούμε να απενεργοποιήσουμε την προστασία. Αυτή τη φορά αντί για τον 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>".
Λίγο πιο κάτω (έχω αποπλέξει τα σύμβολα με το c++filt):
listing4.txt[16]
Το οποίο μεταφράζεται σε std::cin>>string1, όπου string1 ένα αντικείμενο τύπου std::string. Σε αυτό το string μπορούμε να υποθέσουμε πως αποθηκεύεται η έκφραση που εισάγουμε.
Ας αφήσουμε την ανάλυση του 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]
Σχηματικά:
Στη συνάρτηση 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 μέρες ακόμα!
Αν θέλουμε να πειράξουμε το πρόγραμμα για να λειτουργεί για πάντα (σχεδόν...), μια επιλογή είναι να αντικαταστήσουμε την
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 χρόνια περίπου :)
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
Σκοπός του προηγούμενου 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 που είναι πιο πολύπλοκοι αλλά και πιο δημιουργικοί από αυτόν που προτείνω.
Συγχαρητήρια για τη λύση και τους 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
Αγαπητοί αναγνώστες,
πρόσφατα στο υπόγειο των καινούργιων κτιρίων του περιοδικού μας ανακαλύφθηκε ένα κρυφό δωμάτιο. Μέσα σε αυτό βρέθηκε μια 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 δωρεάν συνδρομών στο περιοδικό μας.
Με εκτίμηση,
Ο Πρόεδρος