Magaz, The Greek Linux Magazine
Magaz Logo

Επόμενο  Προηγούμενο  Περιεχόμενα

6. Hands-on Παράδειγμα

Έστω ότι μια μέρα πέφτει στα χέρια σας το παρακάτω εκτελέσιμο : rce2.bz2 (1.7k)

Όλο χαρά το εκτελείτε:

bash$ rce2
Say the password: sesame
What???
bash$ rce2
Say the password: kuku
What???

Ποιο να είναι άραγε το μυστικό password;

Ας ετοιμαστούμε λοιπόν για αντιμετωπίσουμε ατελείωτα listing με εκατομμύρια γραμμές ακατανόητου κώδικα! Η ας δοκιμάσουμε κάτι πιο απλό:

bash$ strings rce2
...

Η strings τυπώνει όλες τις εκτυπώσιμες ASCII ακολουθίες που υπάρχουν στο εκτελέσιμο και έχουν μήκος πάνω από 4 χαρακτήρες (default).

Μέσα στη λίστα με τα strings θα παρατηρήσετε κάποιο πιο ενδιαφέρον από τα υπόλοιπα :) Σπάνια βέβαια συμβαίνει να υπάρχουν plain-text κωδικοί μέσα στο εκτελέσιμο αλλά δε χάνουμε τίποτα να δοκιμάσουμε!

Για εκπαιδευτικούς σκοπούς, θεωρήστε ότι η προηγούμενη διαδικασία δε απέδωσε καρπούς. Ας δούμε τι μπορούμε να μάθουμε για τη ροή του προγράμματος.

bash$ ltrace -i -o rce2.ltr rce2
Say the password: sesame
What???
bash$ cat rce2.ltr
[080483b5] __libc_start_main(0x0804851c, 1, 0xbffff784, 0x0804830c, 0x08048594
<unfinished ...>
[080484d5] printf("%s: ", "Say the password")                               =
18
[080484e1] fflush(0x40150340)                                               = 0
[080484f5] fgets("sesame\n", 20, 0x401501e0)                                =
0xbffff700
[0804849c] isspace(10, 0x40153234, 0xbffff700, 0xbffff784, 0xbffff6e8)      =
8192
[0804849c] isspace(101, 0x40153234, 0xbffff700, 0xbffff784, 0xbffff6e8)     = 0
[08048557] puts("What???")                                                  = 8
[ffffffff] +++ exited (status 0) +++

Μόλις αποκτήσαμε δύο σπουδαίες πληροφορίες.
Κοιτάξτε καλά τη __libc_start_main().
Συνεχίστε να την κοιτάζετε.
H __libc_start_main(), εκτός των άλλων, φροντίζει να κληθεί και το δικό μας κυρίως πρόγραμμα.
Κοιτάξτε λίγο ακόμα...
H __libc_start_main() πρέπει να γνωρίζει που βρίσκεται ο δικός μας κώδικας. Επομένως...
Για να σας βγάλω από την αγωνία, σας λέω πως η πρώτη παράμετρος της __libc_start_main() πρόκειται για τη διεύθυνση της main (0x0804851c)!

Αυτή η πρώτη πληροφορία οδηγεί στη δεύτερη. Παρατηρήστε ότι όλες οι διευθύνσεις μετά την __libc_start_main() και μέχρι πριν την puts() είναι μικρότερες από την αρχή της main(). Άρα είναι ασφαλές να υποθέσουμε πως δεν ανήκουν σε αυτή αλλά σε κάποια άλλη συνάρτηση.

Το τρίτο πράγμα που μάθαμε, είναι ότι στη διεύθυνση 0x08048557 έχει ήδη αποφασιστεί αν το password μας είναι σωστό ή όχι.

Ας χρησιμοποιήσουμε το βαρύ πυροβολικό...

bash$ gdb rce2
(no debugging symbols found)...(gdb) break main
Function "main" not defined.
(gdb)

Ουπς. Το εκτελέσιμο δεν περιέχει σύμβολα αλλά εμείς ξέρουμε τη διεύθυνση της main()!
(gdb) break *0x804851c
Breakpoint 1 at 0x804851c
(gdb) r
Starting program: /home/alf/projects/magaz/issue1/rce2
(no debugging symbols found)...
Breakpoint 1, 0x0804851c in printf ()
(gdb) x/30i $eip
0x804851c <printf+408>: push   ebp
0x804851d <printf+409>: mov    ebp,esp
0x804851f <printf+411>: push   edi
0x8048520 <printf+412>: push   esi
0x8048521 <printf+413>: sub    esp,0x20
0x8048524 <printf+416>: and    esp,0xfffffff0
0x8048527 <printf+419>: push   esi
0x8048528 <printf+420>: push   0x13
0x804852a <printf+422>: lea    esi,[ebp-40]
0x804852d <printf+425>: push   esi
0x804852e <printf+426>: push   0x80485bd
0x8048533 <printf+431>: call   0x80484bc <printf+312>
0x8048538 <printf+436>: mov    edi,0x80485ce
0x804853d <printf+441>: mov    ecx,0xb
0x8048542 <printf+446>: cld
0x8048543 <printf+447>: add    esp,0x10
0x8048546 <printf+450>: repz cmps ds:[esi],es:[edi]
0x8048548 <printf+452>: jne    0x8048564 <printf+480>
0x804854a <printf+454>: sub    esp,0xc
0x804854d <printf+457>: push   0x80485d9
0x8048552 <printf+462>: call   0x8048354 <puts>
0x8048557 <printf+467>: add    esp,0x10
0x804855a <printf+470>: lea    esp,[ebp-8]
0x804855d <printf+473>: pop    esi
0x804855e <printf+474>: xor    eax,eax
0x8048560 <printf+476>: pop    edi
0x8048561 <printf+477>: leave
0x8048562 <printf+478>: ret
0x8048563 <printf+479>: nop
0x8048564 <printf+480>: sub    esp,0xc

O GDB νομίζει πως βρισκόμαστε 408 bytes από την αρχή της printf για τους γνωστούς λόγους. Πριν αρχίσουμε σαν παλαβοί να κάνουμε single step ας δούμε τι μπορούμε να συνάγουμε από το listing.

push   ebp
mov    ebp,esp
push   edi
push   esi
Καταρχάς η main είναι frame-based κάτι που φαίνεται από το γνώριμο πρόλογο. Επίσης σώζονται στο σωρό οι καταχωρητές esi και edi ώστε να μπορούν να ανακτηθούν οι τιμές τους πριν το τέλος της συνάρτησης. Το γιατί γίνεται αυτό πρόκειται για μια εσωτερική υπόθεση του compiler (o gcc διατηρεί τους ebx, esi και edi κατά τις κλήσεις διότι τους χρησιμοποιεί για δικά του θέματα (πχ προσωρινή αποθήκευση τιμών).
sub    esp,0x20
and    esp,0xfffffff0
Μετά δεσμεύεται χώρος στο σωρό για 0x20 bytes και επίσης ο esp μειώνεται(πιθανότατα) και άλλο ώστε να έρθει σε όριo των 16 bytes. Η όλη διαδικασία είναι αποτέλεσμα του optimization και δεν είναι απαραίτητο πως το πρόγραμμα θα χρησιμοποιήσει όλη τη μνήμη που δεσμεύτηκε τελικά.
push   esi <--- ??
push   0x13
lea    esi,[ebp-40]
push   esi
push   0x80485bd
call   0x80484bc <printf+312>
...
add    esp,0x10
Ακολουθεί ένα μυστηριώδες "push esi" και ύστερα μια κλασική κλήση συνάρτησης. Η συνάρτηση δέχεται τρεις παραμέτρους: func_0x804845c( 0x80485bd, 0x13, ebp-40). H τελευταία παράμετρος είναι η διεύθυνση μιας τοπικής μεταβλητής, αφού έχει αρνητικό offset από τον ebp. Το αναμενόμενο "add esp, X" (διόρθωση του σωρού) βρίσκεται λίγες γραμμές πιο κάτω. Ο λόγος που δε βρίσκεται αμέσως μετά την call, είναι ότι ο gcc αποφάσισε να αλλάξει τη σειρά των εντολών για λόγους optimization (έχει να κάνει με τα pipelines του pentium). Όλα εντάξει;

Ελπίζω να μην απαντήσατε "ναι"! Κάτι δεν πάει καλά σε όσα έχω πει ως τώρα: γιατί δέχτηκα με τόση σιγουριά πως η συνάρτηση δέχεται τρεις παραμέτρους, ενώ όχι μόνο γίνονται τέσσερα push πριν από αυτή αλλά κυρίως, ο σωρός διορθώνεται κατά 0x10 = 4*4 bytes (4 bytes για κάθε παράμετρο). Λοιπόν, εκτός από το γεγονός ότι εγώ έγραψα το πρόγραμμα και το ξέρω :), υπάρχει ένας επιπλέον λόγος. Όταν γίνεται το πρώτο "push esi", ο esi δεν έχει αρχικοποιηθεί μέσα στη main() και έτσι έχει μια άγνωστη τιμή. Γιατί να περάσουμε σε μια συνάρτηση μια παράμετρο με άγνωστη τιμή;

Η απάντηση για ακόμα μια φορά είναι το optimization (περιττό να σας πω ότι το πρόγραμμα έγινε compile με -Ο2). Ο compiler θέλει να κρατάει τον esp σε διευθύνσεις πολλαπλάσιες των 16 bytes! Το κόστος προσπέλασης σε μη aligned διευθύνσεις είναι τόσο σημαντικό, ώστε ο gcc εισάγει dummy εντολές για να το αποφύγει!

mov    edi,0x80485ce
mov    ecx,0xb
cld
...
repz cmps ds:[esi],es:[edi]
jne    0x8048564 <printf+480> 
Εδώ στον edi φορτώνεται μια διεύθυνση μνήμης και στον edi η τιμή 11. Μετά "καθαρίζεται" το direction flag. Ακολουθεί η εντολή "repz cmps ds:[esi],es:[edi]", η οποία με λίγα λόγια λέει: όσο τα bytes που βρίσκονται στις διευθύνσεις που δείχνουν esi και edi είναι ίσα και ο ecx δεν είναι 0 αύξησε (αν το direction flag είναι 0/clear) τους esi και edi κατά ένα byte και έλεγξε ξανά (η μαγεία των CISC επεξεργαστων...). Ουσιαστικά κάνει ακριβώς την ίδια δουλειά με μια κλήση strncmp(esi,edi,ecx), ελέγχει αν τα πρώτα #ecx bytes δύο string που αρχίζουν στις διευθύνσεις esi και edi είναι ίσα.

Ωραία, ξέρουμε την τιμή του edi αλλά ο esi τη τιμή έχει; Λίγο πιο πάνω υπάρχει η "lea esi,[ebp-40]" και επειδή ξέρουμε ότι ο gcc φροντίζει να μην αλλάζει ο esi από συναρτήσεις, είμαστε σίγουροι ότι έχει ακόμα την ίδια τιμή.

Για να συνοψίσουμε, το πρόγραμμα καλεί μια συνάρτηση της οποίας μια παράμετρος είναι η διεύθυνση μιας τοπικής μεταβλητής (ebp-40) και μετά συγκρίνει τα bytes που βρίσκονται εκεί με κάποια άλλα που βρίσκονται σε μια σταθερή θέση. Δε ξέρω τι λέτε εσείς αλλά εμένα μου φαίνεται πως εδώ γίνεται ο έλεγχος του password!

Η επόμενη εντολή είναι η jne (jump if not equal/zero). Αν η σύγκριση είναι επιτυχής τότε το zero flag έχει ενεργοποιηθεί από το προηγούμενο βήμα και έτσι δεν ακολουθούμε το άλμα. Αν το password είναι λάθος το ZF=0 και το άλμα γίνεται.

Για να δούμε ποίο είναι το password:

(gdb) break *0x8048546
Breakpoint 2 at 0x8048546
(gdb) c
Continuing.
Say the password: sesame

Breakpoint 2, 0x08048546 in printf ()
(gdb) x/5i $eip
0x8048546 <printf+450>: repz cmps ds:[esi],es:[edi]
0x8048548 <printf+452>: jne    0x8048564 <printf+480>
0x804854a <printf+454>: sub    esp,0xc
0x804854d <printf+457>: push   0x80485d9
0x8048552 <printf+462>: call   0x8048354 <puts>
(gdb) x/s $esi
0xbffff720:      "sesame"
(gdb) x/s $edi
0x80485ce <_IO_stdin_used+26>:   .......... xe xe!

Η παραπάνω τεχνική, όπου βρίσκουμε ένα σωστό password/serial εντοπίζοντας το σημείο που γίνεται η σύγκριση με αυτό που έχουμε εισάγει εμείς, λέγεται password/serial fishing

Εντάξει, μάθαμε το password, γιατί όμως να σταματήσουμε εδώ; Γιατί να μην πειράξουμε το πρόγραμμα ώστε να δέχεται ως σωστό κάθε password; Για να το πετύχουμε, αρκεί να μην ακολουθούμε ποτέ το άλμα. Ξέρουμε ότι η jne (σε αυτή τη μορφή) καταλαμβάνει στη μνήμη 2 bytes, διότι η επόμενη εντολή αρχίζει δύο bytes πιο μετά.

(gdb) x/2b 0x8048548
0x8048548 <printf+452>: 0x75    0x1a

Το 0x75 είναι το opcode της εντολής ενώ το 0x1a η (προσημασμένη) απόσταση του άλματος. Αν γίνει το άλμα ο έλεγχος θα μεταφερθεί 0x1a bytes από το τέλος της εντολής(αρχή της επόμενης), εδώ 0x804854a + 0x1a = 0x8048564.

Για να πετύχουμε το σκοπό μας, αρκεί να αντικαταστήσουμε τα δύο αυτά bytes με nop (no operation, opcode 0x90). Φυσικά δεν έχει νόημα να το κάνουμε αυτό μόνο στη μνήμη αλλά στο image του εκτελέσιμου που βρίσκεται στο αρχείο. Το πρόβλημα είναι να εντοπίσουμε σε ποιο σημείο του αρχείου βρίσκεται ο κώδικας που θέλουμε να πειράξουμε. Ο ELF header έχει όλες τις πληροφορίες που χρειαζόμαστε αλλά αφού δεν έχουμε αναφερθεί ακόμα σε αυτόν θα κάνουμε κάτι άλλο: θα ψάξουμε το αρχείο για να βρούμε την αρχική ακολουθία από bytes. Υπάρχει περίπτωση η ίδια ακολουθία να υπάρχει σε πολλά σημεία και πρέπει να είμαστε σίγουροι πως πειράζουμε το σωστό. Για αυτό χρειαζόμαστε αρκετά bytes γύρω από την εντολή (το context).

(gdb) x/10b 0x8048548-5
0x8048543 <printf+447>: 0x83    0xc4    0x10    0xf3    0xa6    0x75   
0x1a    0x83
0x804854b <printf+455>: 0xec    0x0c

Αν ψάξετε για αυτά τα bytes στο αρχείο(με έναν hexeditor πχ του mc) θα τα βρείτε μόνο μια φορά και το offset του 0x75 0x1a είναι 0x548. Αντικαταστήστε τα δύο αυτά bytes με 0x90 0x90.

bash$ ./rce2
Say the password: qrwrwr
Hooray!
bash$ ./rce2
Say the password: sdfeg4453
Hooray!

Συγχαρητήρια, μόλις "σπάσατε" το πρόγραμμα!

Επόμενο  Προηγούμενο  Περιεχόμενα


Valid HTML 4.01!   Valid CSS!