Προηγούμενο Περιεχόμενα
Η προηγούμενη πρόκληση είχε ως στόχο την ανακάλυψη ενός
τρόπου για να ανοίξει η θήκη μέσα στην οποία βρίσκεται
ένας πρότυπος 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 η υποστήριξη για το συγκεκριμένο χαρακτηριστικό δεν είναι πλήρης :)
Αυτή τη φορά έλαβα μόνο μια απάντηση, η οποία όμως ήταν
πραγματικά αξιόλογη!
Συγχαρητήρια, λοιπόν, στον Γιώργο Πρέκα για τη
λύση του !
Από το 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. Καλή συνέχεια!
Προηγούμενο Περιεχόμενα