1631 γραμμές
152 KiB
Markdown
1631 γραμμές
152 KiB
Markdown
+++
|
||
title = 'Reverse Engineering σε περιβάλλον Linux, Μέρος 2'
|
||
date = '2003-09-01T00:00:00Z'
|
||
description = ''
|
||
author = 'Φραντζής Αλέξανδρος (aka Alf) alf82 at freemail dot gr'
|
||
issue = ['Magaz 34']
|
||
issue_weight = 5
|
||
+++
|
||
|
||
----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
*To άρθρο αυτό είναι το τρίτο της σειράς \"Reverse Engineering σε περιβάλλον Linux\". Σκοπός της σειράς είναι να εξοικοιώσει τους αναγνώστες με τις βασικές
|
||
τεχνικές του Reverse Enineering, με έμφαση στο πως αυτές μπορούν να εφαρμοστούν στο Linux, και να τους προσφέρει πιο βαθιές γνώσεις για τη λειτουργία του
|
||
συστήματος τους. Στο συγκεκριμένο άρθρο θα ασχοληθούμε κυρίως με τη διαδικασία της εκτέλεσης των προγραμμάτων και τις πληροφορίες που μπορούμε να αποκομίσουμε
|
||
από αυτά.*
|
||
|
||
----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
**1. Εισαγωγή**
|
||
--------------------------------------
|
||
|
||
**2. Διαδικασία Δημιουργίας και Φόρτωσης Εκτελέσιμου**
|
||
-----------------------------------------------------------------------------
|
||
|
||
- [2.1 Εισαγωγή](#ss2.1)
|
||
- [2.2 Η πορεία ενός bit: Από την πηγαίο κώδικα στο τελικό εκτελέσιμο](#ss2.2)
|
||
- [2.3 Το φόρτωμα του προγράμματος](#ss2.3)
|
||
- [2.4 Relocation - Εναλλακτικοί τρόποι για τη διόρθωση των αναφορών στο dynamic linking.](#ss2.4)
|
||
|
||
**3. Το ELF**
|
||
------------------------------------
|
||
|
||
- [3.1 Τι είναι το ELF;](#ss3.1)
|
||
- [3.2 Βασική δομή του ELF](#ss3.2)
|
||
- [3.3 Ο ELF Header](#ss3.3)
|
||
- [3.4 Τα ELF Sections](#ss3.4)
|
||
- [3.5 Τα ELF Segments](#ss3.5)
|
||
- [3.6 Εκτέλεση ενός ELF executable και dynamic linking](#ss3.6)
|
||
- [3.7 Εργαλεία για το ELF](#ss3.7)
|
||
|
||
**4. Το /proc filesystem**
|
||
-------------------------------------------------
|
||
|
||
- [4.1 Τι είναι το /proc filesystem](#ss4.1)
|
||
- [4.2 Γενικές πληροφορίες για το σύστημα](#ss4.2)
|
||
- [4.3 Πληροφορίες για μια διεργασία](#ss4.3)
|
||
|
||
**5. Χρήσιμες έως πολύ χρήσιμες πληροφορίες ΙΙ**
|
||
-----------------------------------------------------------------------
|
||
|
||
- [5.1 Dead Listing και Εργαλεία](#ss5.1)
|
||
- [5.2 Packed Executables - Συμπιεσμένα Εκτελέσιμα](#ss5.2)
|
||
|
||
**6. Hands-on Παράδειγμα - Υπό πίεση**
|
||
-------------------------------------------------------------
|
||
|
||
- [6.1 Πράξη 1η](#ss6.1)
|
||
- [6.2 Επιστροφή σε Κανονικές Συνθήκες πίεσης και θερμοκρασίας.](#ss6.2)
|
||
- [6.3 Σταματώντας τον χρόνο](#ss6.3)
|
||
- [6.4 To patch](#ss6.4)
|
||
- [6.5 Ασκήσεις για το σπίτι :)](#ss6.5)
|
||
|
||
**7. Πρόκληση**
|
||
--------------------------------------
|
||
|
||
- [7.1 Προηγούμενη Πρόκληση (\#1) - Λύση και Hall Of Fame](#ss7.1)
|
||
- [7.2 Πρόκληση \#2](#ss7.2)
|
||
|
||
|
||
### [1. Εισαγωγή]{#s1}
|
||
|
||
Καλωσήρθατε στο τρίτο άρθρο (η μέτρηση αρχίζει από το 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. Διαδικασία Δημιουργίας και Φόρτωσης Εκτελέσιμου]{#s2}
|
||
|
||
### [2.1 Εισαγωγή]{#ss2.1}
|
||
|
||
Η δημιουργία ενός εκτελέσιμου είναι μια από τις πιο βασικές διαδικασίες σε οποιοδήποτε υπολογιστικό σύστημα. Την εποχή του 1950-1960 τα πράγματα ήταν σχετικά
|
||
\"απλά\". Ο προγραμματιστής έγραφε τον αλγόριθμο σε μνημονική γλώσσα assembly και τον μετέφραζε με το χέρι σε γλώσσα μηχανής. Ύστερα τον περνούσε με κάποιο
|
||
τρόπο (βύσματα, διάτρητες κάρτες) στο σύστημα και προσευχόταν όλα να πάνε καλά!
|
||
|
||
Η πρώτη προσπάθεια αυτοματοποίησης ήρθε με την δημιουργία των assemblers. Τώρα πια ο ίδιος ο υπολογιστής έκανε την κουραστική δουλειά της μετάφρασης από
|
||
assembly σε γλώσσα μηχανής. Οι (τεμπέληδες :) )προγραμματιστές, όμως, δεν αρκέστηκαν σε αυτό. Ανέπτυξαν γλώσσες υψηλού επιπέδου και δημιούργησαν compilers οι
|
||
οποίοι τις μετέφραζαν σε γλώσσα assembly. Οι assemblers που ήδη υπήρχαν ολοκλήρωναν τη διαδικασία αλλά τα πράγματα δε σταμάτησαν ούτε εδώ! Ακολούθησε η χρυσή
|
||
εποχή του δομημένου προγραμματισμού και των modules. Αποφασίστηκε ότι ήταν σοφό να επαναχρησιμοποιείται ο κώδικας που υπήρχε ήδη και έτσι έπρεπε να βρεθεί ένας
|
||
τρόπος να μπορούν να συνενώνονται κομμάτια κώδικα (σε δυαδική μορφή) που βρίσκονταν σε διαφορετικά αρχεία.
|
||
|
||
### [2.2 Η πορεία ενός bit: Από την πηγαίο κώδικα στο τελικό εκτελέσιμο]{#ss2.2}
|
||
|
||
Υπάρχουν τρία βασικά είδη object αρχείων:
|
||
|
||
- Relocatable Object File: Περιέχουν δεδομένα και κώδικα και είναι κατάλληλα για τη δημιουργία executable ή shared object αρχείων με τη διαδικασία του linking
|
||
(Κατάληξη: \".o\", \".obj\" )
|
||
- Executable Object File: Αρχεία κατάλληλα για εκτέλεση (Κατάληξη: στα Unix συστήματα συνήθως καμία, \".bin\", \".exe\")
|
||
- Shared Object File: Μπορούν να γίνουν link με άλλα Shared Object ή Relocatable Object Files για να δημιουργήσουν Executable Files Επιπλέον μπορούν να
|
||
συνδεθούν δυναμικά με το εκτελέσιμο κατά τη διάρκεια της φόρτωσης του. (Κατάληξη: \".so\", \".dll\") ????
|
||
|
||
Το παρακάτω σχήμα δείχνει συνοπτικά τα στάδια που περνάει ένα πρόγραμμα από τη στιγμή της δημιουργίας του μέχρι την εκτέλεση.
|
||
|
||

|
||
|
||
Το παραπάνω πρόγραμμα αποτελείται από δύο modules (Relocatable Object File 1 και 2). Επιπλέον, χρησιμοποιεί δύο \"βιβλιοθήκες\" (Shared Object File 1 και 2). Η
|
||
πρώτη συνδέεται στατικά στο πρόγραμμα μας, δηλαδή ο κώδικας της συγχωνεύεται στο τελικό object αρχείο. Η δεύτερη συνδέεται δυναμικά. Στην περίπτωση αυτή, στο
|
||
στάδιο του linking δε γίνεται συγχώνευση κώδικα, αλλά εισάγονται πληροφορίες ώστε όταν φορτωθεί το πρόγραμμα ο dynamic linker να μπορέσει να βρει τις
|
||
διευθύνσεις των συναρτήσεων και των δεδομένων.
|
||
|
||
### [2.3 Το φόρτωμα του προγράμματος]{#ss2.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.]{#ss2.4}
|
||
|
||
Το 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 χρησιμοποιεί μια παραλλαγή του μοντέλου αυτού και
|
||
θα το εξετάσουμε αναλυτικότερα παρακάτω.
|
||
|
||

|
||
|
||
|
||
### [3. Το ELF]{#s3}
|
||
|
||
### [3.1 Τι είναι το ELF;]{#ss3.1}
|
||
|
||
Το Εxecutable and Linking Format (ELF) αποτελεί το format που χρησιμοποιεί το linux για τα object αρχεία του. Υποστηρίζει μια πληθώρα αρχιτεκτονικών και για
|
||
αυτό είναι ιδανικό για ένα multi-platform λειτουργικό όπως το linux. Παρακάτω θα γίνει μια περιγραφή των βασικών στοιχείων του ELF. Για μια πιο αναλυτική
|
||
περιγραφή ανατρέξτε στο standard (pdf): [ELF 1.1](http://www.nondot.org/sabre/os/files/Executables/ELF.pdf), [ELF
|
||
1.2](http://x86.ddj.com/ftp/manuals/tools/elf.pdf)
|
||
|
||
Αυτό το ELF δεν έχει καμία σχέση με τον Tolkien :)
|
||
|
||
### [3.2 Βασική δομή του ELF]{#ss3.2}
|
||
|
||
Τα βασικά συστατικά που απαρτίζουν κάθε ELF object αρχείο είναι:
|
||
|
||
- O ELF header
|
||
- Το Program Header Table (optional στα Relocatable Object Files)
|
||
- Τα sections και τα segments. Τα segments αποτελούνται από ένα ή παραπάνω sections.
|
||
- To Section Header Table (optional στα Executable Object Files)
|
||
|
||
Σχηματικά:
|
||
|
||

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

|
||
|
||
Ο ELF Header περιέχει βασικές πληροφορίες για το object αρχείο. To πρώτο του κομμάτι (16 bytes) είναι το ELF Identification. Αυτό εκτός από τον \"μαγικό
|
||
αριθμό\" (υπογραφή) του ELF καθορίζει
|
||
|
||
- **File Class**: Aν το εκτελέσιμο είναι 32-bit ή 64-bit (πάνε τα 16-bit, πόσο μάλλον τα 8-bit!).
|
||
- **Data Encoding**: Aν τα δεδομένα είναι σε LSB (little-endian) ή MSB (big-endian) μορφή.
|
||
- **Header Ver \#**: Την έκδοση του ELF Header.
|
||
|
||
Ακολουθούν 9 padding bytes και ύστερα αρχίζει ο κυρίως header.
|
||
|
||
- **Object File Type** : Το είδος του object file (relocatable, executable, shared object).
|
||
- **Required Architecture** : Η αρχιτεκτονική στην οποία πρέπει να είναι στηριγμένος ο επεξεργαστής ώστε να μπορεί να λειτουργήσει το πρόγραμμα (Intel 80386,
|
||
SPARC, MIPS RS3000 κτλ)
|
||
- **Object File Version \#** : H εκδοση του ELF.
|
||
- **Process Entry point** : H εικονική διεύθυνση της πρώτης εντολής του προγράμματος η οποία θα εκτελεστεί.
|
||
- **Program Header Table Offset** : To offset του PHT στο object αρχείο.
|
||
- **Section Header Table Offset** : To offset του SHT στο object αρχείο.
|
||
- **Processor Specific Flags** : Σημαίες που αφορούν σε ιδιότητες του εκάστοτε επεξεργαστή.
|
||
- **ELF Header Size** : To μέγεθος του ELF Header σε bytes.
|
||
- **PHT Entry Size** : Το μέγεθος μιας καταχώρησης στο Program Header Table.
|
||
- **Number of PHT entries** : Ο αριθμός των καταχωρήσεων στο Program Header Table σε bytes.
|
||
- **SHT Entry Size** : Το μέγεθος μιας καταχώρησης στο Section Header Table σε bytes .
|
||
- **Number of SHT entries** : Ο αριθμός των καταχωρήσεων στο Section Header Table.
|
||
- **Section Name String Table SHT Index** : Το index στο SHT που καθορίζει ποιο section περιέχει το string table με τα ονόματα των sections.
|
||
|
||
### [3.4 Τα ELF Sections]{#ss3.4}
|
||
|
||
Το section είναι ένα τμήμα του object αρχείου το οποίο περιέχει συγκεκριμένες και ομογενείς πληροφορίες. Για παράδειγμα, ένα section μπορεί να περιέχει τον
|
||
κώδικα του προγράμματος, ένα άλλο τα δεδομένα, ένα τρίτο το string table κτλ. Τα sections ενός ELF αρχείου καθορίζονται στο Section Header Table. Αν και είναι
|
||
προαιρετικός στο executable object αρχεία, πάντα περιλαμβάνεται (από όσο έχω δει). Το Section Header Table αποτελείται από μια σειρά από περιγραφείς, καθένας
|
||
από τους οποίους μας δίνει πληροφορίες για ένα section:
|
||
|
||

|
||
|
||
Οι πληροφορίες που μας παρέχει η παραπάνω δομή είναι:
|
||
|
||
- **Section Name String Table Index** : To offset του ονόματος του section στο string table.
|
||
- **Section Type** : Το είδος του section (πχ program-defined, symbol table, dynamic linking info).
|
||
- **Section Flags** : Σημαίες που καθορίζουν διάφορα χαρακτηριστικά του section (πχ allocation (αν το section φορτώνεται στη μνήμη κατα την εκτέλεση), write,
|
||
execute).
|
||
- **Section Virtual Address** : Η εικονική διεύθυνση στην οποία θα φορτωθεί το section στη μνήμη ή 0 αν δεν φορτώνεται.
|
||
- **Section File Offset** : Το offset στο οποίο αρχίζει το section στο object αρχείο.
|
||
- **Section Size** : Το μέγεθος του section σε bytes
|
||
- **Section Header Table Index Link** : Κάποια sections χρειάζονται πληροφορίες από κάποιο άλλο section. To πεδίο καθορίζει το index του απαραίτητου section
|
||
στο section header table. Χαρακτηριστικό παράδειγμα είναι τα sections που περιέχουν πληροφορίες για το dynamic linking. Σε αυτά, το πεδίο καθορίζει σε ποιo
|
||
string table section βρίσκονται τα ονόματα του εξωτερικών συμβόλων.
|
||
- **Additional Section Info** : Επιπλέον πληροφορίες, η σημασία των οποίων εξαρτάται από το είδος του section.
|
||
- **Section Address Alignment** : Καθορίζει σε τι όρια πρέπει να φορτωθεί το section.
|
||
- **Fixed-Size Entry Size** : Αν το section αποτελείται από κάποιον πίνακα στον οποίο κάθε καταχώρηση έχει σταθερό μέγεθος, το πεδίο αυτό δίνει το μέγεθος της
|
||
κάθε καταχώρησης (πχ το symbol table section)
|
||
|
||
Υπάρχουν κάποια sections τα οποία κατά σύμβαση περιέχουν συγκεκριμένες πληροφορίες. Μια πλήρης λίστα μπορεί να βρεθεί στο standard. Τα πιο σημαντικά και κοινά
|
||
είναι:
|
||
|
||
- **.text** : Περιέχει τον εκτελέσιμο κώδικα του προγράμματος.
|
||
- **.data** : Περιέχει τα αρχικοποιημένα δεδομένα του προγράμματος (πχ global μεταβλητές στις οποίες έχουμε δώσει αρχική τιμή).
|
||
- **.bss** : Περιέχει μη αρχικοποιημένα δεδομένα του προγράμματος (πχ global μεταβλητές στις οποίες δεν έχουμε δώσει αρχική τιμή). Το section αυτό δεν
|
||
καταλαμβάνει χώρο στο αρχείο αλλά δημιουργείται μόνο στη μνήμη, όπου όλα τα bytes του μηδενίζονται.
|
||
- **.dynamic** : Περιέχει πληροφορίες απαραίτητες για το dynamic linking.
|
||
- **.strtab** : Περιέχει strings που χρησιμοποιούνται κυριώς από το symbol table.
|
||
- **.dynstr** : Περιέχει τα strings σχετικά με το dynamic linking.
|
||
- **.shstrtab** : Περιέχει τα strings των ονομάτων των sections.
|
||
- **.symtab** : Περιέχει τo symbol table για το αρχείο.
|
||
- **.dynsym** : Περιέχει τo symbol table για τα σύμβολα που χρειάζονται για το dynamic linking.
|
||
- **.plt** : Περιέχει τo procedure linkage table. Θα ασχοληθούμε με αυτό αργότερα.
|
||
- **.interp** : Περιέχει το path του interpreter που θα φορτώσει το εκτελέσιμο.
|
||
|
||
#### String Table Section
|
||
|
||
Τα τρία 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"
|
||
κτλ
|
||
|
||
### [3.5 Τα ELF Segments]{#ss3.5}
|
||
|
||
Τα segments αποτελούνται από ένα ή περισσότερα sections τα οποία κατά τη φόρτωση του εκτελέσιμου/shared object αρχείου έχουν κοινές ιδιότητες. Για κάθε segment
|
||
υπάρχει μια καταχώρηση στο Program Header Table:
|
||
|
||

|
||
|
||
- **Segment Type** : To είδος του segment και των πληροφοριών που περιέχει (πχ loadable(θα φορτωθεί στη μνήμη), dynamic linking info)
|
||
- **Segment File offset** : Το offset της αρχής του segment στο αρχείο.
|
||
- **Segment Virtual Address** : Η εικονική διεύθυνση στην οποία θα φορτωθεί το segment στη μνήμη.
|
||
- **Segment Physical Address** : Η φυσική διεύθυνση μνήμη στην οποία θα φορτωθεί το segment. Προφανώς τα περισσότερα συστήματα αγνοούν αυτό το πεδίο.
|
||
- **Segment File Image Size** : To μέγεθος που καταλαμβάνει το segment στο αρχείο.
|
||
- **Segment Memory Image Size** : To μέγεθος που θα καταλαμβάνει το segment στη μνήμη (\>=Segment File Image Size)
|
||
- **Segment Flags** : Σημαίες που καθορίζουν ιδιότητες του segment.
|
||
- **Segment Alignment** : Καθορίζει σε τι όρια πρέπει να φορτωθεί το segment.
|
||
|
||
### [3.6 Εκτέλεση ενός ELF executable και dynamic linking]{#ss3.6}
|
||
|
||
#### Φόρτωση - Εκτέλεση
|
||
|
||
Σε γενικές γραμμές ακολουθούνται τα εξής βήματα:
|
||
|
||
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]{#ss3.7}
|
||
|
||
Η δομή του 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>). Πρόκειται για μια scripting γλώσσα που μας επιτρέπει να διαχειριστούμε
|
||
ένα ELF αρχείο. Οι δυνατότητές του ποικίλουν από μια απλή απεικόνιση των πληροφοριών του header, μέχρι και συγχώνευση δύο object αρχείων! Η \"ψυχή\" του
|
||
προγράμματος βρίσκεται σε μια καλοσχεδιασμένη βιβλιοθήκη (libelfsh) και έτσι μπορεί να χρησιμοποιηθεί σε οποιαδήποτε άλλη εφαρμογή.
|
||
|
||
Μια πιο εύχρηστη λύση είναι o **HT Editor** (Site: <http://hte.sourceforge.net>). Παρέχει πλήρη διαχείριση του object αρχείου και έναν αρκετά καλό disassembler.
|
||
Θα αναφερθούμε λίγο περισσότερο σε αυτόν στο κομμάτι για το [dead-listing](05_rce3-5.html#dead-listing).
|
||
|
||
|
||
### [4. Το /proc filesystem]{#s4}
|
||
|
||
### [4.1 Τι είναι το /proc filesystem]{#ss4.1}
|
||
|
||
To /proc είναι ένα εικονικό σύστημα αρχείων το οποίο μας δίνει τη δυνατότητα να πάρουμε πληροφορίες από τις δομές δεδομένων του πυρήνα. Είναι εικονικό με την
|
||
έννοια ότι τα αρχεία που βλέπουμε δεν έχουν κάποια φυσική υπόσταση (πχ δεν βρίσκονται σε κάποια συσκευή). Τα περισσότερα αρχεία μπορούν να ανοιχτούν μόνο για
|
||
ανάγνωση. Με ένα \"man proc\" θα λάβετε ότι πληροφορίες για το /proc θέλετε και δε θέλετε να μάθετε :)
|
||
|
||
### [4.2 Γενικές πληροφορίες για το σύστημα]{#ss4.2}
|
||
|
||
Στο βασικό κατάλογο /proc υπάρχει ένα πλήθος από αρχεία και καταλόγους. Κάποια από αυτά περιέχουν ολόκληρες δομές πληροφοριών, ενώ άλλα απλώς την τιμή μιας
|
||
συγκεκριμένης μεταβλητής του πυρήνα. Τα περισσότερα αρχεία έχουν ονόματα που αυτοεξηγούνται. Κάποια κύρια είναι:
|
||
|
||
- **/proc/\<num\> (directory)** : Οι κατάλογοι που το όνομα τους είναι ένας αριθμός, περιέχουν πληροφορίες για τη διεργασία με τον συγκεκριμένο process id. Θα
|
||
τους εξετάσουμε αναλυτικά σε λίγο.
|
||
- **/proc/self (directory)** : Symbolic link στον κατάλογο που περιέχει πληροφορίες για την τρέχουσα διεργασία
|
||
- **/proc/ide (directory)** : Πληροφορίες για τις συσκευές που είναι συνδεδεμένες στο ide bus.
|
||
- **/proc/sys (directory)** : Πλήθος πληροφοριών για το σύστημα και τον πυρήνα.
|
||
- **/proc/cpuinfo** : Πληροφορίες για τον επεξεργαστή μας και τα bugs του :)
|
||
- **/proc/filesystems** : Λίστα από τα filesystem που υποστηρίζει ο πυρήνας.
|
||
- **/proc/meminfo** : Πληροφορίες για τη χρήση της μνήμης στο σύστημα σας.
|
||
- **/proc/modules** : Ποια modules είναι φορτωμένα στον πυρήνα. Αυτό χρησιμοποιεί και η *lsmod*.
|
||
- **/proc/interrupts** : Από ποιες συσκευές πηγάζουν τα hardware interrupts.
|
||
- **/proc/partitions** : Λίστα με όλα τα partitions στον υπολογιστή (άσχετα αν είναι mounted η όχι).
|
||
- **/proc/mounts** : Λίστα με όλα τα mounted directories. (link στο self/mounts)
|
||
|
||
### [4.3 Πληροφορίες για μια διεργασία]{#ss4.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. Χρήσιμες έως πολύ χρήσιμες πληροφορίες ΙΙ]{#s5}
|
||
|
||
### []{#dead-listing} [5.1 Dead Listing και Εργαλεία]{#ss5.1}
|
||
|
||
#### Τι είναι το 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>, ενώ τη 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
|
||
|
||
#### Bastard Disassembly Enviroment
|
||
|
||
Στον κόσμο του open source τώρα, στο sourceforge θα βρείτε projects που υπόσχονται πολλά αλλά δυστυχώς είναι σε πρώιμο στάδιο. Ένα από αυτά είναι το Bastard
|
||
Disassembly Enviroment. Πρακτικά πρόκειται για μια scripting γλώσσα και το αντίστοιχο interpreter shell. Site: <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>.
|
||
|
||
> ....... ! ;********************************************************
|
||
> ....... ! ; 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, έχει και τα ίδια [αδύνατα σημεία](#objdump-weakness). Site:
|
||
<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)
|
||
>
|
||
> * Possible StringData Ref from Code Obj ->"Usage: %s <number>"
|
||
> |
|
||
> :080483aa 6864840408 push 8048464
|
||
>
|
||
> * Reference To: GLIBC_2.0::printf
|
||
> |
|
||
> :080483af e8f8feffff call 080482ac
|
||
> :080483b4 83c410 add esp, 10
|
||
> :080483b7 83ec0c sub esp, c
|
||
> :080483ba 6a01 push 1
|
||
>
|
||
> * Reference To: GLIBC_2.0::exit -.
|
||
> |
|
||
> :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)
|
||
>
|
||
> * Reference To: alf -------------.
|
||
> |
|
||
> :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
|
||
>
|
||
> * Possible StringData Ref from Code Obj ->"Ok!"
|
||
> |
|
||
> :080483e0 6878840408 push 8048478
|
||
>
|
||
> * Reference To: GLIBC_2.0::printf
|
||
> |
|
||
> :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
|
||
>
|
||
> * Possible StringData Ref from Code Obj ->"Failed!"
|
||
> |
|
||
> :080483f2 687d840408 push 804847d
|
||
>
|
||
> * Reference To: GLIBC_2.0::printf
|
||
> |
|
||
> :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]
|
||
>
|
||
> * Reference To: GLIBC_2.0::atoi -.
|
||
> |
|
||
> :08048412 e8b5feffff call 080482cc
|
||
> :08048417 83c410 add esp, 10
|
||
> :0804841a c9 leave
|
||
> :0804841b c3 ret
|
||
|
||
### []{#packing}[5.2 Packed Executables - Συμπιεσμένα Εκτελέσιμα]{#ss5.2}
|
||
|
||
#### Τι είναι;
|
||
|
||
Τα εκτελέσιμα αρχεία, όπως και όλα τα αρχεία, περιέχουν μέσα τους επαναλήψεις που καθιστούν δυνατή τη συμπίεση τους. Η συμπίεση στα εκτελέσιμα, εφόσον η
|
||
αποσυμπίεση μπορεί να εκτελεστεί αρκετά γρήγορα ώστε να μη γίνεται αισθητή, είναι σίγουρα επιθυμητή. Τα αρχεία καταλαμβάνουν λιγότερο χώρο και επίσης είναι πιο
|
||
δύσκολο να ερευνηθούν και να αλλαχτούν (βέβαια, για όσους κάνουμε RCE αυτό είναι μεγάλο πρόβλημα). Μάλιστα, πολλά προγράμματα συμπίεσης εκτελέσιμων εφαρμόζουν
|
||
και άλλες τεχνικές, όπως κρυπτογράφηση και CRC ελέγχους. Είναι σαφές πως δε βολεύει απλώς να συμπιεστεί το αρχείο με κάποια παραδοσιακή μέθοδο (πχ με το gzip).
|
||
Αυτό συμβαίνει, διότι εκτός από το ότι το εκτελέσιμο δεν είναι πια εκτελέσιμο (η κατάσταση διορθώνεται με χρήση scripts για αυτόματη αποσυμπίεση, το utility
|
||
gzexe λειτουργεί έτσι), χάνεται η προστασία από το RCE, αφού τελικά το εκτελέσιμο θα βρεθεί στην αρχική του μορφή πριν εκτελεστεί. Για αυτό, έχουν αναπτυχθεί
|
||
διάφορες άλλες τεχνικές για συμπίεση προγραμμάτων.
|
||
|
||
#### Βασικές Τεχνικές packing
|
||
|
||
Καταρχάς θα δούμε την απλή 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) και έτσι θα αρχίσει το κυρίως πρόγραμμα.
|
||
|
||
#### Βασικές Τεχνικές 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 Παράδειγμα - Υπό πίεση]{#s6}
|
||
|
||
### [6.1 Πράξη 1η]{#ss6.1}
|
||
|
||
Αυτή τη φορά θα ασχοληθούμε με το demo του εκπληκτικού προγράμματος για πράξεις μη αρνητικών ακεραίων: [rce2-files/hands-on.gz](rce2-files/hands-on.gz). Το 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](rce2-files/listing1.txt)\
|
||
Ουακ! Τι είναι όλα αυτά;
|
||
|
||
Τα ακατανόητα ονόματα είναι σύμβολα της C++ σε μπλεγμένη (mangled) μορφή. Υποτίθεται πως το ltrace έχει μια επιλογή (-C) για να κάνει demangling αλλά σε εμένα
|
||
δε βελτίωσε την κατάσταση. Ευτυχώς υπάρχει ένα πρόγραμμα, το **c++filt**, το οποίο αποκωδικοποιεί τα ονόματα των c++ συμβόλων.\
|
||
[listing2](rce2-files/listing2.txt)\
|
||
Αν δεν είστε μυημένοι στους μυστικούς συμβολισμούς της 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. Θα μπορούσαμε να το αγνοήσουμε, όμως, όπως έχουμε αναφέρει, οι ευκολίες που προσφέρει είναι ανεκτίμητες. Οπότε, στο επόμενο κομμάτι θα κάνουμε
|
||
ότι μπορούμε για να φέρουμε το εκτελέσιμο όσο πιο κοντά γίνεται στην αυθεντική του μορφή.
|
||
|
||
### [6.2 Επιστροφή σε Κανονικές Συνθήκες πίεσης και θερμοκρασίας.]{#ss6.2}
|
||
|
||
(ΣΗΜΕΙΩΣΗ: Αν δεν έχετε διαβάσει τις πληροφορίες για το packing, [τώρα είναι μια καλή στιγμή να το κάνετε](05_rce3-5.html#packing))
|
||
|
||
Πως όμως θα δούμε το αρχικό εκτελέσιμο; Αυτό μπορεί να επιτευχθεί ως εξής:
|
||
|
||
**1. Ο εύκολος τρόπος**
|
||
|
||
Μπορούμε να πάμε στη σελίδα του UPX <http://upx.sourceforge.net>, να το κατεβάσουμε και να αποσυμπιέσουμε το εκτελέσιμο. Αν και είναι απλή μέθοδος, δεν έχει
|
||
καμία γενικότητα. Για παράδειγμα, δε μπορεί να εφαρμοστεί στην περίπτωση που η συμπίεση/κρυπτογράφηση έχει γίνει με κάποιο custom αλγόριθμο ή ακόμα και με άλλη
|
||
έκδοση του ίδιου προγράμματος.
|
||
|
||
**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 Σταματώντας τον χρόνο]{#ss6.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](rce2-files/listing3.txt)\
|
||
Όλη η προηγούμενη ακολουθία μας λέει πως καλείται η συνάρτηση για τον overloaded τελεστή \<\<, με αριστερή πλευρά ένα αντικείμενο τύπου *basic\_ostream* (output
|
||
stream) και δεξιά *const char \**. Με απλά λόγια, η εντολή που εκτελέστηκε ήταν η std::cout\<\<\"Ready\>\" ή std::cout\<\<\"Not Ready\>\".
|
||
|
||
Λίγο πιο κάτω (έχω αποπλέξει τα σύμβολα με το c++filt):\
|
||
[listing4.txt](rce2-files/listing4.txt)\
|
||
Το οποίο μεταφράζεται σε 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 μέρες ακόμα!
|
||
|
||
### [6.4 To patch]{#ss6.4}
|
||
|
||
Αν θέλουμε να πειράξουμε το πρόγραμμα για να λειτουργεί για πάντα (σχεδόν\...), μια επιλογή είναι να αντικαταστήσουμε την
|
||
|
||
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 Ασκήσεις για το σπίτι :)]{#ss6.5}
|
||
|
||
1. Πως φτάσαμε στο συμπέρασμα πως η εντολή *80489e7 ! mov \[ebp-18h\], eax* τοποθετεί στη διεύθυνση \[ebp-18h\] την ώρα τελευταίας τροποποίησης του
|
||
εκτελέσιμου;
|
||
2. Εκτός από τον χρονικό έλεγχο για την εκτύπωση του \"Ready\>\"/\"Not Ready\>\" γίνεται χρονικός έλεγχος και σε κάποιο άλλο σημείο. Που είναι αυτό και τι
|
||
επιπτώσεις έχει στο πρόγραμμα;
|
||
|
||
Ο πηγαίος κώδικας του προγράμματος: [rce2-files/hands-on.cpp.gz](rce2-files/hands-on.cpp.gz). Το πρόγραμμα έχει γραφεί επίτηδες ώστε να χρησιμοποιεί στοιχεία
|
||
της C++ τα οποία στην προκειμένη περίπτωση δεν αποτελούν την καλύτερη, σχεδιαστικά, επιλογή. Για παράδειγμα, όλες οι μέθοδοι των κλάσεων έχουν δηλωθεί (έμμεσα)
|
||
να είναι inline και για αυτό δημιουργείται ένας μικρός χαμός στο εκτελέσιμο!
|
||
|
||
|
||
### [7. Πρόκληση]{#s7}
|
||
|
||
### [7.1 Προηγούμενη Πρόκληση (\#1) - Λύση και Hall Of Fame]{#ss7.1}
|
||
|
||
Σκοπός του προηγούμενου 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](rce2-files/prekas_giorgos-rce1sol.tgz)
|
||
|
||
> \...Το πρόγραμμα keygen είναι ένας ολοκληρωμένος key generator. Παρέχει περισσότερες δυνατότητες από όσες περιμένει κανείς από ένα key generator\...
|
||
|
||
2. **Αντώνης Σταμπούλης** [Challenge \#1 Keygen](rce2-files/stampoulis_antonis-rce1sol.tgz)
|
||
|
||
> \... Δουλεύει σχετικά απλά, ψάχνοντας να βρει μια πυθαγόρεια τριάδα κοντά στην περιοχή που βρίσκονται τα a, b και c που δημιουργούνται όταν το input
|
||
> string αποτελείται από χαρακτήρες κενού (ascii 32). Μετά προσθέτει τυχαίους αριθμούς στους χαρακτήρες του input string έτσι ώστε να βγαίνουν τελικά τα
|
||
> επιθυμητά a, b και c\...
|
||
|
||
Ο δικός μου keygen και το source αρχείο του challenge \#1 : [Challenge \#1 source](rce2-files/alf-rce1sol.tgz)
|
||
|
||
### [7.2 Πρόκληση \#2]{#ss7.2}
|
||
|
||
Αγαπητοί αναγνώστες,\
|
||
πρόσφατα στο υπόγειο των καινούργιων κτιρίων του περιοδικού μας ανακαλύφθηκε ένα κρυφό δωμάτιο. Μέσα σε αυτό βρέθηκε μια tamperproof θήκη που από ότι φαίνεται
|
||
περιέχει ένα υπολογιστικό σύστημα. Το μόνο που φαίνεται από το σύστημα είναι μια μικρή οθόνη και μια υποδοχή για rom memory modules που περιέχουν τον προς
|
||
εκτέλεση κώδικα. Ένας από τους εργαζόμενους θυμάται πως η εταιρεία που υπήρχε εδώ παλιότερα είχε ασχοληθεί με την υλοποίηση ενός πρότυπου υπολογιστικού
|
||
συστήματος τεχνολογίας RISC αλλά το project εγκαταλείφθηκε λόγω έλλειψης χρημάτων.
|
||
|
||
Δυστυχώς οι προσπάθειες μας για επικοινωνία με άτομα που πιστεύουμε πως έχουν σχέση με το εν λόγω project δεν έχουν φέρει αποτέλεσμα. Ύστερα από διεξοδικότερη
|
||
έρευνα στο κρυφό δωμάτιο ήρθε στο φως μια δισκέτα που γράφει πάνω \"RISC-Emu v0.42rox\" και από ότι φαίνεται περιέχει και έναν emulator του επεξεργαστή.
|
||
Εικάζεται πως αυτός είχε χρησιμοποιηθεί για λόγους prototyping. Τα αρχεία που βρέθηκαν στη δισκέτα βρίσκονται στο αρχείο:
|
||
[rce2-files/challenge2.tar.gz](rce2-files/challenge2.tar.gz).
|
||
|
||
Η tamperproof θήκη γράφει με μεγάλα γράμματα \"Προσοχή! Η θήκη μπορεί να ανοίξει μόνο από το ίδιο το σύστημα. Οποιαδήποτε προσπάθεια για παραβίαση θα έχει ως
|
||
αποτέλεσμα την απελευθέρωση χημικών που θα καταστρέψουν το hardware\".
|
||
|
||
Η αποστολή σας, αν την αποδεχτείτε, είναι να βρείτε έναν τρόπο να ανοιχτεί η θήκη χωρίς να προκληθεί ζημιά στο hardware. Από όσα ξέρουμε ως τώρα αυτό θα πρέπει
|
||
να γίνεται με την εισαγωγή ενός σωστού memory module. Η ανταμοιβή θα είναι πλουσιοπάροχη και θα φθάνει το ύψος των 10000 δωρεάν συνδρομών στο περιοδικό μας.
|
||
|
||
Με εκτίμηση,
|
||
|
||
Ο Πρόεδρος
|
||
|