Reverse Engineering σε περιβάλλον Linux, Μέρος 3: Πρόκληση
Description: This is issue 35 of Magaz, the Hellenic LinuxMagazine
Keywords: magaz hellenic greek hellas greece hellug linux magazine

Magaz, The Greek Linux Magazine
Τεύχος #35
Περιληπτικά Περιεχόμενα Αναλυτικά Περιεχόμενα Mirrors Links Πληροφορίες Συντάκτες Αναζήτηση Επικοινωνία
Προηγούμενο Περιεχόμενα ### 5. Πρόκληση ### 5.1 Προηγούμενη Πρόκληση Η προηγούμενη πρόκληση είχε ως στόχο την ανακάλυψη ενός τρόπου για να ανοίξει η θήκη μέσα στην οποία βρίσκεται ένας πρότυπος RISC επεξεργαστής, μέσω της μελέτης του emulator του. Καταρχάς, ο emulator περιείχε συνολικά τρία αρχεία: το εκτελέσιμο risc-emu, το change.txt και το log.txt. Παρ' όλο που τα δύο τελευταία αρχεία έχουν .txt κατάληξη, κάθε άλλο παρά text είναι! Θα μπορούσαμε να τα αγνοήσουμε τελείως αλλά για να υπάρχουν εκεί κάποιο ρόλο παίζουν... Επιπλέον, δεν αισθάνεστε την ανάγκη να ικανοποιήσετε την περιέργεια σας; Τι άραγε κρύβουν τα αρχεία αυτά; Μια προσεκτική παρατήρηση των στοιχείων μπορεί να φέρει στην επιφάνεια τέσσερα σημεία που οδηγούν στη λύση: Τα αρχεία έχουν κατάληξη .txt. Αν και τα ίδια δεν αποτελούνται από κείμενο, πιθανότατα να έχουν κάποια σχέση με κείμενο. Βέβαια, ίσως οι καταλήξεις να είναι εντελώς παραπλανητικές {δαιμονικό γέλιο ακούγεται στο υπόβαθρο} :) Τα αρχεία έχουν ακριβώς το ίδιο μέγεθος. Αυτό είναι ισχυρό στοιχείο πως σχετίζονται μεταξύ τους έμμεσα ή άμεσα. Αν συνδυάσουμε τα ονόματα των δύο αρχείων λαμβάνουμε τη λέξη changelog η οποία είναι ένα αρκετά κοινό αρχείο που ακολουθεί προγράμματα. Το γεγονός αυτό ενισχύει την άποψη πως τα δύο αρχεία συνδέονται μεταξύ τους. Το θέμα είναι πως υπάρχουν άπειροι τρόποι να συνδυαστούν δύο αρχεία! Αν προσέξετε το κείμενο της πρόκλησης, ο 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 (μια σελίδα). Η τιμή αυτή μας λέει ταυτόχρονα τρία πράγματα: Τo segment μπορεί να φορτωθεί μόνο σε κομμάτια πολλαπλάσια των 0x1000 bytes. Τo segment μπορεί να φορτωθεί μόνο από file offsets πολλαπλάσια των 0x1000 bytes. Το 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). Από εδώ και πέρα μένει η ανάλυση του κώδικα που είναι το πιο χρονοβόρο και κουραστικό μέρος. Δε θα αναφερθώ περισσότερο σε αυτή διότι στηρίζεται σε μεγάλο βαθμό στην εμπειρία του αναλυτή. Μερικές συμβουλές μπορείτε να βρείτε εδώ. 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 Αυτή τη φορά έλαβα μόνο μια απάντηση, η οποία όμως ήταν πραγματικά αξιόλογη! Συγχαρητήρια, λοιπόν, στον Γιώργο Πρέκα για τη λύση του !
    Από το 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 κώδικα της πρόκλησης και τη δική μου λύση μπορείτε να την κατεβάσετε από εδώ. Ευτυχώς ή δυστυχώς, αυτό το άρθρο είναι μάλλον το τελευταίο της σειράς οπότε δεν υπάρχει επόμενη πρόκληση. Πάντως, όσοι πραγματικά ενδιαφέρονται για γρίφους τέτοιου είδος δεν έχουν παρά να ψάξουν στο internet όπου θα βρουν αρκετές σχετικές σελίδες. Το μόνο αρνητικό στην όλη υπόθεση είναι πως οι περισσότερες από αυτές ασχολούνται με εκτελέσιμα σε περιβάλλον Win32. Καλή συνέχεια! Προηγούμενο Περιεχόμενα