💾 Archived View for magaz.hellug.gr › 35 › 05_rce4 › index.gmi captured on 2024-08-25 at 00:38:15. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-02-05)

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

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

Αλέξανδρος Φραντζής
Φεβ 2006

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

1. Εισαγωγή

2. Εικονικές Μηχανές

3. Μερικές σκέψεις για το μέλλον του RCE - Obfuscation

4. Hands-on παράδειγμα: Java vs C

5. Πρόκληση

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

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

Το γενικό θέμα με το οποίο ασχολείται το παρόν άρθρο είναι οι εικονικές μηχανές (Virtual Machines).

Στο πρώτο τμήμα θα ασχοληθούμε εν συντομία με την ιστορία και το γενικό σχεδιασμό των VMs.

Στο δεύτερο τμήμα εκφράζονται μερικές σκέψεις μου για το μέλλον του RCE και το πως σχετίζεται η τεχνική του obfuscation με αυτό.

Στο τρίτο τμήμα του άρθρου, θα μάθουμε γιατί ο κώδικας Java μπορεί να είναι πιο γρήγορος από τον ίδιο κώδικα σε C.

Στο τέταρτο και τελευταίο τμήμα θα δώσουμε μια ενδεικτική λύση για την προηγούμενη πρόκληση και βέβαια το Hall of Fame.

Καλό RCE!

[2. Εικονικές Μηχανές]

[2.1 Εισαγωγή]

Η εικονική μηχανή (Virtual Μachine), όπως δηλώνει και το όνομα της, είναι μια μηχανή που υφίσταται μόνο στο βασίλειο του αφηρημένου. Πρόκειται για επεξεργαστή υλοποιημένο μόνο με λογισμικό, με δικό του σετ εντολών και ιδιαίτερων χαρακτηριστικών.

Μια προφανής και σημαντική χρήση των εικονικών μηχανών είναι η εξομοίωση και η μελέτη υπαρχόντων ή και υπό σχεδίαση hardware συστημάτων. Για αυτή την κατηγορία έχει επικρατήσει η ονομασία εξομοιωτής (emulator) και σήμερα για σχεδόν όλα τα υπολογιστικά συστήματα περασμένων εποχών υπάρχει ένας εξομοιωτής.

Ένας δεύτερος πολύ σημαντικός λόγος για την ύπαρξη VMs είναι η χρήση τους ως ένα "μονωτικό στρώμα" ανάμεσα στις εφαρμογές και το υλικό. Μια εφαρμογή που είναι σχεδιασμένη για μια VM μπορεί να εκτελεστεί σε οποιοδήποτε επεξεργαστή για τον οποίο υπάρχει ένας διερμηνευτής για τον κώδικα της VM. Αυτή είναι η νοοτροπία του "Προγραμμάτισε μια φορά, τρέξε παντού" (Code Once, Run Everywhere).

Πολύ συχνά οι εικονικές μηχανές χρησιμοποιούνται για να δώσουν στο προγραμματιστή την ψευδαίσθηση πως δουλεύει σε ένα ιδιαίτερα εξελιγμένο σύστημα, με χαρακτηριστικά ειδικά σχεδιασμένα για την εργασία του. Τέτοιες μηχανές χρησιμοποιούνται σε παιχνίδια όπου βασικές εντολές του εικονικού επεξεργαστή μπορεί να είναι για παράδειγμα "μετακίνησε το sprite A από τη θέση x στη θέση y".

Οι εικονικές μηχανές, αν και είναι πολύ στη μόδα σήμερα, δεν αποτελούν καινούργιο φαινόμενο. Ήδη από το 1970 η ανάγκη διαχωρισμού των διάφορων φάσεων της μεταγλώττισης ενός προγράμματος οδήγησε τους δημιουργούς μεταγλωττιστών στην εισαγωγή και χρήση ενδιάμεσων μορφών κώδικα. Μια πολύ γνωστή ενδιάμεση μορφή είναι η P-Code που χρησιμοποιήθηκε για τους μεταγλωττιστές της γλώσσας Pascal. Πολύ σύντομα η P-Code έπαψε να χρησιμοποιείται μόνο στους μεταγλωττιστές και έγινε η βάση μιας εικονική μηχανής για το σύστημα της UCSD Pascal.

[IMG]

Παρ' όλο που που η δεκαετία του '70 μοιάζει μακρινή, τα πράγματα δεν έχουν αλλάξει πολύ στον συγκεκριμένο τομέα. Οι σύγχρονες γλώσσες προγραμματισμού χρησιμοποιούν το ίδιο μοντέλο με μια μικρή αλλά σημαντική προσθήκη. Για λόγους ταχύτητας, οι πιο εξελιγμένοι διερμηνευτές δεν εκτελούν τον κώδικα σε ενδιάμεση μορφή αλλά τον μετατρέπουν στη γλώσσα μηχανής του επεξεργαστή στον οποίο τρέχουν (native code). Η μετατροπή αυτή γίνεται την ώρα της εκτέλεσης και ονομάζεται Just-In-Time (JIT) μεταγλώττιση, ενώ η κλασική μεταγλώττιση έχει την ονομασία Ahead-Of-Time (AOT).

[2.2 Ενδιάμεσες Μορφές Κώδικα]

Οι ενδιάμεσες μορφές κώδικα αλλά και γενικά τα σετ εντολών μπορούν να χωριστούν σε δύο μεγάλες κατηγορίες, ανάλογα με το πως τροφοδοτούνται οι εντολές με δεδομένα.

Στην πρώτη κατηγορία ανήκουν οι μορφές κώδικα στις οποίες τα δεδομένα ανταλλάσσονται μέσω καταχωρητών. Οι μηχανές που υλοποιούν αυτό το μοντέλο ονομάζονται register-based και αποτελούν τη πλειοψηφία των hardware επεξεργαστών. Η πράξη a=a+b*c σε ένα τέτοιο σύστημα έχει τη μορφή:

move t1, b          ;; t1=b
multiply t1, t1, c  ;; t1=t1*c
add a, a, t1        ;; a=a+t1

Αντίθετα, τα συστήματα όπου το μέσο ανταλλαγής δεδομένων είναι ο σωρός ονομάζονται stack-based. Ο σωρός αυτός έχει την ειδική ονομασία σωρός τελεστέων (operand stack). Οι περισσότερες εικονικές μηχανές ακολουθούν αυτό το μοντέλο διότι έχει αποδειχθεί ότι προσφέρει ταχύτερη εκτέλεση σε λογισμικό και είναι πιο απλή και συμπαγής. Η πράξη a=a+b*c εδώ έχει την εξής μορφή (στα σχόλια φαίνεται η κατάσταση του σωρού μετά την εκτέλεσης της εντολής):

load b    ;; Σωρός: [b]
load c    ;; Σωρός: [b] [c]
multiply  ;; Σωρός: [b*c] 
load a    ;; Σωρός: [b*c] [a]
add       ;; Σωρός: [a+b*c]
store a   ;; Σωρός: - , a=a+b*c 

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

Σήμερα, η Java της Sun και το .NET της Microsoft είναι ίσως τα δύο πιο διάσημα περιβάλλοντα που χρησιμοποιούν εικονικές μηχανές. Και τα δύο είναι βασισμένα στη stack-based αρχιτεκτονική και τα σετ εντολών τους είναι παρεμφερή. Τα standards και για τις δύο περιβάλλοντα υπάρχουν ανοικτά στο internet. Στο Linux, υλοποιήσεις της Java Virtual Machine προσφέρονται από τη Sun, την ΙΒΜ και επίσης υπάρχουν μερικές open source προτάσεις όπως το kaffe ( http://www.kaffe.org[1]). Για το .NET στο Linux υπάρχει το open source Mono Project ( http://www.mono-project.com[2]).

1: http://www.kaffe.org

2: http://www.mono-project.com

[2.3 Java Virtual Machine]

Η ενδιάμεση μορφή στην οποία αποθηκεύονται τα προγράμματα σε Java είναι τα Java Bytecodes. Κατά την εκτέλεση της Java Virtual Machine (JVM) κάθε thread αποτελείται από τα παρακάτω στοιχεία:

Κάθε στιγμή το πρόγραμμα βρίσκεται μέσα σε μία συνάρτηση και αποθηκεύει τις τοπικές πληροφορίες σε ένα πλαίσιο στον κανονικό σωρό. Το πλαίσιο περιέχει το σωρό τελεστέων, έναν πίνακα τοπικών μεταβλητών και κάποιες άλλες πληροφορίες που σχετίζονται με τα δεδομένα της κλάσης στην οποία ανήκει η μέθοδος. Η πρώτη τοπική μεταβλητή (index 0) περιέχει την αναφορά στο τρέχον instance της κλάσης. Πρόκειται ουσιαστικά για το this που σίγουρα έχουν χρησιμοποιήσει όσοι έχουν ασχοληθεί με Java. Από εκεί και πέρα (index 1) οι τοπικές μεταβλητές περιέχουν τις παραμέτρους της συνάρτησης. Η έννοια των τοπικών μεταβλητών είναι πιο ευρεία από αυτή που έχουμε συνηθίσει από άλλες γλώσσες (πχ C).

Ένα μικρό παράδειγμα:

public int alf(int x)
{
        if (x > 3)
                x++;
        else
                x--;

        return x;
}

παράγει τα εξής bytecodes:

||  .. ! ;----------------------------------------------
||  .. ! ; public int test::alf(int)
||  .. ! ;----------------------------------------------
||  .. ! alf_fd:                                                            
||  .. !   iload_1              ;; Φόρτωσε στο σωρό τελεστέων τη δεύτερη τοπική 
                                ;; μεταβλητή (την πρώτη παράμετρο της συνάρτησης).
||  fe !   iconst_3             ;; Φόρτωσε στο σωρό τελεστέων τη σταθερά 3.
||  ff !   if_icmpge loc_108    ;; Αν η κορυφαία τιμή στο σωρό τελεστέων είναι μεγαλύτερη ή ίση 
                                ;; με την αμέσως προηγούμενη, πήγαινε στη διεύθυνση 108.
|| 102 !   iinc      1, 1       ;; Πρόσθεσε στη δεύτερη τοπική μεταβλητή την τιμή 1.
|| 105 !   goto      loc_10b    ;; Πήγαινε στη διεύθυνση 10b.
|| 108 !                                                                    
|| ... ! loc_108:                       
|| ... !   iinc      1, 0ffh    ;; Πρόσθεσε στη δεύτερη τοπική μεταβλητή την τιμή -1.
|| 10b !                                                                    
|| ... ! loc_10b:                       
|| ... !   iload_1              ;; Φόρτωσε στο σωρό τελεστέων τη δεύτερη τοπική μεταβλητή.
|| 10c !   ireturn              ;; Πάρε την κορυφαία τιμή από τον τρέχον σωρό τελεστέων και
                                ;; τοποθέτησέ την στην κορυφή του σωρού τελεστέων του κώδικα
                                ;; που κάλεσε την τρέχουσα συνάρτηση.

Για να κάνουμε disassemble κάποιο class αρχείο στις εντολές της JVM (όπως παραπάνω) μπορούμε να χρησιμοποιήσουμε τον ΗΤ editor ( http://hte.sourceforge.net[3]). Όμως, μπορούμε να πάμε ακόμα πιο πέρα, χρησιμοποιώντας κάποιον decompiler για Java ο οποίος θα προσπαθήσει να μας δώσει τον αρχικό πηγαίο κώδικα! Δυο πολύ γνωστοί Java decompilers είναι ο MOCHA ( http://www.brouhaha.com/~eric/computers/mocha.html[4]) και ο JAD ( http://kpdus.tripod.com/jad.html[5]).

3: http://hte.sourceforge.net

4: http://www.brouhaha.com/~eric/computers/mocha.html

5: http://kpdus.tripod.com/jad.html

[3. Μερικές σκέψεις για το μέλλον του RCE - Obfuscation]

Γλώσσες όπως η Java και η C# που χρησιμοποιούν ενδιάμεσες μορφές κώδικα ως το βασικό μέσο μεταφοράς τους, εμφανίζουν μια νέα πρόκληση για το RCE. Οι ενδιάμεσες μορφές μεταφέρουν αναπόφευκτα πολλές πληροφορίες για τον πηγαίο κώδικα και έτσι κάποιος θα μπορούσε να θεωρήσει πως είναι πιο εύκολο να τον ανασυνθέσουμε. Και αυτό είναι, όντως, αλήθεια.

Έπρεπε, λοιπόν, να βρεθεί ένας διαφορετικός τρόπος για προστασία του κώδικα από τα αδιάκριτα μάτια. Αυτό που έγινε, τελικά, είναι να δοθεί περισσότερο βάρος στην παλιά τεχνική του code obfuscation. Οι ίδιες βασικές αρχές παρέμεινα�� αλλά προσαρμόστηκαν στη νέα πραγματικότητα του αντικειμενοστρεφούς μοντέλου.

Σκοπός του obfuscation είναι να μετασχηματίσει ένα πρόγραμμα σε ένα άλλο, ισοδύναμο του, τέτοιο ώστε να είναι πιο δύσκολο να κατανοηθεί από ανθρώπους. Επιπλέον χρειάζεται ο μετασχηματισμός αυτός να είναι δύσκολα αντιστρεπτός από κάποιο άλλο αυτόματο εργαλείο (deobfuscator). Στο παιχνίδι παίζουν ρόλο πολλοί αντικρουόμενοι παράγοντες και έτσι πρέπει να βρεθεί μια ικανοποιητική λύση, ανάλογα με τις ανάγκες του χρήστη. Για παράδειγμα, αύξηση της πολυπλοκότητας του κώδικα μπορεί να επιφέρει δραματική αλλαγή στην ταχύτητα εκτέλεσης, οπότε πρέπει να αποφασιστεί τι έχει μεγαλύτερη σημασία, η απόδοση ή η προστασία.

Ο πιο απλός τρόπος obfuscation ενός προγράμματος είναι η μετονομασία των συμβόλων του (μεταβλητές, συναρτήσεις κτλ) σε ακατανόητες συμβολοσειρές. Άλλο είναι να βλέπεις μια συνάρτηση "CheckUser" και άλλο αυτή να λέγεται "mvkof89". Πάντως, αν και έτσι δυσχεραίνονται οι RCE προσπάθειες, η κατάσταση δεν είναι τόσο άσχημη.

Το επόμενο βήμα είναι το λεγόμενο control-flow obfuscation. Σκοπός αυτής της τεχνικής είναι να μπερδευτεί τόσο πολύ η ροή του προγράμματος ώστε να είναι δύσκολο να την ακολουθήσει κάποιος. Για παράδειγμα το απλό κομμάτι κώδικα:

printf("OK");

μπορεί να μετασχηματιστεί στο ισοδύναμο:

y=72;
...
x=random();     
if ( (x*13) % 5 < 3) {
doit:
        if (y > 61) 
                printf("OK");
        else
                printf("KUKU");
}
else {
        if (y * 2 - 6 == 138) 
                goto doit;
        printf("KUKU");
}

Φανταστείτε τι σύγχυση μπορεί να προκληθεί σε επίπεδο bytecodes (η και γλώσσα μηχανής)! Από τη μεριά τους, οι deobfuscators προσπαθούν να αντιμετωπίσουν τη μέθοδο αυτή χρησιμοποιώντας αυτόματες τεχνικές για την απόδειξη θεωρημάτων. Για παράδειγμα, βλέποντας πως το y είναι 72 ξέρουν πως πάντα ισχύει y>61 και επομένως το printf("KUKU") δεν πρόκειται να εκτελεστεί ποτέ.

Για επιπλέον αύξηση του χάους σε ένα πρόγραμμα, μπορεί να εφαρμοστεί η τεχνική του data obfuscation. Εδώ στο στόχαστρο βρίσκονται πλέον τα δεδομένα του προγράμματος τα οποία σπάνε, συγχωνεύονται, ψευδο-κρυπτογραφούνται και γενικώς υπόκεινται σε ένα σωρό μετασχηματισμούς. Ένα απλό παράδειγμα:

;; Έστω a ένας πίνακας 20 στοιχείων
x = 0;

for(i = 0; i < 20; i++) {
        x += a[i];
}

if (x == 10) 
        printf("Ok!");
else
        printf("Kuku!");

;; Έστω a ένας πίνακας 20 στοιχείων
x = 100;
for(i = 0; i <20; i++) {
        x += 2 * a[i] + i;
}

if (x == 310) 
        printf("Ok!");
else
        printf("Kuku!");

Υπάρχουν πολλές ακόμη κατηγορίες obfuscating μετασχηματισμών και ένας σωστός συνδυασμός τους καθιστά την κατάσταση πολύ δύσκολη για τον reverse engineer.

Στο μέλλον προβλέπω (κοιτώντας τη κρυστάλλινη σφαίρα :) ) πως το παιχνίδι του RCE σε ένα μεγάλο βαθμό θα έχει μετατραπεί σε ένα παιχνίδι obfuscation/deobfuscation. Εδώ και καιρό υπάρχουν εργαλεία για obfuscation ενώ τον τελευταίο καιρό εμφανίζονται πρωτότυποι deobfuscators βασισμένοι σε σχετικές δημοσιεύσεις. Ας αρχίσουν οι χοροί...

[4. Hands-on παράδειγμα: Java vs C]

...και την 42η μέρα ο Θεός δημιούργησε την C. Βλέποντας την απειλή οι δυνάμεις του Κακού αποφάσισαν να αντεπιτεθούν... και έτσι γεννήθηκε η Java.

Η Java από τότε που δημιουργήθηκε κλήθηκε να αντιμετωπίσει τις κυρίαρχες τότε (αλλά και σήμερα) C και C++. Η σύγκριση ήταν αναπόφευκτη΄ η Java διαφημιζόταν ως μια πιο κομψή και ασφαλής C++, μια γλώσσα ικανή να χρησιμοποιηθεί σε ένα ευρύ πεδίο εφαρμογών, από ιστοσελίδες μέχρι ενσωματωμένες συσκευές. Όπως συνηθίζεται στον κόσμο των υπολογιστών, ένας "ιερός" πόλεμος ξέσπασε.

Οι πολέμιοι της Java είχαν κυρίως δύο λόγους για τους οποίους ήθελαν να την κάψουν στην πυρά. Καταρχάς υπήρχαν εκείνοι που στο άκουσμα της λέξης "αντικειμενοστρεφής" έβγαζαν σκόρδα και προσπαθούσαν να ξορκίσουν τα δαιμόνια. Σε αυτούς, βέβαια, δεν άρεσε ούτε η C++, η οποία όμως έπαιρνε άφεση διότι μπορούσε απλώς να χρησιμοποιηθεί ως βελτιωμένη C. Ο δεύτερος και πιο καταδικαστικός λόγος ενάντια στην Java ήταν ότι ήταν αργή. Ειδικά σε ότι είχε σχέση με γραφικές διεπαφές (GUI) ήταν εκνευριστικά αργή και επιπλέον είχε μια μέτριας ποιότητας (εμφανισιακά, τουλάχιστον) βιβλιοθήκη χειριστηριών.

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

Δυστυχώς στις απόψεις πολλών η Java και γενικά οι διερμηνευόμενες (interpreted) γλώσσες έχουν στιγματιστεί από την αργοπορία που τις χαρακτήριζε στα πρώτα τους βήματα. Στο πείραμα που ακολουθεί θα εξετάσουμε και θα εξηγήσουμε την απρόσμενα καλή απόδοση ενός προγράμματος σε Java.

[4.1 Το πείραμα]

Στο πείραμα που ακολουθεί θα μετρήσουμε τους χρόνους εκτέλεσης του ίδιου προγράμματος σε C (gcc 3.2.3) και σε Java (Sun J2RE 1.4.2_01). Το πρόγραμμα είναι η αναδρομική συνάρτηση υπολογισμού των όρων της σειράς fibonacci fn = fn-1 + fn-2, με f0 = 0 και f1 = 1.

Σε C:

#include <stdio.h>
#include <stdlib.h>

unsigned long fib(unsigned long n)
{
   if (n < 2)
        return n;
   else
        return fib(n-1) + fib(n-2);
}

int main(int argc, char *argv[])
{
    int n;

    if (argc < 2)
        n = 1;
    else
        n = atoi(argv[1]);

    printf("%lu\n", fib(n));

    return 0;
}


Σε Java:

public class fib {

    public static void main(String args[]) {
        int n;

        if (args.length < 1)
            n = 1;
        else
            n = Integer.parseInt(args[0]);

        System.out.println(fib(n));
    }

    public static int fib(int n) {
        if (n < 2) 
            return n;
        else
            return fib(n-1) + fib(n-2);
    }
}


Μεταγλωττίζουμε τα δύο προγράμματα και τα εκτελούμε 3 φορές το καθένα. Το J2RE της Sun προσφέρει δύο εικονικές μηχανές, την server και την client (default). Η πρώτη έχει μεγαλύτερες απαιτήσεις μνήμης αλλά έχει ως αποτέλεσμα καλύτερη ταχύτητα εκτέλεσης ενώ η δεύτερη έχει πιο μικρές απαιτήσεις αλλά η ταχύτητα εκτέλεσης δεν είναι η βέλτιστη. Εδώ χρησιμοποιούμε την παράμετρο "-server" που επιλέγει την server VM (εικονική μηχανή).

$ gcc -O3 -fomit-frame-pointer -o fib.c.exe fib.c
$ time fib.c.exe 39
63245986

real    0m7.479s
user    0m7.361s
sys     0m0.017s
$ time fib.c.exe 39
63245986

real    0m7.478s
user    0m7.368s
sys     0m0.013s
$ time fib.c.exe 39
63245986

real    0m7.471s
user    0m7.366s
sys     0m0.016s
$ javac fib.java
$ time java -server fib 39
63245986

real    0m5.853s
user    0m5.618s
sys     0m0.049s
$ time java -server fib 39
63245986

real    0m5.848s
user    0m5.625s
sys     0m0.043s
$ time java -server fib 39
63245986

real    0m5.847s
user    0m5.622s
sys     0m0.044s

Η Java φαίνεται να έχει αρκετά καλύτερες επιδόσεις από τη C!!! Είναι δυνατόν; Ευτυχώς ή δυστυχώς είναι!

[4.2 Η αναζήτηση]

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

Καταρχάς, γνωρίζουμε ότι o διερμηνευτής της Java περιέχει JIT μεταγλωττιστή οπότε γενικά θα περιμέναμε μια καλή απόδοση. Το βασικό ερώτημα είναι το τι έκανε ο JIT μεταγλωττιστής, που δε μπόρεσε να κάνει ο gcc, ώστε να βελτιώσει την απόδοση κατά 20% περίπου. Για να απαντήσουμε σε αυτό το ερώτημα το καλύτερο που μπορούμε να κάνουμε είναι να συγκρίνουμε τον κώδικα μηχανής που παρήγαγε καθένας από τους δύο αντιπάλους.

Τον κώδικα μηχανής που παρήγαγε ο gcc είναι πολύ απλο να τον εξετάσουμε χρησιμοποιώντας τον HT Editor. Βεβαίως, μπορείτε να χρησιμοποιήσετε όποιο εργαλείο σας βολεύει.

|| ....... ! ;****************************************************
|| ....... ! ; function fib (global)
|| ....... ! ;****************************************************
|| ....... ! fib:                            ;xref c80483d7 c80483e4 c8048427
|| ....... !                                 ;xref c8048434
|| ....... !   push    esi
|| 8048411 !   push    ebx
|| 8048412 !   mov     esi, [esp+0ch]       ;; esi=n
|| 8048416 !   cmp     esi, 1
|| 8048419 !   mov     eax, esi
|| 804841b !   ja      loc_8048420
|| 804841d !
|| ....... ! loc_804841d:                    ;xref j804843f
|| ....... !   pop     ebx
|| 804841e !   pop     esi
|| 804841f !   ret
|| 8048420 !
|| ....... ! loc_8048420:                    ;xref j804841b
|| ....... !   sub     esp, 0ch
|| 8048423 !   lea     edx, [esi-1]
|| 8048426 !   push    edx
|| 8048427 !   call    fib                  ;; fib(n-1)
|| 804842c !   lea     edx, [esi-2]
|| 804842f !   mov     ebx, eax
|| 8048431 !   mov     [esp], edx
|| 8048434 !   call    fib                  ;; fib(n-2)
|| 8048439 !   lea     eax, [eax+ebx]       ;; eax = fib(n-1) + fib(n-2)  
|| 804843c !   add     esp, 10h
|| 804843f !   jmp     loc_804841d

Ένα πολύ χρήσιμο χαρακτηριστικό του HT editor είναι ότι υποστηρίζει πληθώρα εκτελέσιμων αρχείων, μεταξύ αυτών και τα .class αρχεία της Java! Έτσι είναι απλό να εξετάσουμε και τα bytecodes στα οποία μεταφράστηκε η συνάρτηση fib:

||     ... ! ;----------------------------------------------
||     ... ! ; public static int fib::fib(int)
||     ... ! ;----------------------------------------------
||     ... ! fib_1fa:
||     ... !   iload_0                                ;; Σωρός: [n] 
||     1fb !   iconst_2                               ;; Σωρός: [n] [2]   
||     1fc !   if_icmpge       loc_201                ;; Σωρός: -, if (n >= 2) goto loc_201   
||     1ff !   iload_0                                ;; Σωρός: [n]     
||     200 !   ireturn
||     201 !
||     ... ! loc_201:                        ;xref j1fc
||     ... !   iload_0                                ;; Σωρός: [n] 
||     202 !   iconst_1                               ;; Σωρός: [n] [1]
||     203 !   isub                                   ;; Σωρός: [n-1] 
||     204 !   invokestatic    <int fib::fib(int)> 4  ;; Σωρός: [fib(n-1)] 
||     207 !   iload_0                                ;; Σωρός: [fib(n-1)] [n] 
||     208 !   iconst_2                               ;; Σωρός: [fib(n-1)] [n] [2] 
||     209 !   isub                                   ;; Σωρός: [fib(n-1)] [n-2] 
||     20a !   invokestatic    <int fib::fib(int)> 4  ;; Σωρός: [fib(n-1)] [fib(n-2)] 
||     20d !   iadd                                   ;; Σωρός: [fib(n-1)+fib(n-2)] 
||     20e !   ireturn

Φως στο τούνελ του Java JIT μεταγλωττιστή

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

Αρ��ίζοντας την εξερεύνηση μας εκτελούμε την ltrace -i -o fib.ltr java -server fib 10 (βλέπε προηγούμενα τεύχη). Εξετάζοντας το fib.ltr τα αποτελέσματα δεν είναι ιδιαίτερα ενθαρρυντικά, οπότε συνεχίζουμε με την strace -i -o fib.str java -server fib 10. Προς το τέλος του fib.str βρίσκουμε τα εξής:

[400379db] open("/home/alf/projects/magaz/issue3/src/fib.class", O_RDONLY|O_LARGEFILE) = 5
[401514e7] fstat64(5, {st_mode=S_IFREG|0644, st_size=561, ...}) = 0
[401512f7] stat64("/home/alf/projects/magaz/issue3/src/fib.class", ...
[40036eeb] read(5, "\312\376\272\276\0\0\0.\0$\n\0\7\0\22\n\0\23\0\24\t\0\25"..., 561) = 561
[40036f5f] close(5)                     = 0
[40118b71] gettimeofday({1081166745, 55282}, NULL) = 0
[40118b71] gettimeofday({1081166745, 55460}, NULL) = 0
[40118b71] gettimeofday({1081166745, 55594}, NULL) = 0
[40118b71] gettimeofday({1081166745, 56101}, NULL) = 0
[40118b71] gettimeofday({1081166745, 56241}, NULL) = 0

...

[401516d7] lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[401516d7] lstat64("/home/alf", {st_mode=S_IFDIR|0711, st_size=4096, ...}) = 0
[401516d7] lstat64("/home/alf/projects", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[401516d7] lstat64("/home/alf/projects/magaz", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[401516d7] lstat64("/home/alf/projects/magaz/issue3", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[401516d7] lstat64("/home/alf/projects/magaz/issue3/src", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
[40118b71] gettimeofday({1081166745, 82175}, NULL) = 0
[40118b71] gettimeofday({1081166745, 82915}, NULL) = 0
[40118b71] gettimeofday({1081166745, 83546}, NULL) = 0
[40118b71] gettimeofday({1081166745, 83719}, NULL) = 0
[40118b71] gettimeofday({1081166745, 83851}, NULL) = 0

...

[40159ac5] brk(0)                       = 0x80c4000
[40159ac5] brk(0x80c5000)               = 0x80c5000
[40159ac5] brk(0)                       = 0x80c5000
[40159ac5] brk(0x80c6000)               = 0x80c6000
[40036e5b] write(1, "89", 2)            = 2
[40036e5b] write(1, "\n", 1)            = 1

...

[400a8ac1] kill(789, SIGRTMIN)          = 0
[400a8ac1] kill(789, SIGRTMIN)          = 0
[40154c7d] unlink("/tmp/hsperfdata_alf/787") = 0

...

Παρατηρούμε πως η Java διαβάζει το .class αρχείο που περιέχει τα bytecodes της εφαρμογής μας. Ύστερα αρχίζει να διαβάζει συνεχώς την ώρα της ημέρας, μαθαίνει κάποια πράγματα για το τρέχον directory, συνεχίζει να δειγματοληπτεί την ώρα, αυξάνει το μέγεθος του data segment κατά 0x2000 bytes και τελικά τυπώνει το αποτέλεσμα (89). Ομολογουμένως οι πληροφορίες δεν είναι ιδιαίτερα χρήσιμες.

Ένα ενδιαφέρον σημείο είναι το αρχείο /tmp/hsperfdata_alf/787 το οποίο βλέπουμε να διαγράφεται. Παραπάνω στο fib.str υπάρχει το σημείο που ανοίγει:

[400379b8] open("/tmp/hsperfdata_alf/787", O_RDWR|O_CREAT|O_TRUNC, 0600) = 3
[4015bd61] ftruncate(3, 16384)          = 0
[4015d8ed] old_mmap(NULL, 16384, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x4081e000
[40036f41] close(3)                     = 0

Το /tmp/hsperfdata_alf/787 αντιστοιχείται (mapped) στις διευθύνσεις μνήμης 0x4081e000-0x40822000 και περαιτέρω προσπελάσεις γίνονται μέσα από αυτές τις διευθύνσεις. Μπορούμε να δούμε τα περιεχόμενα του αρχείου αν φροντίσουμε ώστε να αργήσει να τελειώσει το πρόγραμμα μας (πχ με java -server fib 100), για να μην προλάβει να σβηστεί το προσωρινό αυτό αρχείο.

Μια σύντομη εξέταση του αρχείου μας δείχνει πως πρόκειται για πληροφορίες που χρησιμοποιεί ο HotSpot(TM) JIT μεταγλωττιστής της Sun. Έτσι εξηγείται και το "hsperfdata_alf" που μάλλον σημαίνει HotSpot(TM) Performance Data για τον χρήστη alf. Επίσης, το αρχείο αρχίζει με τα bytes 0xCA 0xFE 0xC0 0xC0. Η αναζήτηση στο internet για το νόημα των bytes ήταν άκαρπη, πάντως υποψιάζομαι ότι είναι το signature των αρχείων δεδομένων του JIT μεταγλωττιστή.

Αν και (ελπίζω) ενδιαφέρουσα, η ως τώρα περιήγηση στον JIT μεταγλωττιστή δε μας οδήγησε πιο κοντά στη λύση. Παραμένει το καίριο ερώτημα για τη θέση του native κώδικα.

Ένας συλλογισμός που ίσως μας οδηγήσει στη λύση είναι ο παρακάτω: Ο java διερμηνευτής αφού δημιουργήσει τον native κώδικα περνάει τον έλεγχο σε αυτόν. Ο κώδικας μας είναι αμιγώς cpu-intensive, δηλαδή χρησιμοποιεί πολύ τον επεξεργαστή και δεν έχει I/O που μπορούν να διακόψουν τη λειτουργία του. Αυτό σημαίνει πως αν σε μια τυχαία χρονική στιγμή εξετάσουμε σε ποια διεύθυνση δείχνει ο instruction pointer (IP) της διεργασίας, αυτή με πολύ μεγάλη πιθανότητα θα βρίσκεται μέσα στις διευθύνσεις που καταλαμβάνει ο native κώδικας.

Επομένως, το πρόβλημα μας μετασχηματίστηκε στην εύρεση ενός τρόπου να παρακολουθούμε σε ποια διεύθυνση βρίσκεται η εκτέλεση κάποια διεργασίας!

...Σαν να ψάχνεις διεύθυνση στα άχυρα

Μια πρώτη σκέψη είναι να εκτελέσουμε την java στο GDB, να διακόπτουμε το πρόγραμμα κάθε λίγο και να καταγράφουμε τις τιμές του IP. Απλό και αποτελεσματικό... μόνο που δε φαίνεται να λειτουργεί στη δική μας περίπτωση :(

$ gdb -q java
(no debugging symbols found)...(gdb) r -server fib 10
Starting program: /usr/lib/j2sdk1.4.2_01/bin/java -server fib 10
(no debugging symbols found)...[New Thread 16384 (LWP 1536)]
(no debugging symbols found)...
(no debugging symbols found)...Cannot find user-level thread for LWP 1536: generic error
(gdb) info reg
No selected frame.
(gdb) q
The program is running.  Exit anyway? (y or n) y
Cannot find thread 16384: generic error
(gdb) q
The program is running.  Exit anyway? (y or n) y
Cannot find thread 16384: generic error

Όχι μόνο δεν καταφέραμε να τρέξουμε τη Java αλλά "τα πήρε" και ο GDB. Αααργκ!!! Αν κάποιος ξέρει τι συμβαίνει παρακαλώ να μου γράψει...

Μην απελπίζεστε! Ευτυχώς για εμάς, το πάντα χρήσιμο /proc προσφέρει την υπηρεσία που χρειαζόμαστε (τον τρέχων IP μιας διεργασίας). Η πληροφορία βρίσκεται καλά κρυμμένη μέσα στο /proc/#/stat και θα χρησιμοποιήσουμε την εντολή ps για να τη φέρουμε στην επιφάνεια. Συγκεκριμένα θα χρησιμοποιήσουμε τη παράμετρο -ο για να ορίσουμε τις πληροφορίες που θέλουμε να εμφανίζει η ps (βλέπε man page).

Τώρα, λοιπόν, έχουμε όλα τα εργαλεία στα χέρια μας και μπορούμε να αρχίσουμε δουλειά! Με τις παρακάτω εντολές τρέχουμε τη java και κάθε 0.2 δευτερόλεπτα τυπώνουμε τον IP της (30 δείγματα).

$ java -server fib 100 & 
[1] 1390
$ for ((i=0;$i<30;i++)); do ps --pid=1390 -o eip; sleep 0.2; done
     EIP
42900340
     EIP
429003b8
     EIP
4290033b
     EIP
4290031c
     EIP
4290030f
     EIP
42900302
     EIP
42900372
     EIP
429002e0
     EIP
429002e0
     EIP
42900414
     EIP
42900386
     EIP
429003dc
     EIP
429003d3
     EIP
429002e0
     EIP
4290044d
     EIP
42900455
     EIP
429003dc
     EIP
429003b4
     EIP
42900369
     EIP
42900453
     EIP
42900409
     EIP
42900350
     EIP
42900441
     EIP
429003c4
     EIP
429002e7
     EIP
42900372
     EIP
42900390
     EIP
42900311
     EIP
429003b4
     EIP
429003b4

Παρατηρούμε πως ο κώδικας βρίσκεται σε ένα βρόχο με μικρότερη διεύθυνση την 0x429002e0 και μέγιστη 0x42900453. Οι διευθύνσεις αυτές μπορεί να μην είναι τα ακριβή άκρα του βρόχου, αφού έχουμε κάνει τυχαία δειγματοληψία, αλλά με μεγάλη πιθανότητα είναι κάπου εκεί κοντά.

Το μόνο που μένει τώρα είναι να διαβάσουμε τον κώδικα που βρίσκεται σε αυτές τις θέσεις μνήμης. Για το σκοπό αυτό θα χρησιμοποιήσουμε και πάλι το /proc και συγκεκριμένα το "αρχείο" /proc/#/mem. Η πρόσβαση στο αρχείο γίνεται με τις γνωστές εντολές (open/fopen, lseek/fseek, read/fread) αλλά υπάρχουν δύο σημεία που πρέπει να προσεχθούν. Καταρχάς, για να μας επιτραπεί να διαβάσουμε από το mem αρχείο θα πρέπει η δική μας διεργασία να έχει "δεθεί" με τη διεργασία της οποίας τη μνήμη σκοπεύουμε να προσπελάσουμε. Αυτό το "δέσιμο" γίνεται μέσω της ptrace, η οποία μας δίνει το δικαίωμα να παρακολουθούμε τη διεργασία. Ένα δεύτερο σημείο προσοχής είναι ότι θα πρέπει η διεργασία που μας ενδιαφέρει να μην είναι σε κατάσταση εκτέλεσης. Αυτό επιτυγχάνεται στέλνοντάς της το σήμα SIGSTOP.

$ kill -SIGSTOP 1390
$ memread 1390 0x429002e0 0x200 < fib.bin
Reading from Process: 1390 Address: 0x429002e0 Size: 512
Successfully read 512 bytes!

Ο αριθμός των 512 bytes επιλέχθηκε αυθαίρετα, με μόνη προϋπόθεση να είναι μεγαλύτερος από τα όρια του βρόχου που βρήκαμε προηγουμένως.

Ο κώδικας του memread: memread.c.gz[6].

6: ./memread.c.gz

[4.3 Η ανάλυση]

Τώρα που επιτέλους έχουμε στα χέρια μας τον native κώδικα μπορούμε να αρχίσουμε την ανάλυση. Τι μυστικά κρύβει, άραγε, αυτό το μικρό αρχείο των 512 bytes;

Φορτώνουμε το αρχείο στον ht με ht fib.bin και επιλέγουμε disasm/x86 mode (πατώντας space ή F6). Παρακάτω φαίνεται ο κώδικας με κάποια σχόλια:

00000000 89842400d0ffff    mov     [esp-00003000], eax
00000007 81ec24000000      sub     esp, 0x24          
0000000d 83f902            cmp     ecx, 0x2           ;; ecx=n
00000010 0f8c5d010000      jl      0x173              ;; n < 2 ?
00000016 89742414          mov     [esp+0x14], esi    
0000001a 896c2418          mov     [esp+0x18], ebp    
0000001e 897c241c          mov     [esp+0x1c], edi    
00000022 8be9              mov     ebp, ecx           
00000024 4d                dec     ebp                ;; ebp=n-1
00000025 8bd9              mov     ebx, ecx           
00000027 83c3fb            add     ebx, fffffffb      ;; ebx=n-5
0000002a 8bf1              mov     esi, ecx           
0000002c 83c6fe            add     esi, fffffffe      ;; esi=n-2
0000002f 8bc1              mov     eax, ecx           
00000031 83c0fc            add     eax, fffffffc      ;; eax=n-4
00000034 8bd1              mov     edx, ecx           
00000036 83c2fd            add     edx, fffffffd      ;; edx=n-3

00000039 83fd02            cmp     ebp, 0x2           
0000003c 0f8c96000000      jl      0xd8               ;; n-1 < 2 => n < 3 ?
00000042 83fe02            cmp     esi, 0x2           
00000045 7c40              jl      0x87               ;; n-2 < 2 => n < 4 ?
00000047 8954240c          mov     [esp+0xc], edx     ;;
0000004b 89442408          mov     [esp+0x8], eax     ;; Save registers (1)
0000004f 895c2404          mov     [esp+0x4], ebx     ;;
00000053 890c24            mov     [esp], ecx         ;;
00000056 8bca              mov     ecx, edx           
00000058 90                nop                        
00000059 90                nop           
0000005a 90                nop           
0000005b e8a0ffffff        call    0x0                ;; call fib(n-3) 
00000060 89442410          mov     [esp+0x10], eax    
00000064 8b4c2408          mov     ecx, [esp+0x8]     
00000068 90                nop           
00000069 90                nop           
0000006a 90                nop           
0000006b e890ffffff        call    0x0                ;; call fib(n-4)
00000070 8bf8              mov     edi, eax           
00000072 037c2410          add     edi, [esp+0x10]    ;; edi = fib(n-3)+fib(n-4)
00000076 8b0c24            mov     ecx, [esp]         ;;
00000079 8b5c2404          mov     ebx, [esp+0x4]     ;; Restore registers (1)
0000007d 8b442408          mov     eax, [esp+0x8]     ;;
00000081 8b54240c          mov     edx, [esp+0xc]     ;;
00000085 eb02              jmp     0x89

00000087 8bfe              mov     edi, esi           ;; edi = n-2
00000089 83fa02            cmp     edx, 0x2                       
0000008c 7c26              jl      0xb4               ;; n-3 < 2 => n < 5 ?
0000008e 8954240c          mov     [esp+0xc], edx     ;; 
00000092 89442408          mov     [esp+0x8], eax     ;; Save registers (2)
00000096 895c2404          mov     [esp+0x4], ebx     ;;
0000009a 890c24            mov     [esp], ecx         ;;
0000009d 8bc8              mov     ecx, eax           
0000009f e85cffffff        call    0x0                ;; call fib(n-4)
000000a4 8be8              mov     ebp, eax           
000000a6 8b4c2404          mov     ecx, [esp+0x4]     
000000aa 90                nop           
000000ab e850ffffff        call    0x0                ;; call fib(n-5)
000000b0 03c5              add     eax, ebp           ;; eax = fib(n-4) + fib(n-5)
000000b2 eb11              jmp     0xc5               

000000b4 8954240c          mov     [esp+0xc], edx     ;;
000000b8 89442408          mov     [esp+0x8], eax     ;; Save registers (3)
000000bc 895c2404          mov     [esp+0x4], ebx     ;;
000000c0 890c24            mov     [esp], ecx         ;;
000000c3 8bc2              mov     eax, edx           
000000c5 8be8              mov     ebp, eax           
000000c7 03ef              add     ebp, edi           
000000c9 8b0c24            mov     ecx, [esp]         ;;
000000cc 8b5c2404          mov     ebx, [esp+0x4]     ;; Restore registers (2,3)
000000d0 8b442408          mov     eax, [esp+0x8]     ;;
000000d4 8b54240c          mov     edx, [esp+0xc]     ;;

000000d8 83fe02            cmp     esi, 0x2           
000000db 0f8c80000000      jl      0x161              ;; n-2 < 2 => n < 4
000000e1 83fa02            cmp     edx, 0x2           
000000e4 7c39              jl      0x11f              ;; n-3 < 2 => n < 5
000000e6 8bfa              mov     edi, edx           
000000e8 89442408          mov     [esp+0x8], eax     ;;
000000ec 895c2404          mov     [esp+0x4], ebx     ;; Save registers (4)
000000f0 890c24            mov     [esp], ecx         ;;
000000f3 8bc8              mov     ecx, eax           
000000f5 90                nop           
000000f6 90                nop           
000000f7 e804ffffff        call    0x0                ;; call fib(n-4)
000000fc 8944240c          mov     [esp+0xc], eax     
00000100 8b4c2404          mov     ecx, [esp+0x4]     
00000104 90                nop           
00000105 90                nop           
00000106 90                nop           
00000107 e8f4feffff        call    0x0                ;; call fib(n-5)
0000010c 8bf8              mov     edi, eax           
0000010e 037c240c          add     edi, [esp+0xc]     ;; edi = fib(n-4) + fib(n-5)
00000112 8b0c24            mov     ecx, [esp]         ;;
00000115 8b5c2404          mov     ebx, [esp+0x4]     ;; Restore registers (4)
00000119 8b442408          mov     eax, [esp+0x8]     ;;
0000011d eb02              jmp     0x121 

0000011f 8bfa              mov     edi, edx           
00000121 83f802            cmp     eax, 0x2           
00000124 7c37              jl      0x15d              ;; n-4 < 2 => n < 6
00000126 890424            mov     [esp], eax         
00000129 8bf1              mov     esi, ecx           
0000012b 8bcb              mov     ecx, ebx           
0000012d 90                nop           
0000012e 90                nop           
0000012f e8ccfeffff        call    0x0                ;; call fib(n-5)   
00000134 890424            mov     [esp], eax         
00000137 8bce              mov     ecx, esi           
00000139 83c1fa            add     ecx, fffffffa      ;; n' = n - 6
0000013c 90                nop           
0000013d 90                nop           
0000013e 90                nop           
0000013f e8bcfeffff        call    0x0                ;; call fib(n-6)  
00000144 030424            add     eax, [esp]         ;; eax = fib(n-5) + fib(n-6)
00000147 eb14              jmp     0x15d 
00000149 8b6c2418          mov     ebp, [esp+0x18]    
0000014d 8b7c241c          mov     edi, [esp+0x1c]    
00000151 8b742414          mov     esi, [esp+0x14]    
00000155 83c424            add     esp, 0x24          
00000158 e9a377ffff        jmp     ffff7900           
0000015d 03c7              add     eax, edi           
0000015f eb02              jmp     0x163 
00000161 8bc6              mov     eax, esi           
00000163 03c5              add     eax, ebp           
00000165 8b6c2418          mov     ebp, [esp+0x18]    
00000169 8b7c241c          mov     edi, [esp+0x1c]    
0000016d 8b742414          mov     esi, [esp+0x14]    
00000171 eb02              jmp     0x175 
00000173 8bc1              mov     eax, ecx           
00000175 83c424            add     esp, 0x24          
00000178 c3                ret           

00000179 90                nop                        ;; Από εδώ και κάτω είναι άχρηστος (για τους σκοπούς μας) κώδικας
0000017a 90                nop           
0000017b 8bc8              mov     ecx, eax           
0000017d ebca              jmp     0x149 
0000017f 8bc8              mov     ecx, eax           
00000181 ebc6              jmp     0x149 
00000183 8bc8              mov     ecx, eax           
00000185 ebc2              jmp     0x149 
00000187 8bc8              mov     ecx, eax           
00000189 ebbe              jmp     0x149 
0000018b 8bc8              mov     ecx, eax           
0000018d ebba              jmp     0x149 
0000018f 8bc8              mov     ecx, eax           
00000191 ebb6              jmp     0x149 
00000193 8bc8              mov     ecx, eax           
00000195 ebb2              jmp     0x149 
00000197 8bc8              mov     ecx, eax           
00000199 ebae              jmp     0x149   

Το παραπάνω listing αν και μεγάλο σε μήκος είναι στην ουσία αρκετά απλό. Οι βασικοί λόγοι για τους οποίους φαίνεται μπερδεμένο είναι οι συνεχείς μετακινήσεις προς και από το σωρό και οι επαναχρησιμοποίηση των καταχωρητών για εντελώς διαφορετικό σκοπό (πχ ο ebp αρχικά εκφράζει το n-1 ενώ αργότερα, από τη διεύθυνση 0xc7 και πέρα περιέχει κάποιο άλλο άθροισμα).

[]{#advice} Το πρώτο βήμα για την ανάλυση κώδικα σε assembly είναι συνήθως η επισύναψη σχολίων πάνω στον κώδικα όπως παραπάνω. Ένας καλός τρόπος για να συνεχίσουμε είναι η σταδιακή μετατροπή του κώδικα σε κάποια πιο υψηλή και γνώριμη μορφή, αγνοώντας περιττές λεπτομέρειες. Για παράδειγμα, το κομμάτι ανάμεσα στις διευθύνσεις 0x39 και 0x87 θα μπορούσε ως επόμενο βήμα να γραφτεί:

    if (n-1 < 2) goto 0xd8
    if (n-4 < 2) goto 0x87
    edi = fib(n-3) + fib(n-4)
    goto 89
87: edi = n-2
89:

και ύστερα, αναγνωρίζοντας τη δομή if-else που υπάρχει:

    if (n-1 < 2) goto 0xd8
    if (n-4 >= 2) 
        edi = fib(n-3) + fib(n-4);
    else
        edi = n-2;
89:

Ακολουθώντας μια τέτοια διαδικασία τελικά καταλήγουμε στον παρακάτω Java/C κώδικα που εκφράζει πιο ξεκάθαρα την λειτουργία του assembly κώδικα:

int fib_jit(int n)
{
        int C,D;

        if (n < 2)
        return n;

        if (n >= 3) {
                int A,B;

                if (n >= 4)
                        A=fib_jit(n-3)+fib_jit(n-4);            
                else
                        A=n-2;

                if (n >= 5)
                        B=fib_jit(n-4)+fib_jit(n-5);
                else
                        B=n-3;

                C=A+B;
        }
        else
                C=n-1;  

        if (n >= 4) {
                int A,B;

                if (n >= 5)
                        A=fib_jit(n-4)+fib_jit(n-5);
                else
                        A=n-3;

                if (n >= 6)
                        B=fib_jit(n-5)+fib_jit(n-6);
                else
                        B=n-4;

                D=A+B;
        }
        else
                D=n-2; 

        return D+C;
}

Βέβαια, ακόμη και με την παραπάνω μορφή το τι ακριβώς συμβαίνει και γιατί λειτουργεί το κατασκεύασμα του JIT μεταγλωττιστή παραμένει κάπως μυστήριο. Για να ανακαλύψουμε όλη την αλήθεια θα πρέπει να θυμηθούμε την αρχική συνάρτηση fib. Αυτή μπορεί να ξαναγραφτεί ως:

public static int fib(int n) {
    int R;

    if (n >= 2) 
        R=fib(n-1) + fib(n-2);
    else
        R=n;

    return R;   
}

Σας θυμίζει τίποτα; Η δομή if-else που υπάρχει στη αρχική fib φαίνεται να μοιάζει πολύ με τις if-else που υπάρχουν διάσπαρτες στην fib_jit. Για την ακρίβεια, οι δομές που υπάρχουν στην fib_jit είναι η fib() για διάφορες παραστάσεις του n!!!

Για να γίνει πιο κατανοητό το παραπάνω θεωρήστε το:

if (n >= 4)
    A = fib_jit(n-3)+fib_jit(n-4);              
else
    A = n-2;

το οποίο πρακτικά είναι το:

Α=fib(n-2);

Κάνοντας τις παραπάνω αντικαταστάσεις και κάποιες απλοποιήσεις προκύπτει η εξής fib_jit1:

int fib_jit1(int n)
{
        int C,D;

        if (n < 2)
        return n;

        if (n >= 3)
                C = fib(n-2) + fib(n-3);
        else
                C = n-1;  

        if (n >= 4) 
                D = fib(n-3) + fib(n-4);
        else
                D = n-2; 

        return D+C;
}

Χμμ, εμφανίστηκαν πάλι οι γνωστές δομές if-else. Είναι σαν τον κώδικα της μαρμότας... ξαναζείς τον ίδιο κάθε φορά :) Αν αντικαταστήσουμε και αυτές τις δομές με την αντίστοιχη fib προκύπτει:

int fib_jit2(int n)
{

        if (n < 2)
        return n;

        return fib(n-1) + fib(n-2);     
}

H παραπάνω συνάρτηση δεν είναι άλλη από τη αυθεντική fib! Επομένως φτάσαμε στο συμπέρασμα πως fib=fib_jit και άρα όντως η fib_jit λειτουργεί σωστά, αφού στην ουσία είναι η ίδια η fib σε άλλη μορφή.

Τώρα πιστεύω πως είναι σαφές τι "μαγικά" έκανε ο JIT μεταγλωττιστής της Java. Ουσιαστικά έκανε inlining των αναδρομικών κλήσεων της fib σε δύο επίπεδα βάθους! Αυτό είχε ως αποτέλεσμα να αποφευχθεί ένα μέρος του κόστους των κλήσεων συναρτήσεων και επομένως να αυξηθεί η ταχύτητα εκτέλεσης.

[4.4 Το συμπέρασμα]

Τελικά είναι η Java πιο γρήγορη από τη C; Θέλω να ελπίζω πως θα συμφωνήσετε μαζί στο ότι αυτή είναι η λάθος ερώτηση! Αυτό που πρέπει να ρωτήσουμε είναι αν ο JIT μεταγλωττιστής παράγει πιο γρήγορο κώδικα από τον gcc. Η απάντηση είναι πως ειδικά σε αυτή τη περίπτωση ναι και σε γενικές γραμμές παράγει εξίσου καλό κώδικα.

Αν συνεχίσουμε το πείραμα και μεταγλωττίσουμε τη συνάρτηση fib_jit() (σε C) με τον gcc, το τελικό εκτελέσιμο θα έχει ελαφρώς καλύτερη απόδοση από αυτό της Java. Το θέμα εδώ είναι ότι ο JIT μας απαλλάσσει από αυτή τη διαδικασία. Βέβαια, για να υπερασπιστούμε λίγο και τον gcc, o JIT μεταγλωττιστής έχει το πλεονέκτημα πως έχει πρόσβαση και στις πληροφορίες δυναμικής εκτέλεσης του προγράμματος ενώ ο gcc μπορεί μόνο στατικά να ελέγξει τον κώδικα.

Ηθικό δίδαγμα: οι γλώσσες δε μπορούν να συγκριθούν με κριτήριο την ταχύτητα, μόνο οι μεταγλωττιστές τους μπορούν.

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

[5.1 Προηγούμενη Πρόκληση]

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

Καταρχάς, ο emulator περιείχε συνολικά τρία αρχεία: το εκτελέσιμο risc-emu, το change.txt και το log.txt. Παρ' όλο που τα δύο τελευταία αρχεία έχουν .txt κατάληξη, κάθε άλλο παρά text είναι! Θα μπορούσαμε να τα αγνοήσουμε τελείως αλλά για να υπάρχουν εκεί κάποιο ρόλο παίζουν... Επιπλέον, δεν αισθάνεστε την ανάγκη να ικανοποιήσετε την περιέργεια σας; Τι άραγε κρύβουν τα αρχεία αυτά;

Μια προσεκτική παρατήρηση των στοιχείων μπορεί να φέρει στην επιφάνεια τέσσερα σημεία που οδηγούν στη λύση:

1.

Τα αρχεία έχουν κατάληξη .txt. Αν και τα ίδια δεν αποτελούνται από κείμενο, πιθανότατα να έχουν κάποια σχέση με κείμενο. Βέβαια, ίσως οι καταλήξεις να είναι εντελώς παραπλανητικές {δαιμονικό γέλιο ακούγεται στο υπόβαθρο} :)

2.

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

3.

Αν συνδυάσουμε τα ονόματα των δύο αρχείων λαμβάνουμε τη λέξη changelog η οποία είναι ένα αρκετά κοινό αρχείο που ακολουθεί προγράμματα. Το γεγονός αυτό ενισχύει την άποψη πως τα δύο αρχεία συνδέονται μεταξύ τους. Το θέμα είναι πως υπάρχουν άπειροι τρόποι να συνδυαστούν δύο αρχεία!

4.

Αν προσέξετε το κείμενο της πρόκλησης, ο emulator ονομάζεται "RISC-emu v0.42rox". Η ύποπτη λέξη εδώ είναι το rox το οποίο είναι το ανάποδο του xor...

Πιστεύω πως τώρα πια το μυστήριο αρχίζει να ξεδιαλύνει. Αν εφαρμόσετε το δυαδικό τελεστή xor, byte προς byte μεταξύ των δύο αρχείων, θα καταλήξετε στο εξής:

RISC-emu Changelog
--------------------
v0.42
------
- Preliminary support for sorting rom module authentication scheme.
  The random values are stored at registers 0x90, 0x91, 0x92 after startup.
- Added support for the "random reg" instruction

v0.41
-----
- Added Support for the new "swap reg1,reg2" instruction (opcode 0x82)
- Preliminary support for the "random reg" instruction

v0.40
------
- The CPU architecture has changed significantly so this version is 
  an almost complete rewrite.
  *ram 1024 bytes,
  *registers 256, 4-bytes each
  *The instruction size is now 4 bytes.

Οι πιο σημαντικές πληροφορίες που μας δίνει το παραπάνω changelog είναι ότι ο επεξεργαστής έχει μέγεθος εντολών 4 bytes και εφόσον πρόκειται για RISC αρχιτεκτονική αυτό ισχύει για όλες τις εντολές. Επίσης μαθαίνουμε ότι υπάρχει μια εντολή swap με opcode 0x82 και μια εντολή random reg με άγνωστο opcode. Τέλος στους καταχωρητές 0x90, 0x91 και 0x92 αρχικά τοποθετούνται τυχαίες τιμές που πρέπει να ταξινομήσουμε (sorting rom module authentication scheme).

Αν τρέξουμε το πρόγραμμα μερικές φορές μας ζητάει να ταξινομήσουμε τρεις αριθμούς κάθε φορά:

$ ./risc-emu
Welcome to Skynet. Please sort: 975
$ ./risc-emu
Welcome to Skynet. Please sort: 396

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

Αν φορτώσουμε το εκτελέσιμο στον ht και κάνουμε μια βόλτα στο .text section θα παρατηρήσουμε ότι τα πράγματα δεν είναι και τόσο καλά.

|| .......   ;**************************************************************
|| .......   ;  section 13 <.text>
|| .......   ;  virtual address  08048464  virtual size   000004b8
|| .......   ;  file offset      00000464  file size      000004b8
|| .......   ;**************************************************************
|| .......     mov     eax, 0d6b4dc0bh
|| 804846a     mov     cl, 0a5h
|| 804846c     add     eax, 493d0701h
|| 8048471     fcom    double ptr [ecx+5dh]
|| 8048474     cmp     eax, 5d51d6d9h
|| 8048479     add     al, 3
|| 804847b     cmp     eax, 5d51d041h
|| 8048480     mov     ebp, 0aaaaaa2ah

Οι παραπάνω εντολές δεν είναι του είδους που θα περίμενε κάποιος σε ένα τέτοιο πρόγραμμα. Και το entry point που είναι χαμένο; Επιλέγοντας το mode elf/header στον ht βρίσκουμε ότι το entry point είναι στη διεύθυνση 0x80489ea. Χμμ... ελέγχοντας τη διεύθυνση, με έκπληξη διαπιστώνουμε πως δεν ανήκει σε κανένα section!!!

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  ...

11 .text         000004b8  08048464  08048464  00000464  2**2
                  CONTENTS, ALLOC, LOAD, CODE
12 .fini         0000001b  0804891c  0804891c  0000091c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .rodata       00000010  08048938  08048938  00000938  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .data         000000a4  08049960  08049960  00000960  2**5
                  CONTENTS, ALLOC, LOAD, DATA
  ...

To κοντινότερο section είναι το .rodata το οποίο όμως τελειώνει αρκετά πριν το entry point, στη διεύθυνση 0x8048947. Τι γίνεται εδώ;

Θυμηθείτε πως για το φόρτωμα του προγράμματος βασική δομική μονάδα δεν είναι το section αλλά το segment. Με άλλα λόγια, δεν φορτώνονται sections αλλά segments που μπορούν να περιέχουν παραπάνω από ένα sections. Για το risc-emu η λίστα με τα segments είναι (με objdump -p):

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x000000c0 memsz 0x000000c0 flags r-x
  INTERP off    0x000000f4 vaddr 0x080480f4 paddr 0x080480f4 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00000948 memsz 0x00000948 flags rwx
    LOAD off    0x00000960 vaddr 0x08049960 paddr 0x08049960 align 2**12
         filesz 0x000001c0 memsz 0x000001c4 flags rwx
 DYNAMIC off    0x00000a08 vaddr 0x08049a08 paddr 0x08049a08 align 2**2
         filesz 0x000000c8 memsz 0x000000c8 flags rw-
    NOTE off    0x00000108 vaddr 0x08048108 paddr 0x08048108 align 2**2
         filesz 0x00000020 memsz 0x00000020 flags r--

Το πρώτο load segment είναι το code segment και το δεύτερο το data segment. Και πάλι το entry point δε φαίνεται να ανήκει σε κανένα segment!

Το κλειδί στην όλη υπόθεση είναι το πεδίο align, με τιμή 2^12= 0x1000 (μια σελίδα). Η τιμή αυτή μας λέει ταυτόχρονα τρία πράγματα:

1.

Τo segment μπορεί να φορτωθεί μόνο σε κομμάτια πολλαπλάσια των 0x1000 bytes.

2.

Τo segment μπορεί να φορτωθεί μόνο από file offsets πολλαπλάσια των 0x1000 bytes.

3.

Το segment μπορεί να τοποθετηθεί μόνο σε εικονικές διευθύνσεις πολλαπλάσιες των 0x1000 bytes.

Αν το data segment αρχίζει από ένα άλλο σημείο μιας σελίδας τότε φορτώνεται όλη η σελίδα στην οποία ανήκει η αρχή του data segment και έτσι ίσως φορτωθεί ένα μέρος από το τέλος του code segment. Επίσης σε περίπτωση που το text segment δεν τελειώνει σε όρια σελίδας τότε πάλι φορτώνεται όλη η σελίδα στην οποία ανήκει και επομένως ίσως φορτωθεί και ένα μέρος από την αρχή του data segment. Αυτή η τελευταία παρατήρηση είναι ο λόγος που το πρόγραμμα μας με το άφαντο entry point λειτουργεί. Το παρακάτω σχήμα ίσως ξεκαθαρίσει λίγο τα πράγματα:

[IMG]

Τα δύο segments αντιστοιχούνται σε δύο διαφορετικά σημεία της εικονικής μνήμης αλλά υπάρχουν μόνο μια φορά στη φυσική μνήμη. Έτσι η διεύθυνση 0x80489ea (entry point) έχει τα ίδια δεδομένα με τη διεύθυνση 0x80499ea η οποία ανήκει στο data segment! Ουσιαστικά το entry point δείχνει σε κώδικα που βρίσκεται μέσα στο data segment στο αρχείο αλλά έχει φορτωθεί παρέα με το code segment στη μνήμη. Πολύ μπέρδεμα η όλη υπόθεση... ελπίζω να βγάλατε άκρη τελικά ;)

Για να δούμε τι υπάρχει στο entry point αρκεί λοιπόν να πάμε με τον ht στη διεύθυνση 0x80499ea. Εκεί, όμως, δε θα δούμε εντολές διότι ο ht δεν επεξεργάζεται τα bytes που βρίσκονται στο .data section, αφού θεωρεί (δίκαια) πως δε περιέχουν κώδικα. Για να αναλύσει τη συγκεκριμένη διεύθυνση θα πρέπει να δηλώσουμε ότι όντως εκεί υπάρχει κώδικας. Αρκεί στο mode elf/sections headers να επιλέξουμε to section .data και να αλλάξουμε το πεδίο flags ώστε το flag executable να είναι 1 (πατώντας F4 όταν είμαστε στα flags μπαίνουμε σε edit mode). Τώρα στο elf/image θα έχει αναλυθεί και ο κώδικας στο entry point:

|| 80499ea     mov     ecx, 4b8h           
|| 80499ef     mov     eax, 8048464h       
|| 80499f4     xor     byte ptr [eax], 55h 
|| 80499f7     inc     eax                 
|| 80499f8     dec     ecx                 
|| 80499f9     jnz     80499f4h            
|| 80499fb     jmp     8049464h            
|| 8049a00     add     [eax], al           
|| 8049a02     add     [eax], al   

Ο κώδικας είναι πολύ απλός: αρχίζοντας από τη διεύθυνση 0x8048464 και για τα επόμενα 0x4b8 bytes, το κάθε byte (b) αντικαθίσταται με το (b xor 0x55). Πρόκειται για έναν στοιχειώδη αλγόριθμο κρυπτογράφησης. Η διεύθυνση 0x8048464 (όπως και η διεύθυνση 0x8049464) είναι η αρχή του .text section και η τιμή 0x4b8 το μήκος του section. Αυτός είναι και ο λόγος που τα περιεχόμενα του .text δεν έβγαζαν κάποιο νόημα όταν τα εξετάσαμε στην αρχή.

Από εδώ και πέρα υπάρχουν δύο τρόποι για να προχωρήσουμε. Ο πρώτος είναι να "παγώσουμε" το πρόγραμμα αμέσως μετά την αποκρυπτογράφηση και να αποθηκεύσουμε την εικόνα του σε κάποιο αρχείο (βλέπε προηγούμενο άρθρο Packed Executables - Συμπιεσμένα Εκτελέσιμα). Αυτό μπορεί να επιτευχθεί χρησιμοποιώντας το gdb, με την τοποθέτηση ενός breakpoint στη διεύθυνση 0x80489fb. Αφού "χτυπήσει" το breakpoint μπορούμε με την εντολή dump να αποκτήσουμε την εικόνα της μνήμης του προγράμματος.

Επειδή ο αλγόριθμος κρυπτογράφησης είναι εξαιρετικά απλός, μπορούμε με κάποιο πρόγραμμα ή hex editor να αποκρυπτογραφήσουμε μόνοι μας το .text section. Αν θέλουμε να μπορεί να εκτελείται το πρόγραμμα, δε θα πρέπει να αμελήσουμε να διορθώσουμε το entry point η τουλάχιστον να αδρανοποιήσουμε τον αλγόριθμο κρυπτογράφησης (πχ αλλάζοντας το κλειδί από 0x55 σε 0x00).

Από εδώ και πέρα μένει η ανάλυση του κώδικα που είναι το πιο χρονοβόρο και κουραστικό μέρος. Δε θα αναφερθώ περισσότερο σε αυτή διότι στηρίζεται σε μεγάλο βαθμό στην εμπειρία του αναλυτή. Μερικές συμβουλές μπορείτε να βρείτε εδώ[7]. To μόνο περίεργο που μπορεί να συναντήσετε είναι το γεγονός ότι η δομή ελέγχου switch υλοποιείται σε χαμηλό επίπεδο (από τον gcc) με εμφωλευμένες δομές if, ακολουθώντας τη λογική της δυαδικής αναζήτησης. Για παράδειγμα το:

7: 05_rce4-4.html#advice

switch(x) {
        case 1: ...; break;
        case 2: ...; break;
        case 3: ...; break;
        case 4: ...; break;
        case 5: ...; break;
        case 6: ...; break;
        default: ...; break;
}

μπορεί να μεταγλωττιστεί σα να είχατε γράψει κώδικα της μορφής:

if (x<4) {
        if (x<=2) {
                if (x==1) ...;
                if (x==2) ...;
        }
        else /* x==3 */
                ...;
}
else if (x<=6) 
        if (x<=5) {
                if (x==4) ...;
                if (x==5) ...;
        }
        else /* x==6 */
                ...;
}
else /* default */
        ...;

O επεξεργαστής έχει εντολές των 4 bytes με το byte 0 να είναι το opcode. Οι διαθέσιμες εντολές είναι:

--------------------------------------------------------------------------------

\ Opcode Περιγραφή Όνομα

HALT 0x00 halt

ADD 0x01 reg{b1} = reg{b2} + reg{b3}

LOADIL 0x02 low word(reg{b1}) = (b2b3)

LOADIH 0x03 high word(reg{b1}) = (b2b3)

STORE 0x04 mem{(b1b2)} = reg{b3}

LOAD 0x05 reg{b3} = mem{(b1b2)}

INDSTORE 0x06 mem{reg{b1} + reg{b2}} = b3

INDLOAD 0x07 b3 = mem{reg{b1} + reg{b2}}

BE 0x08 if (reg{b1} == reg{b2}) ip += 4 * b3

BNE 0x09 if (reg{b1} != reg{b2}) ip += 4 * b3

BU 0x0A ip += 4 * (b1b2)

BR 0x0B ip += reg{b1}

PRINT 0x0C τύπωσε τα δεδομένα με αρχή τη θέση μνήμης (b1b2) ως δεκαεξαδικό, χαρακτήρα η συμβολοσειρά (b3=0,1,2)

SETLT 0x0D if (reg{b2} < reg{b3}) reg{b1} = 1; else reg{b1} = 0;

SWAP 0x82 reg{b1} <=> reg{b2}

RANDOM 0xFF reg{b1} = random(0,9)

--------------------------------------------------------------------------------

όπου reg[i]: το περιεχόμενο του καταχωρητή i, mem[i]: το περιεχόμενο της θέσης μνήμης i, (ij): η λέξη (16-bit) που δημιουργείται από τα bytes i και j με i το λιγότερο σημαντικό byte, ip: δείκτης για την επόμενη εντολή που θα εκτελεστεί, bi: το byte i της τρέχουσας εντολής.

Για να ταξινομήσουμε τους τρεις αριθμούς ένας απλός αλγόριθμος είναι:

if (r91 < r90) swap(r90, r91);
if (r92 < r90) swap(r90, r92);
if (r92 < r91) swap(r91, r92);

δηλαδή στη γλώσσα του δικού μας RISC:

setlt r9,r91,r90
be r0,r0,+1
swap r91,r90
setlt r9,r92,r90
be r9,r0,+1
swap r92,r90
setlt r9,r92,r91
be r9,r0,+1
swap r92,r91

Αρκεί να σώσουμε το δυαδικό κώδικα σε ένα αρχείο και να το φορτώσουμε στον εξομοιωτή με risc-emu <αρχείο>. Βέβαια, μην περιμένετε ο εξομοιωτής να επιβεβαιώσει την ορθότητα της λειτουργίας του προγράμματος... όπως λέει και το changelog η υποστήριξη για το συγκεκριμένο χαρακτηριστικό δεν είναι πλήρης :)

[5.2 Hall of Fame]

Αυτή τη φορά έλαβα μόνο μια απάντηση, η οποία όμως ήταν πραγματικά αξιόλογη!

Συγχαρητήρια, λοιπόν, στον Γιώργο Πρέκα για τη λύση[8] του !

8: ./prekas_geo-rce2sol.tar.gz

Από το email του Γιώργου:
"Η λύση αποτελείται από τα εξής αρχεία:

changelog.txt, το αποκρυπτογραφημένο changelog του εξομοιωτή.

geo.c, το πρόγραμμα που αποκρυπτογραφεί το changelog.txt αρκεί να υπάρχουν
στον κατάλογο εκτέλεσής του τα αρχεία change.txt και log.txt.

newprg, ο αποκρυπτογραφημένος εξομοιωτής. Φαίνεται πως κάποιο λάθος έκανα με
την εντολή dump του gdb και το αρχείο περιέχει δύο images: Ένα
αποκρυπτογραφημένο και ένα κρυπτογραφημένο. Δεν το διόρθωσα γιατί φοβόμουν
ότι μετά θα χάνονταν όλα τα σχόλια που είχα γράψει στον ht.

newprg.ht*, αρχεία για τον ht.

rom, αποτελεί ένα rom module, του οποίου η εκτέλεση μπορεί να εξομοιωθεί με
την εντολή ./risc-emu rom, και αφού πρώτα ταξινομήσει τις τιμές των
καταχωρητών 0x90, 0x91, 0x92, εκτελεί έναν ατέρμονα βρόχο, ώστε να μπορέσει
τελικά να ανοιχτεί η περιβόητη θήκη. Σημείωση: Από όσα κατάλαβα, για να
θεωρηθεί ένα rom module έγκυρο πρέπει να ταξινομήσει τους συγκεκριμένους
καταχωρητές. Αυτό δεν το ελέγχει ο εξομοιωτής, αν και έπειτα αφού
αποκρυπτογράφησα το changelog.txt, κατάλαβα πως δεν είναι ολοκληρωμένη η
υποστήριξη της συγκεκριμένης λειτουργίας."

Τον source κώδικα της πρόκλησης και τη δική μου λύση μπορείτε να την κατεβάσετε από εδώ[9].

9: ./alf-rce2sol.tar.gz

Ευτυχώς ή δυστυχώς, αυτό το άρθρο είναι μάλλον το τελευταίο της σειράς οπότε δεν υπάρχει επόμενη πρόκληση. Πάντως, όσοι πραγματικά ενδιαφέρονται για γρίφους τέτοιου είδος δεν έχουν παρά να ψάξουν στο internet όπου θα βρουν αρκετές σχετικές σελίδες. Το μόνο αρνητικό στην όλη υπόθεση είναι πως οι περισσότερες από αυτές ασχολούνται με εκτελέσιμα σε περιβάλλον Win32. Καλή συνέχεια!

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