1253 γραμμές
87 KiB
Markdown
1253 γραμμές
87 KiB
Markdown
+++
|
||
title = 'Reverse Engineering σε περιβάλλον Linux, Μέρος 3'
|
||
date = '2006-02-01T00:00:00Z'
|
||
description = ''
|
||
author = 'Αλέξανδρος Φραντζής'
|
||
issue = ['Magaz 35']
|
||
issue_weight = 5
|
||
+++
|
||
----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
*To άρθρο αυτό είναι το τέταρτο της σειράς \"Reverse Engineering σε περιβάλλον Linux\". Σκοπός της σειράς είναι να εξοικοιώσει τους αναγνώστες με τις βασικές
|
||
τεχνικές του Reverse Engineering, με έμφαση στο πως αυτές μπορούν να εφαρμοστούν στο Linux, και να τους προσφέρει πιο βαθιές γνώσεις για τη λειτουργία του
|
||
συστήματος τους.*
|
||
|
||
----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
**1. Εισαγωγή**
|
||
--------------------------------------
|
||
|
||
**2. Εικονικές Μηχανές**
|
||
-----------------------------------------------
|
||
|
||
- [2.1 Εισαγωγή](#ss2.1)
|
||
- [2.2 Ενδιάμεσες Μορφές Κώδικα](#ss2.2)
|
||
- [2.3 Java Virtual Machine](#ss2.3)
|
||
|
||
**3. Μερικές σκέψεις για το μέλλον του RCE - Obfuscation**
|
||
---------------------------------------------------------------------------------
|
||
|
||
**4. Hands-on παράδειγμα: Java vs C**
|
||
------------------------------------------------------------
|
||
|
||
- [4.1 Το πείραμα](#ss4.1)
|
||
- [4.2 Η αναζήτηση](#ss4.2)
|
||
- [4.3 Η ανάλυση](#ss4.3)
|
||
- [4.4 Το συμπέρασμα](#ss4.4)
|
||
|
||
**5. Πρόκληση**
|
||
--------------------------------------
|
||
|
||
- [5.1 Προηγούμενη Πρόκληση](#ss5.1)
|
||
- [5.2 Hall of Fame](#ss5.2)
|
||
|
||
|
||
### [1. Εισαγωγή]{#s1}
|
||
|
||
Καλωσήρθατε στο τέταρτο άρθρο (η μέτρηση αρχίζει από το 0) για Reverse Code Engineering σε Linux!
|
||
|
||
Το γενικό θέμα με το οποίο ασχολείται το παρόν άρθρο είναι οι εικονικές μηχανές (Virtual Machines).
|
||
|
||
Στο πρώτο τμήμα θα ασχοληθούμε εν συντομία με την ιστορία και το γενικό σχεδιασμό των VMs.
|
||
|
||
Στο δεύτερο τμήμα εκφράζονται μερικές σκέψεις μου για το μέλλον του RCE και το πως σχετίζεται η τεχνική του obfuscation με αυτό.
|
||
|
||
Στο τρίτο τμήμα του άρθρου, θα μάθουμε γιατί ο κώδικας Java μπορεί να είναι πιο γρήγορος από τον ίδιο κώδικα σε C.
|
||
|
||
Στο τέταρτο και τελευταίο τμήμα θα δώσουμε μια ενδεικτική λύση για την προηγούμενη πρόκληση και βέβαια το Hall of Fame.
|
||
|
||
Καλό RCE!
|
||
|
||
|
||
### [2. Εικονικές Μηχανές]{#s2}
|
||
|
||
### [2.1 Εισαγωγή]{#ss2.1}
|
||
|
||
Η εικονική μηχανή (Virtual Μachine), όπως δηλώνει και το όνομα της, είναι μια μηχανή που υφίσταται μόνο στο βασίλειο του αφηρημένου. Πρόκειται για επεξεργαστή
|
||
υλοποιημένο μόνο με λογισμικό, με δικό του σετ εντολών και ιδιαίτερων χαρακτηριστικών.
|
||
|
||
Μια προφανής και σημαντική χρήση των εικονικών μηχανών είναι η εξομοίωση και η μελέτη υπαρχόντων ή και υπό σχεδίαση hardware συστημάτων. Για αυτή την κατηγορία
|
||
έχει επικρατήσει η ονομασία εξομοιωτής (emulator) και σήμερα για σχεδόν όλα τα υπολογιστικά συστήματα περασμένων εποχών υπάρχει ένας εξομοιωτής.
|
||
|
||
Ένας δεύτερος πολύ σημαντικός λόγος για την ύπαρξη VMs είναι η χρήση τους ως ένα \"μονωτικό στρώμα\" ανάμεσα στις εφαρμογές και το υλικό. Μια εφαρμογή που είναι
|
||
σχεδιασμένη για μια VM μπορεί να εκτελεστεί σε οποιοδήποτε επεξεργαστή για τον οποίο υπάρχει ένας διερμηνευτής για τον κώδικα της VM. Αυτή είναι η νοοτροπία του
|
||
\"Προγραμμάτισε μια φορά, τρέξε παντού\" (Code Once, Run Everywhere).
|
||
|
||
Πολύ συχνά οι εικονικές μηχανές χρησιμοποιούνται για να δώσουν στο προγραμματιστή την ψευδαίσθηση πως δουλεύει σε ένα ιδιαίτερα εξελιγμένο σύστημα, με
|
||
χαρακτηριστικά ειδικά σχεδιασμένα για την εργασία του. Τέτοιες μηχανές χρησιμοποιούνται σε παιχνίδια όπου βασικές εντολές του εικονικού επεξεργαστή μπορεί να
|
||
είναι για παράδειγμα \"μετακίνησε το sprite A από τη θέση x στη θέση y\".
|
||
|
||
Οι εικονικές μηχανές, αν και είναι πολύ στη μόδα σήμερα, δεν αποτελούν καινούργιο φαινόμενο. Ήδη από το 1970 η ανάγκη διαχωρισμού των διάφορων φάσεων της
|
||
μεταγλώττισης ενός προγράμματος οδήγησε τους δημιουργούς μεταγλωττιστών στην εισαγωγή και χρήση ενδιάμεσων μορφών κώδικα. Μια πολύ γνωστή ενδιάμεση μορφή είναι
|
||
η P-Code που χρησιμοποιήθηκε για τους μεταγλωττιστές της γλώσσας Pascal. Πολύ σύντομα η P-Code έπαψε να χρησιμοποιείται μόνο στους μεταγλωττιστές και έγινε η
|
||
βάση μιας εικονική μηχανής για το σύστημα της UCSD Pascal.
|
||
|
||

|
||
|
||
Παρ\' όλο που που η δεκαετία του \'70 μοιάζει μακρινή, τα πράγματα δεν έχουν αλλάξει πολύ στον συγκεκριμένο τομέα. Οι σύγχρονες γλώσσες προγραμματισμού
|
||
χρησιμοποιούν το ίδιο μοντέλο με μια μικρή αλλά σημαντική προσθήκη. Για λόγους ταχύτητας, οι πιο εξελιγμένοι διερμηνευτές δεν εκτελούν τον κώδικα σε ενδιάμεση
|
||
μορφή αλλά τον μετατρέπουν στη γλώσσα μηχανής του επεξεργαστή στον οποίο τρέχουν (native code). Η μετατροπή αυτή γίνεται την ώρα της εκτέλεσης και ονομάζεται
|
||
Just-In-Time (JIT) μεταγλώττιση, ενώ η κλασική μεταγλώττιση έχει την ονομασία Ahead-Of-Time (AOT).
|
||
|
||
### [2.2 Ενδιάμεσες Μορφές Κώδικα]{#ss2.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>).
|
||
Για το .NET στο Linux υπάρχει το open source Mono Project ( <http://www.mono-project.com>).
|
||
|
||
### [2.3 Java Virtual Machine]{#ss2.3}
|
||
|
||
Η ενδιάμεση μορφή στην οποία αποθηκεύονται τα προγράμματα σε Java είναι τα Java Bytecodes. Κατά την εκτέλεση της Java Virtual Machine (JVM) κάθε thread
|
||
αποτελείται από τα παρακάτω στοιχεία:
|
||
|
||
- PC (μετρητή προγράμματος): κάθε στιγμή δείχνει τη διεύθυνση της τρέχουσας εντολής.
|
||
- Normal Stack: περιέχει κυρίως τα πλαίσια (frames) των συναρτήσεων και ενδιάμεσες τιμές.
|
||
- Heap: περιοχή από την οποία μοιράζεται η μνήμη στα αντικείμενα.
|
||
- Method Area: περιοχή η οποία περιέχει τα bytecode των μεθόδων και τις σταθερές των κλάσεων.
|
||
|
||
Κάθε στιγμή το πρόγραμμα βρίσκεται μέσα σε μία συνάρτηση και αποθηκεύει τις τοπικές πληροφορίες σε ένα πλαίσιο στον κανονικό σωρό. Το πλαίσιο περιέχει το σωρό
|
||
τελεστέων, έναν πίνακα τοπικών μεταβλητών και κάποιες άλλες πληροφορίες που σχετίζονται με τα δεδομένα της κλάσης στην οποία ανήκει η μέθοδος. Η πρώτη τοπική
|
||
μεταβλητή (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>).
|
||
Όμως, μπορούμε να πάμε ακόμα πιο πέρα, χρησιμοποιώντας κάποιον decompiler για Java ο οποίος θα προσπαθήσει να μας δώσει τον αρχικό πηγαίο κώδικα! Δυο πολύ
|
||
γνωστοί Java decompilers είναι ο MOCHA ( <http://www.brouhaha.com/~eric/computers/mocha.html>) και ο JAD ( <http://kpdus.tripod.com/jad.html>).
|
||
|
||
|
||
### [3. Μερικές σκέψεις για το μέλλον του RCE - Obfuscation]{#s3}
|
||
|
||
Γλώσσες όπως η 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]{#s4}
|
||
|
||
> \...και την 42η μέρα ο Θεός δημιούργησε την C. Βλέποντας την απειλή οι δυνάμεις του Κακού αποφάσισαν να αντεπιτεθούν\... και έτσι γεννήθηκε η Java.
|
||
|
||
Η Java από τότε που δημιουργήθηκε κλήθηκε να αντιμετωπίσει τις κυρίαρχες τότε (αλλά και σήμερα) C και C++. Η σύγκριση ήταν αναπόφευκτη΄ η Java διαφημιζόταν ως
|
||
μια πιο κομψή και ασφαλής C++, μια γλώσσα ικανή να χρησιμοποιηθεί σε ένα ευρύ πεδίο εφαρμογών, από ιστοσελίδες μέχρι ενσωματωμένες συσκευές. Όπως συνηθίζεται
|
||
στον κόσμο των υπολογιστών, ένας \"ιερός\" πόλεμος ξέσπασε.
|
||
|
||
Οι πολέμιοι της Java είχαν κυρίως δύο λόγους για τους οποίους ήθελαν να την κάψουν στην πυρά. Καταρχάς υπήρχαν εκείνοι που στο άκουσμα της λέξης
|
||
\"αντικειμενοστρεφής\" έβγαζαν σκόρδα και προσπαθούσαν να ξορκίσουν τα δαιμόνια. Σε αυτούς, βέβαια, δεν άρεσε ούτε η C++, η οποία όμως έπαιρνε άφεση διότι
|
||
μπορούσε απλώς να χρησιμοποιηθεί ως βελτιωμένη C. Ο δεύτερος και πιο καταδικαστικός λόγος ενάντια στην Java ήταν ότι ήταν αργή. Ειδικά σε ότι είχε σχέση με
|
||
γραφικές διεπαφές (GUI) ήταν εκνευριστικά αργή και επιπλέον είχε μια μέτριας ποιότητας (εμφανισιακά, τουλάχιστον) βιβλιοθήκη χειριστηριών.
|
||
|
||
Σήμερα, αρκετό καιρό ύστερα από την έναρξη της διαμάχης, τα πράγματα φαίνεται να έχουν μπει σε μια τάξη. Η Java έχει καταφέρει να βελτιωθεί θεαματικά στο φλέγον
|
||
θέμα της απόδοσης και έτσι έχει κατακτήσει μια αρκετά υψηλή θέση στις καρδιές των προγραμματιστών αλλά και των διοικητικών στελεχών. Η C παραμένει κυρίαρχη στον
|
||
τομέα για τον οποίο σχεδιάστηκε, τον προγραμματισμό συστημάτων, ενώ η C++ απολαμβάνει μια σταθερή θέση σε εφαρμογές που επωφελούνται από την αντικειμενοστρεφή
|
||
προσέγγιση και ταυτόχρονα έχουν αυξημένες απαιτήσεις απόδοσης.
|
||
|
||
Δυστυχώς στις απόψεις πολλών η Java και γενικά οι διερμηνευόμενες (interpreted) γλώσσες έχουν στιγματιστεί από την αργοπορία που τις χαρακτήριζε στα πρώτα τους
|
||
βήματα. Στο πείραμα που ακολουθεί θα εξετάσουμε και θα εξηγήσουμε την απρόσμενα καλή απόδοση ενός προγράμματος σε Java.
|
||
|
||
### [4.1 Το πείραμα]{#ss4.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 Η αναζήτηση]{#ss4.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](./memread.c.gz).
|
||
|
||
### [4.3 Η ανάλυση]{#ss4.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 Το συμπέρασμα]{#ss4.4}
|
||
|
||
Τελικά είναι η Java πιο γρήγορη από τη C; Θέλω να ελπίζω πως θα συμφωνήσετε μαζί στο ότι αυτή είναι η λάθος ερώτηση! Αυτό που πρέπει να ρωτήσουμε είναι αν ο JIT
|
||
μεταγλωττιστής παράγει πιο γρήγορο κώδικα από τον gcc. Η απάντηση είναι πως ειδικά σε αυτή τη περίπτωση ναι και σε γενικές γραμμές παράγει εξίσου καλό κώδικα.
|
||
|
||
Αν συνεχίσουμε το πείραμα και μεταγλωττίσουμε τη συνάρτηση fib\_jit() (σε C) με τον gcc, το τελικό εκτελέσιμο θα έχει ελαφρώς καλύτερη απόδοση από αυτό της
|
||
Java. Το θέμα εδώ είναι ότι ο JIT μας απαλλάσσει από αυτή τη διαδικασία. Βέβαια, για να υπερασπιστούμε λίγο και τον gcc, o JIT μεταγλωττιστής έχει το
|
||
πλεονέκτημα πως έχει πρόσβαση και στις πληροφορίες δυναμικής εκτέλεσης του προγράμματος ενώ ο gcc μπορεί μόνο στατικά να ελέγξει τον κώδικα.
|
||
|
||
Ηθικό δίδαγμα: οι γλώσσες δε μπορούν να συγκριθούν με κριτήριο την ταχύτητα, μόνο οι μεταγλωττιστές τους μπορούν.
|
||
|
||
|
||
### [5. Πρόκληση]{#s5}
|
||
|
||
### [5.1 Προηγούμενη Πρόκληση]{#ss5.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
|
||
λειτουργεί. Το παρακάτω σχήμα ίσως ξεκαθαρίσει λίγο τα πράγματα:
|
||
|
||

|
||
|
||
Τα δύο 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).
|
||
|
||
Από εδώ και πέρα μένει η ανάλυση του κώδικα που είναι το πιο χρονοβόρο και κουραστικό μέρος. Δε θα αναφερθώ περισσότερο σε αυτή διότι στηρίζεται σε μεγάλο βαθμό
|
||
στην εμπειρία του αναλυτή. Μερικές συμβουλές μπορείτε να βρείτε [εδώ](05_rce4-4.html#advice). To μόνο περίεργο που μπορεί να συναντήσετε είναι το γεγονός ότι η
|
||
δομή ελέγχου switch υλοποιείται σε χαμηλό επίπεδο (από τον gcc) με εμφωλευμένες δομές if, ακολουθώντας τη λογική της δυαδικής αναζήτησης. Για παράδειγμα το:
|
||
|
||
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]{#ss5.2}
|
||
|
||
Αυτή τη φορά έλαβα μόνο μια απάντηση, η οποία όμως ήταν πραγματικά αξιόλογη!
|
||
|
||
Συγχαρητήρια, λοιπόν, στον *Γιώργο Πρέκα* για τη [λύση](./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 κώδικα της πρόκλησης και τη δική μου λύση μπορείτε να την κατεβάσετε από [εδώ](./alf-rce2sol.tar.gz).
|
||
|
||
Ευτυχώς ή δυστυχώς, αυτό το άρθρο είναι μάλλον το τελευταίο της σειράς οπότε δεν υπάρχει επόμενη πρόκληση. Πάντως, όσοι πραγματικά ενδιαφέρονται για γρίφους
|
||
τέτοιου είδος δεν έχουν παρά να ψάξουν στο internet όπου θα βρουν αρκετές σχετικές σελίδες. Το μόνο αρνητικό στην όλη υπόθεση είναι πως οι περισσότερες από
|
||
αυτές ασχολούνται με εκτελέσιμα σε περιβάλλον Win32. Καλή συνέχεια!
|
||
|