Επόμενο Προηγούμενο Περιεχόμενα
Αυτή τη φορά θα ασχοληθούμε με το demo του εκπληκτικού
προγράμματος για πράξεις μη αρνητικών ακεραίων:
rce2-files/hands-on.gz.
Το demo θα σταματήσει να λειτουργεί μετά από κάποιο
χρονικό διάστημα. Αυτό το είδος της προστασίας
ονομάζεται Cinderella (Σταχτοπούτα) protection, διότι
όπως και στο γνωστό παραμύθι, όταν παρέλθει κάποιο
χρονικό διάστημα η άμαξα/πρόγραμμα θα γίνει κολοκύθα
:) Για να δούμε...
bash$ ./hands-on
Ready> 1+3
Result: 4
Ready> 4*5
Result: 20
Ready> 25/5
Result: 5
Ready>
Αν πάμε την ημερομηνία του συστήματος μερικές μέρες
μπροστά...
./hands-on
Not ready> 3+4
Result: 11
Not ready> 6*6
Result: 108
Not ready> 2/3
Result: -1
Not ready>
Χμμ, το πρόγραμμα όντως σταμάτησε να λειτουργεί (σωστά
τουλάχιστον). Αν επιστρέψουμε στην αρχική ημερομηνία και
εκτελέσουμε το πρόγραμμα θα δούμε ότι λειτουργεί
κανονικά. Όποιος έφτιαξε την προστασία μάλλον δεν το
έκανε πολύ καλά :) Πάντως είναι εκνευριστικό να πρέπει να
γυρνάμε το ρολόι πίσω όποτε θέλουμε να εκτελέσουμε αυτή
την εκπληκτική εφαρμογή. Θα πρέπει να κάνουμε
κάτι καλύτερο!
Ας δούμε τι μπορούμε να μάθουμε για το πρόγραμμα...
listing1
Ουακ! Τι είναι όλα αυτά;
Τα ακατανόητα ονόματα είναι σύμβολα της C++ σε μπλεγμένη
(mangled) μορφή. Υποτίθεται πως το ltrace έχει μια
επιλογή (-C) για να κάνει demangling αλλά σε εμένα δε
βελτίωσε την κατάσταση. Ευτυχώς υπάρχει ένα πρόγραμμα, το
c++filt, το οποίο αποκωδικοποιεί τα ονόματα των
c++ συμβόλων.
listing2
Αν δεν είστε μυημένοι στους μυστικούς συμβολισμούς της
STL (Standard Template Library) της C++, τα παραπάνω ίσως
να σας φαίνονται τόσο ακατανόητα όσο και τα mangled
σύμβολα. Η αλήθεια είναι, όμως, ότι περιέχουν σημαντικές
πληροφορίες. Για παράδειγμα, οι περισσότερες γραμμές που
αρχίζουν με std::basic_ostream είναι κλήσεις στη
συνάρτηση operator<<() η οποία είναι υπεύθυνη για
την υπερφόρτωση του τελεστή <<. Αν έχετε ασχοληθεί
στοιχειωδώς με C++ σίγουρα θα έχετε δει το
cout<<"Hello World". Στην πραγματικότητα αυτό είναι
μια κλήση operator<<(cout, "Hello World") η οποία
γράφει δεδομένα στο stream της κονσόλας. Oι κλήσεις στο
παραπάνω listing δεν είναι τίποτα άλλο από τέτοιου είδους
κλήσεις. Παρομοίως, οι γραμμές που αρχίζουν με
std::basic_istream είναι κλήσεις στη συνάρτηση
operator>>() η οποία διαβάζει δεδομένα από κάποιο
stream (πχ πληκτρολόγιο).
Ξεφεύγοντας λίγο από τη C++, άξιες προσοχής είναι οι
κλήσεις στην time(NULL) που επιστρέφει την
τρέχουσα ώρα και επίσης στη stat (__xstat) που επιστρέφει
πληροφορίες για κάποιο αρχείο (το "hands-on" στην
περίπτωση αυτή). Παρατηρήστε, επίσης, ότι στο πρόγραμμα
οι κλήσεις αρχίζουν να επαναλαμβάνονται: η [08048be0]
time(NULL) καλείται στην αρχή και ξανακαλείται από το
ίδιο σημείο αργότερα. Επίσης, αμέσως μετά και τις δύο
αυτές time() (0x08048be0), υπάρχει η ίδια ακολουθία
ostream..., istream..., string... . Υπαρχεί μεγάλη
πιθανότητα αυτό να είναι το σημείο στο οποίο εμφανίζεται
το prompt (ostream), εισάγουμε την αριθμητική παράσταση
(istream) και αυτή αποθηκεύεται (string). Αλλά αρκετά με
τις υποθέσεις. Ας δούμε τι άλλο μπρούμε να μάθουμε για το
εκτελέσιμο:
bash$ objdump -x ./hands-on
/hands-on: file format elf32-i386
./hands-on
architecture: i386, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x08048080
Program Header:
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x000005b8 memsz 0x000005b8 flags r-x
LOAD off 0x000005b8 vaddr 0x080495b8 paddr 0x080495b8 align 2**12
filesz 0x0000002c memsz 0x0000002c flags rw-
Sections:
Idx Name Size VMA LMA File off Algn
SYMBOL TABLE:
no symbols
...όχι και πολλά πράγματα. Μάλιστα, κάτι ύποπτο
συμβαίνει! Ενώ γνωρίζουμε ότι το εκτελέσιμο καλεί
δυναμικά συναρτήσεις βιβλιοθηκών (από το ltrace) δεν
υπάρχει DYNAMIC segment. Εκτός των άλλων, δεν υπάρχουν
καθόλου sections, γεγονός παράξενο (βέβαια δεν είναι
απαραίτητο να υπάρχουν στο εκτελέσιμο αλλά είναι
συνηθισμένο).
Συνεχίζοντας να μαζεύουμε πληροφορίες:
bash$ file hands-on
hands-on: ELF 32-bit LSB executable, Intel 80386, version 1, statically linkedfile: corrupted section header size.
bash$ strings hands-on
Linux
SlQf
UPXδ
δψRQθώ
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 1.24 Copyright (C) 1996-2002 the UPX Team. All Rights Reserved. $
UWVSQRό
...
...
Τώρα όλα βγάζουν νόημα... Το εκτελέσιμο έχει συμπιεστεί
με το πρόγραμμα UPX! Στην περίπτωση που θέλουμε απλώς να
εργαστούμε με κάποιον debugger αυτό δε μας ενοχλεί
ιδιαίτερα, αρκεί κάθε φορά να προσπερνάμε τον κώδικα της
αποσυμπίεσης και να ασχολούμαστε με το πραγματικό
εκτελέσιμο. Το πρόβλημα είναι στη περίπτωση του
dead-listing. Θα μπορούσαμε να το αγνοήσουμε, όμως, όπως
έχουμε αναφέρει, οι ευκολίες που προσφέρει είναι
ανεκτίμητες. Οπότε, στο επόμενο κομμάτι θα κάνουμε ότι
μπορούμε για να φέρουμε το εκτελέσιμο όσο πιο κοντά
γίνεται στην αυθεντική του μορφή.
(ΣΗΜΕΙΩΣΗ: Αν δεν έχετε διαβάσει τις πληροφορίες για το
packing, τώρα είναι μια
καλή στιγμή να το κάνετε)
Πως όμως θα δούμε το αρχικό εκτελέσιμο; Αυτό μπορεί να
επιτευχθεί ως εξής:
1. Ο εύκολος τρόπος
Μπορούμε να πάμε στη σελίδα του UPX http://upx.sourceforge.net,
να το κατεβάσουμε και να αποσυμπιέσουμε το εκτελέσιμο. Αν
και είναι απλή μέθοδος, δεν έχει καμία γενικότητα. Για
παράδειγμα, δε μπορεί να εφαρμοστεί στην περίπτωση που η
συμπίεση/κρυπτογράφηση έχει γίνει με κάποιο custom
αλγόριθμο ή ακόμα και με άλλη έκδοση του ίδιου
προγράμματος.
2. Ο καλύτερος τρόπος
Χρησιμοποιώντας το strace (κάτι που αμελήσαμε να κάνουμε
πριν):
bash$ strace -ohands-on.st hands-on
Ready>
^C
bash$ cat -n hands-on.st
1 execve("./hands-on", ["hands-on"], [/* 48 vars */]) = 0
2 getpid() = 690
3 open("/proc/690/exe", O_RDONLY) = 3
4 lseek(3, 1508, SEEK_SET) = 1508
5 read(3, "0e\300\177\314\30\0\0\314\30\0\0", 12) = 12
6 gettimeofday({1062315999, 908816}, NULL) = 0
7 unlink("/tmp/upxCOXBQXPAAVS") = -1 ENOENT (No such file or directory)
8 open("/tmp/upxCOXBQXPAAVS", O_WRONLY|O_CREAT|O_EXCL, 0700) = 4
9 ftruncate(4, 6348) = 0
10 old_mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40000000
11 read(3, "\314\30\0\0b\f\0\0", 8) = 8
12 read(3, "\177?d\371\177ELF\1\0\2\0\3\0\r\30\211\4\377o\263\335\010"..., 3170) = 3170
13 write(4, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\2\0\3\0\1\0\0\0\30\211"..., 6348) = 6348
14 read(3, "\0\0\0\0UPX!", 8) = 8
15 munmap(0x40000000, 12288) = 0
16 close(4) = 0
17 close(3) = 0
18 open("/tmp/upxCOXBQXPAAVS", O_RDONLY) = 3
19 access("/proc/690/fd/3", R_OK|X_OK) = 0
20 unlink("/tmp/upxCOXBQXPAAVS") = 0
21 fcntl(3, F_SETFD, FD_CLOEXEC) = 0
22 execve("/proc/690/fd/3", ["hands-on"], [/* 48 vars */]) = 0
...
Λοιπόν, για να δούμε τι συμβαίνει.
1: Εκτελείται το hands-on.
2-5: Το πρόγραμμα μαθαίνει το pid του και ανοίγει
το ίδιο το αρχείο του μέσω του /proc filesystem (θα
μπορούσε να έχει χρησιμοποιήσει το /proc/self άλλα ίσως
δεν το έκανε για λόγους συμβατότητας). Ύστερα διαβάζει 12
bytes από το offset 1508 στο αρχείο.
6-9: Διαβάζεται η ώρα του συστήματος, γίνεται μια
προσπάθεια να διαγραφεί το αρχείο "/tmp/upxCOXBQXPAAVS"
το οποίο δεν υπάρχει και δημιουργείται εκ νέου με
δικαιώματα 0700 = -rwx------. Τέλος, το μέγεθος του
αρχείου καθορίζεται στα 6348 bytes. Η δημιουργία του
αρχείου με δικαίωμα εκτέλεσης ελπίζω να σας προβλημάτισε.
Παρεπιπτόντως, παρατηρήστε οτι οι δύο τελευταίες τετράδες
bytes από τα 12 bytes που διαβάστηκαν στη γραμμή 5
αντιστοιχούν στο 6348 αν τις θεωρήσουμε ακέραιους των 4
bytes (διαβασμένα LSB first).
10-15: Αρχικά γίνονται map 12288 bytes. Μετά
διαβάζονται 8 bytes από αρχικό αρχείο (hands-on)
(συνεχίζοντας από εκεί που είχε μείνει μετά τα 12 bytes).
Αν ερμηνευτούν ως ακέραιοι των 4-bytes είναι οι τιμές
6348 και 3170. Ύστερα διαβάζονται από το αρχικό αρχείο
3170 bytes (τι σύμπτωση!) και γράφονται στο καινούργιο
("/tmp/upxCOXBQXPAAVS") 6348 bytes. Παρατηρήστε πως η
αρχή των δεδομένων που γράφονται είναι ένας ELF header.
Τέλος, διαβάζονται άλλα 8 bytes (μοιάζουν με end of data
signature) και γίνονται unmap αυτά που είχαν γίνει map
πιο πριν.
16-22: Κλείνουν τα δύο αρχεία και ανοίγει πάλι το
"/tmp/upxCOXBQXPAAVS" με δικαιώματα μόνο ανάγνωσης αυτή
τη φορά. Μετά ελέγχεται αν η τρέχουσα διεργασία έχει
δικαιώματα ανάγνωσης (R_OK) και εκτέλεσης (X_OK) για το
αρχείο και αμέσως μετά αυτό διαγράφεται. Η διαγραφή αυτή
όμως είναι τυπική διότι η τρέχουσα διεργασία έχει ήδη ένα
file handle (3), οπότε αν και το αρχείο δεν υπάρχει πια
ως μέρος του filesystem τα δεδομένα του δεν έχουν χαθεί.
Με τη fcntl() που ακολουθεί, καθορίζεται ότι σε περίπτωση
που κληθεί η exec() για την αντικατάσταση της διεργασίας
με κάποια καινούργια, η καινούργια δε θα λάβει
τον file descriptor 3. Αυτό ονομάζεται close-on-exec.
Τέλος, εκτελείται το αρχείο.
Ελπίζω ύστερα από τα παραπάνω, η γενική λειτουργία του
UPX να έχει γίνει φανερή. Με απλά λόγια, όταν ένα
πρόγραμμα συμπιεσμένο με UPX εκτελείται, το αυθεντικό
αρχείο αποσυμπιέζεται σε ένα προσωρινό αρχείο στον
κατάλογο "/tmp" και ο έλεγχος περνάει σε αυτό. Το
αποσυμπιεσμένο αρχείο "διαγράφεται" αλλά τα δεδομένα του
υφίστανται μέχρι να τελειώσει η εκτέλεση.
Τώρα τίθεται το ερώτημα: πως θα έχουμε πρόσβαση στο
αποσυμπιεσμένο εκτελέσιμο; Και από το πουθενά ακούγεται
ένας υπερκοσμικός ψίθυρος: /proc/<pid>/exe!
Πράγματι :
bash$ ./hands-on &
[1] 804
Ready> bash$ cat /proc/804/exe > ./hands-on-unpacked
[1]+ Stopped ./hands-on
bash$ %1
./hands-on
^C
bash$ objdump -x ./hands-on-unpacked
./hands-on-unpacked: file format elf32-i386
./hands-on-unpacked
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048918
Program Header:
PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags r-x
INTERP off 0x00000114 vaddr 0x08048114 paddr 0x08048114 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00001050 memsz 0x00001050 flags r-x
LOAD off 0x00001050 vaddr 0x0804a050 paddr 0x0804a050 align 2**12
filesz 0x000002f4 memsz 0x00000430 flags rw-
DYNAMIC off 0x000011f8 vaddr 0x0804a1f8 paddr 0x0804a1f8 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags rw-
NOTE off 0x00000128 vaddr 0x08048128 paddr 0x08048128 align 2**2
filesz 0x00000020 memsz 0x00000020 flags r--
EH_FRAME off 0x00001014 vaddr 0x08049014 paddr 0x08049014 align 2**2
filesz 0x0000003c memsz 0x0000003c flags r--
Dynamic Section:
NEEDED libstdc++.so.5
NEEDED libm.so.6
NEEDED libgcc_s.so.1
...
bash$ ./hands-on-unpacked
Ready>
^C
Αυτό ήταν, τώρα πια έχουμε το εκτελέσιμο στην αυθεντική
του μορφή!
Αφού αποσυμπιέσαμε ο εκτελέσιμο, ήρθε η ώρα να δούμε πως
μπορούμε να απενεργοποιήσουμε την προστασία. Αυτή τη φορά
αντί για τον GDB θα χρησιμοποιήσουμε την dead-listing
προσέγγιση με τον HTEditor. Φορτώνοντας το πρόγραμμα στο
ht εμφανίζεται μπροστά μας ένα γραφικό περιβάλλον σε
ncurses. Με το F6/Space εμφανίζεται το παράθυρο επιλογής
mode, και εμείς επιλέγουμε το mode elf/image. Τώρα πια
βλέπουμε το listing αρχίζοντας από το entry point. Με τα
βελάκια μπορούμε να κινηθούμε στις διάφορες
διευθύνσεις/σύμβολα και με το πλήκτρο enter το listing
μετακινείται στο σημείο όπου αναφέρεται η τρέχουσα
επιλογή. Αρχικά έχουμε:
8048918 !
....... ! ;******************************************************************
....... ! ; end of section <.plt>
....... ! ;******************************************************************
....... !
....... ! ;******************************************************************
....... ! ; section 13 <.text>
....... ! ; virtual address 08048918 virtual size 00000678
....... ! ; file offset 00000918 file size 00000678
....... ! ;******************************************************************
....... !
....... ! ;****************************
....... ! ; executable entry point
....... ! ;****************************
....... ! entrypoint:
....... ! xor ebp, ebp
804891a ! pop esi
804891b ! mov ecx, esp
804891d ! and esp, 0fffffff0h
8048920 ! push eax
8048921 ! push esp
8048922 ! push edx
8048923 ! push offset_8048f90
8048928 ! push offset_80487e0
804892d ! push ecx
804892e ! push esi
804892f ! push offset_80489c8
8048934 ! call __libc_start_main
8048939 ! hlt
804893a ! nop
804893b ! nop
804893c !
Έτσι, αν επιλέξουμε το offset_80489c8 και πατήσουμε
enter, το listing θα αρχίζει από τη διεύθυνση 0x80489c8 η
οποία είναι και η διεύθυνση της main.
Κάνοντας μερικές "βόλτες" στη main παρατηρούμε ότι τα
πράγματα δεν είναι και πολύ κατατοπιστικά. Αυτό οφείλεται
εν μέρει στα optimizations του g++ αλλά και την ίδια τη
C++ ως γλώσσα. Θα μπορούσαμε να κυκλοφορούμε σαν την
άδικη κατάρα στο listing ψάχνοντας για οτιδήποτε
ενδιαφέρον αλλά σίγουρα μπορούμε να σκεφτούμε κάτι
καλύτερο. Ας καταστρώσουμε, λοιπόν, κάποιο σχέδιο. Σκοπός
μας είναι, καταρχάς, να εντοπίσουμε σε ποιο σημείο (ή
σημεία) γίνεται ο έλεγχος για το αν έχουμε ξεπεράσει το
επιτρεπτό χρονικό όριο. Αυτό συχνά είναι και το πιο
δύσκολο κομμάτι σε μια προσπάθεια RCE, το να εντοπίσουμε
και να ξεχωρίσουμε μέσα στις χιλιάδες εντολές αυτές που
μας ενδιαφέρουν.
Ένας καλός τρόπος να το πετύχουμε αυτό είναι να δούμε πως
η εσωτερική αλλαγή/έλεγχος επηρεάζει εξωτερικά (προς το
χρήστη) το πρόγραμμα και να ψάξουμε για το σημείο στον
κώδικα που γίνεται αυτή η εξωτερική αλλαγή. Επειδή η
προηγούμενη περίοδος είναι κάπως ασαφής ας κάνουμε τα
πράγματα πιο συγκεκριμένα. Αυτό που μας ενδιαφέρει είναι
να εντοπίσουμε που γίνεται ο χρονικός έλεγχος. Επειδή
αυτό είναι κάπως δύσκολο, ας αναλογιστούμε πως το
αποτέλεσμα του χρονικού ελέγχου (αλλαγή της εσωτερικής
κατάστασης) επηρεάζει τη συμπεριφορά του προγράμματος.
Είδαμε στην αρχή πως, όταν δεν έχει λήξει η demo
περίοδος, το πρόγραμμα εμφανίζει το prompt "Ready>"
ενώ όταν έχει λήξει το "Not Ready>". Ας ψάξουμε λοιπόν
μήπως βρούμε κάποιο από αυτά τα strings. Με το F7
εμφανίζεται το παράθυρο αναζήτησης όπου αν βάλουμε
"Ready>" θα βρεθούμε στο εξής
8048fb1 db 00h ; ' '
8048fb2 db 02h ; ' '
8048fb3 db 00h ; ' '
8048fb4
....... strz_Not_ready___8048fb4: ;xref o8048bf3
....... db "Not ready> \0"
8048fc0
....... strz_Result:__8048fc0: ;xref o8048c25
....... db "Result: \0"
8048fc9
....... strz_Ready___8048fc9: ;xref o8048c8f
....... db "Ready> \0"
8048fd1 db 33h ; '3'
8048fd2 db 42h ; 'B'
8048fd3 db 75h ; 'u'
8048fd4 db 6ch ; 'l'
8048fd5 db 00h ; ' '
8048fd6 db 33h ; '3'
8048fd7 db 44h ; 'D'
8048fd8 db 69h ; 'i'
8048fd9 db 76h ; 'v'
8048fda db 00h ; ' '
Ο disassembler έχει βρει τα strings και μάλιστα τους έχει
δώσει και όνομα (πχ strz_Ready___8048fc9). To xref
o8048c8f σημαίνει ότι αυτό το σύμβολο/διεύθυνση έχει
"αναφερθεί"/χρησιμοποιηθεί στη διεύθυνση 0x8048c8f ως
offset σε κάποια εντολή. Ας πάμε εκεί λοιπόν :
....... ! loc_8048c84: ;xref j8048c20
....... ! lea esp, [ebp-0ch]
8048c87 ! pop ebx
8048c88 ! pop esi
8048c89 ! pop edi
8048c8a ! leave
8048c8b ! ret
8048c8c !
....... ! loc_8048c8c: ;xref j8048bea
....... ! sub esp, 8
8048c8f ! push strz_Ready___8048fc9
8048c94 ! jmp loc_8048bf8
8048c99 nop
8048c9a nop
8048c9b nop
Παρατηρούμε ότι το offset του string
χρησιμοποιήθηκε στην push. Σημειώστε τη διεύθυνση του
τελικού jmp και ότι ο μόνος τρόπος για να φτάσουμε σε αυτό
το κομμάτι κώδικα είναι μέσω ενός jump που βρίσκεται στη
διεύθυνση 0x8048bea (το xref μας έδωσε αυτές τις
πληροφορίες). Δε μένει τίποτα παρά να πάμε εκεί να δούμε:
8048bd4 !
....... ! loc_8048bd4: ;xref j8048c73
....... ! sub esp, 0ch
8048bd7 ! mov ebx, [edi]
8048bd9 ! push 0
8048bdb ! call time
8048be0 ! mov edx, [ebx+8]
8048be3 ! add edx, [ebx]
8048be5 ! add esp, 10h
8048be8 ! cmp eax, edx
8048bea ! jng loc_8048c8c <------ Αυτό το jump μας ενδιαφέρει
8048bf0 ! sub esp, 8
8048bf3 ! push strz_Not_ready___8048fb4
8048bf8 !
....... ! loc_8048bf8: ;xref j8048c94
....... ! push _ZSt4cout
8048bfd ! call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
8048c02 ! pop ebx
8048c03 ! pop esi
8048c04 ! push dword ptr [ebp-10h]
8048c07 ! push _ZSt3cin
8048c0c ! call _ZStrsIcSt11char_traitsIcESaIcEERSt13basic_istreamIT_T0_ES7_RSbIS4_S5_T1_E
8048c11 ! mov [esp], edi
8048c14 ! call sub_8048c9c
8048c19 ! add esp, 10h
8048c1c ! test eax, eax
8048c1e ! mov ebx, eax
8048c20 ! jz loc_8048c84
8048c22 ! sub esp, 8
8048c25 ! push strz_Result:__8048fc0
8048c2a ! push _ZSt4cout
8048c2f ! call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
8048c34 ! mov esi, eax
Κάτι μου λέει πως φτάνουμε στη λύση του μυστηρίου! Το
jump ψάχνουμε είναι ακριβώς μετά από μια σύγκριση του eax
με τον edx. Από το listing βλέπουμε πως ο eax περιέχει
την τιμή επιστροφής της κλήσης της συνάρτησης time(0)
(@0x8048bdb). Αυτή επιστρέφει την τρέχουσα ώρα σε
δευτερόλεπτα, μετρημένη από τις 1/1/1970 (το λεγόμενο
Epoch). Η τιμή στον edx προκύπτει από την πρόσθεση δύο
τιμών που βρίσκονται στις διευθύνσεις ebx και ebx+8. Αν η
τωρινή ώρα είναι μικρότερη από το άθροισμα, τότε το
πρόγραμμα κάνει άλμα, κάνει push "Ready>" και γυρίζει
στη διεύθυνση 0x8048bf8. Αντίθετα, αν η τρέχουσα ώρα
είναι μεγαλύτερη ή ίση από το άθροισμα, το άλμα δεν
εκτελείται, γίνεται push "Not Ready>" και φτάνουμε
πάλι στη διεύθυνση 0x8048bf8. Μου φαίνεται ότι είναι πια
ξεκάθαρο πως το άθροισμα που παράγεται ( [ebx] + [ebx+8]
) αποτελεί τη χρονική στιγμή πέρα από την οποία λήγει το
demo.
Στη διεύθυνση 0x8048bf8 γίνεται push το σύμβολο cout και
καλείται η συνάρτηση
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc.
Χμμ, όχι και πολύ ξεκάθαρα πράγματα. Ας το κάνουμε
demangle manually με το c++filt
listing3.txt
Όλη η προηγούμενη ακολουθία μας λέει πως καλείται η
συνάρτηση για τον overloaded τελεστή <<, με
αριστερή πλευρά ένα αντικείμενο τύπου
basic_ostream (output stream) και δεξιά
const char *. Με απλά λόγια, η εντολή που
εκτελέστηκε ήταν η std::cout<<"Ready>" ή
std::cout<<"Not Ready>".
Λίγο πιο κάτω (έχω αποπλέξει τα σύμβολα με το
c++filt):
listing4.txt
Το οποίο μεταφράζεται σε std::cin>>string1, όπου
string1 ένα αντικείμενο τύπου std::string. Σε
αυτό το string μπορούμε να υποθέσουμε πως αποθηκεύεται η
έκφραση που εισάγουμε.
Ας αφήσουμε την ανάλυση του listing για λίγο και ας
αναλογιστούμε πως μπορούμε να πειράξουμε το εκτελέσιμο
ώστε να ξεπεράσουμε το χρονικό έλεγχο. Η πιο απλή λύση
είναι να αντικαταστήσουμε το jng στη διεύθυνση 0x8048bea
με ένα jmp στη διεύθυνση 0x8048c8. Το πρόβλημα με αυτή
την προσέγγιση είναι ότι αντιμετωπίζουμε το αποτέλεσμα
και όχι την αιτία. Θα πρέπει σε κάθε σημείο που γίνεται
ένα τέτοιος έλεγχος να αλλάξουμε το άλμα. Εμείς βρήκαμε
ένα τέτοιο σημείο αλλά πιθανότατα υπάρχει τουλάχιστον
ακόμα ένα. Θυμηθείτε πως όταν το πρόγραμμα είχε λήξει,
εκτός από το prompt "Not Ready>", κάτι δεν πήγαινε
καλά και με τις πράξεις. Στο σημείο που αναλύσαμε εμείς,
ο έλεγχος φαίνεται να επηρεάζει μόνο το prompt, οπότε θα
πρέπει να υπάρχει και κάποιο άλλο checkpoint. Σε ένα
πλήρες πρόγραμμα τα σημεία ελέγχου μπορεί να είναι
εκατοντάδες και σίγουρα δεν είναι πρακτικό να τα
αλλάξουμε όλα. Η πιο σωστή λύση είναι να βρούμε σε ποιο
σημείο αρχικοποιούνται οι μεταβλητές που περιέχουν τις
πληροφορίες για τη λήξη του χρόνου και να τις
"πειράξουμε" εκεί. Ας αρχίσουμε λοιπόν...
Επιστρέφοντας στο listing λίγο πιο πάνω από εκεί που
είχαμε μείνει:
8048bc0 !
....... ! ;-----------------------
....... ! ; S U B R O U T I N E
....... ! ;-----------------------
....... ! sub_8048bc0: ;xref c8048a7d
....... ! push ebp
8048bc1 ! mov ebp, esp
8048bc3 ! push edi
8048bc4 ! push esi
8048bc5 ! push ebx
8048bc6 ! sub esp, 0ch
8048bc9 ! mov edi, [ebp+8]
8048bcc ! lea eax, [edi+4]
8048bcf ! mov [ebp-10h], eax
8048bd2 ! mov esi, esi
8048bd4 !
....... ! loc_8048bd4: ;xref j8048c73
....... ! sub esp, 0ch
8048bd7 ! mov ebx, [edi]
8048bd9 ! push 0
8048bdb ! call time
8048be0 ! mov edx, [ebx+8]
8048be3 ! add edx, [ebx]
8048be5 ! add esp, 10h
8048be8 ! cmp eax, edx
8048bea ! jng loc_8048c8c
8048bf0 ! sub esp, 8
8048bf3 ! push strz_Not_ready___8048fb4
Είπαμε πως ο edx περιέχει την ημερομηνία λήξης του demo.
Αυτή προκύπτει από το άθροισμα των τιμών στις διευθύνσεις
[ebx+8] και [ebx]. Προχωρώντας πιο πάνω βλέπουμε πως ο
ebx εξαρτάται από το edi (8048bd7 ! mov ebx, [edi]) το
οποίο με τη σειρά του εξαρτάται από το ebp (8048bc9 ! mov
edi, [ebp+8]). Μάλιστα το ebp+8 είναι η πρώτη παράμετρος
της συνάρτησης στην οποία είμαστε! Επομένως, κατά κάποιο
τρόπο τα δεδομένα για τη λήξη έχουν περαστεί ως
παράμετρος στη συνάρτηση. Παρατηρήστε πως η παράμετρος
γίνεται dereferenced 2 φορές και επομένως μπορούμε να
υποθέσουμε πως είναι κάποιο είδος δείκτη σε δείκτη. Η
συνάρτηση στην οποία βρισκόμαστε καλείται από το σημείο
0x8048a7d (xref c8048a7d) οπότε καλό θα ήταν να ελέγξουμε
τι συμβαίνει εκεί.
80489c8 !
....... ! offset_80489c8: ;xref o804892f
....... ! push ebp
80489c9 ! mov ebp, esp
80489cb ! push esi
80489cc ! push ebx
80489cd ! sub esp, 70h
80489d0 ! and esp, 0fffffff0h
80489d3 ! push eax
80489d4 ! lea ebx, [ebp-78h]
80489d7 ! push ebx
80489d8 ! mov eax, [ebp+0ch]
80489db ! push dword ptr [eax]
80489dd ! push 3
80489df ! call __xstat
80489e4 ! mov eax, [ebp-38h]
80489e7 ! mov [ebp-18h], eax
80489ea ! mov eax, [ebp-40h]
80489ed ! mov [ebp-14h], eax
80489f0 ! lea esi, [ebp-18h]
80489f3 ! mov dword ptr [ebp-10h], 2a300h
80489fa ! mov edx, ?data_804a470
80489ff ! mov eax, 1
8048a04 ! lock add [edx], eax
8048a07 ! mov dword ptr [ebp-74h], ?data_804a474
8048a0e ! mov dword ptr [ebp-6ch], 0
8048a15 ! mov dword ptr [ebp-68h], 0
8048a1c ! mov dword ptr [ebp-70h], data_804a0a8
8048a23 ! mov dword ptr [ebp-60h], 0
8048a2a ! mov dword ptr [ebp-5ch], 0
8048a31 ! mov dword ptr [ebp-64h], data_804a098
8048a38 ! mov dword ptr [ebp-54h], 0
8048a3f ! mov dword ptr [ebp-50h], 0
8048a46 ! mov dword ptr [ebp-58h], data_804a078
8048a4d ! mov dword ptr [ebp-48h], 0
8048a54 ! mov dword ptr [ebp-44h], 0
8048a5b ! mov dword ptr [ebp-4ch], data_804a088
8048a62 ! mov dword ptr [ebp-3ch], 0
8048a69 ! mov dword ptr [ebp-38h], 0
8048a70 ! mov dword ptr [ebp-40h], data_804a068
8048a77 ! mov [ebp-78h], esi
8048a7a ! mov [esp], ebx <---- Ο ebx είναι παράμετρος της συνάρτησης
8048a7d ! call sub_8048bc0 <---- Η συνάρτηση στην οποία βρισκόμασταν
8048a82 ! mov edx, [ebp-74h]
Πάνω από την call sub_8048bc0 υπάρχει η εντολή mov
[esp],ebx. Αυτή είναι ένας τρόπος να
αντικαταστήσουμε την κορυφαία τιμή στο σωρό. Αντιστοιχεί
με pop <κάπου>, push ebx. Παρατηρήστε πιο πάνω πως
η _xstat δεν "καθαρίζει" το σωρό και επομένως, η τιμή που
αντικαθίσταται είναι απλώς η πρώτη παράμετρος της _xstat,
άχρηστη πια (για περισσότερες πληροφορίες περί σωρού βλ.
προηγούμενο άρθρο #1).
Και τώρα αρχίζει το μπλέξιμο...
Πιο πάνω:
80489d4 ! lea ebx, [ebp-78h] (load effective address)
Ο ebx, δηλαδή, περιέχει τη διεύθυνση ebp-78
(είναι ένας δείκτης προς αυτή). Ας προσπαθήσουμε να βρούμε
τι περιέχει αυτή η διεύθυνση. Ψάχνοντας για μια άλλη
αναφορά στην ebp-78 βρίσκουμε λίγο πριν την κλήση της
συνάρτησης sub_8048bc0:
8048a77 ! mov [ebp-78h], esi
Ο καταχωρητής esi παίρνει τιμή πιο πάνω και
περιέχει τη διεύθυνση της θέσης μνήμης ebp-18:
80489f0 ! lea esi, [ebp-18h]
Σχηματικά:
Στη συνάρτηση sub_8048bc0 θυμηθείτε πως η παράμετρος
edi=[ebp+8] (που είναι το ebx της καλούσας συνάρτησης και
του σχήματος) γινόταν dereferenced μια φορά στο 8048bd7 !
mov ebx, [edi] οπότε και το ebx περιέχει τη διεύθυνση
ebp'-18 (με τον ebp' να είναι ο frame pointer της
προηγούμενης (καλούσας) συνάρτησης). Μετά είχαμε τα [ebx]
και [ebx+8] που αναφέρονται τελικά στο [ebp'-18] και
[ebp'-10]. Επομένως, επιστρέφοντας τη συζήτηση στην
καλούσα συνάρτηση (ebp=ebp'), τo άθροισμα
[ebp-18]+[ebp-10] καθορίζει πότε θα λήξει το πρόγραμμα!
Στη διεύθυνση 0x80489f3 έχουμε mov dword ptr [ebp-10h],
2a300h δηλαδή το ένα από τα δύο μέρη του αθροίσματος έχει
τη σταθερή τιμή 0x2a300=172800. Επειδή όλες οι χρονικές
συγκρίσεις γίνονται σε δευτερόλεπτα μπορούμε να
υποθέσουμε πως και αυτή η τιμή είναι σε δευτερόλεπτα
οπότε 172800sec=48h=2 μέρες! Το πρώτο κομμάτι του
αθροίσματος παίρνει τιμή μετά από μια κλήση στην stat.
Μετά από λίγο ψάξιμο συμπεραίνουμε πως είναι η τιμή που
έχει, είναι η χρονική στιγμή της τελευταίας τροποποίησης
του εκτελέσιμου αρχείου (βλέπε ασκήσεις...).
Με λίγα λόγια λοιπόν, το πρόγραμμα διαβάζει την ώρα
τελευταίας τροποποίησης του αρχείου, προσθέτει σε αυτή 2
μέρες και ελέγχει αν η τρέχουσα ώρα είναι μεγαλύτερη από
αυτό το όριο. Αν αναλογιστεί κάποιος την προστασία αυτή,
συμπεραίνει πως είναι εντελώς άχρηστη :) Εκτός από το
γεγονός ότι αν γυρίσουμε το ρολόι πίσω το ληγμένο
πρόγραμμα λειτουργεί ξανά, μπορούμε απλώς να κάνουμε
touch το εκτελέσιμο ώστε να αλλάξουμε το last
modification time και έτσι να επεκτείνουμε το όριο για 2
μέρες ακόμα!
Αν θέλουμε να πειράξουμε το πρόγραμμα για να λειτουργεί
για πάντα (σχεδόν...), μια επιλογή είναι να
αντικαταστήσουμε την
80489f3 ! mov dword ptr [ebp-10h], 2a300h
με
80489f3 ! mov dword ptr [ebp-10h], 7fffffffh (η μεγαλύτερη θετική τιμή)
και την
80489e4 ! mov eax, [ebp-38h]
με
80489e4 ! xor eax, eax
Η δεύτερη αλλαγή γίνεται ώστε να μην έχουμε αρνητικό αποτέλεσμα κατά την πρόσθεση
των [ebp-10h] και [ebp-18h]. Αυτό θα είχε ως συνέπεια ο έλεγχος να αποτυγχάνει πάντα!
Αρκεί, λοιπόν, να βρούμε που στο αρχείο βρίσκεται η
συγκεκριμένη εντολή. Θα μπορούσαμε να ψάξουμε το αρχείο
για την ακολουθία από bytes που αποτελούν την εντολή και
μερικές άλλες γύρω της (βλέπε hands-on στο προηγούμενο
τεύχος) αλλά αυτή τη φορά θα στηριχτούμε στον ELF Header.
H έξοδος του objdump είναι:
bash$ objdump -x ./hands-on-unpacked
./hands-on-unpacked: file format elf32-i386
./hands-on-unpacked
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048918
Program Header:
PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags r-x
INTERP off 0x00000114 vaddr 0x08048114 paddr 0x08048114 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00001050 memsz 0x00001050 flags r-x
LOAD off 0x00001050 vaddr 0x0804a050 paddr 0x0804a050 align 2**12
filesz 0x000002f4 memsz 0x00000430 flags rw-
DYNAMIC off 0x000011f8 vaddr 0x0804a1f8 paddr 0x0804a1f8 align 2**2
filesz 0x000000e0 memsz 0x000000e0 flags rw-
NOTE off 0x00000128 vaddr 0x08048128 paddr 0x08048128 align 2**2
filesz 0x00000020 memsz 0x00000020 flags r--
EH_FRAME off 0x00001014 vaddr 0x08049014 paddr 0x08049014 align 2**2
filesz 0x0000003c memsz 0x0000003c flags r--
...
Η διεύθυνση 0x80489f3 βρίσκεται στο τρίτο
segment διότι αυτό καταλαμβάνει τις διευθύνσεις
0x08048000-0x08049050 (
vaddr μέχρι
vaddr+memsz-1) στην οποία ανήκει και η
προηγούμενη.
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00001050 memsz 0x00001050 flags r-x
Το virtual offset της 0x80489f3 από την αρχή
του segment είναι 0x80489f3-0x08048000=0x09f3. Στο αρχείο,
τώρα, το segment αρχίζει από το 0 και επομένως η διεύθυνση
0x80489f3 αντιστοιχεί στο byte offset 0 + 0x09f3=0x09f3.
Απλά μαθηματικά :)
Η εντολή καταλαμβάνει 7 bytes (0x80489fa - 0x80489f3,
όπου 0x8048bf0 η αρχή της επόμενης εντολής): 0xc7 0x45
0xf0 0x00 0xa3 0x02 0x00. Τα τονισμένα bytes είναι
η τιμή 0x0002a300 (σε little endian μορφή) τα οποία αρκεί
να αντικαταστήσουμε με 0xff 0xff 0xff 0x7f.
Ομοίως, βρίσκουμε ότι η διεύθυνση 0x80489e4 αντιστοιχεί
στο byte offset 0x09e4. Η εντολή καταλαμβάνει 3 bytes: 8b
45 c8. Αντικαθιστούμε το πρώτο byte με 0x31, το δεύτερο
με 0xc0 (xor eax, eax) και τo τελευταίο με 0x90 (nop).
Τώρα το εκτελέσιμο θα λειτουργεί χωρίς πρόβλημα για τα
επόμενα 30 χρόνια περίπου :)
-
Πως φτάσαμε στο συμπέρασμα πως η εντολή 80489e7 !
mov [ebp-18h], eax τοποθετεί στη διεύθυνση
[ebp-18h] την ώρα τελευταίας τροποποίησης του
εκτελέσιμου;
-
Εκτός από τον χρονικό έλεγχο για την εκτύπωση του
"Ready>"/"Not Ready>" γίνεται χρονικός έλεγχος
και σε κάποιο άλλο σημείο. Που είναι αυτό και τι
επιπτώσεις έχει στο πρόγραμμα;
Ο πηγαίος κώδικας του προγράμματος: rce2-files/hands-on.cpp.gz.
Το πρόγραμμα έχει γραφεί επίτηδες ώστε να χρησιμοποιεί
στοιχεία της C++ τα οποία στην προκειμένη περίπτωση δεν
αποτελούν την καλύτερη, σχεδιαστικά, επιλογή. Για
παράδειγμα, όλες οι μέθοδοι των κλάσεων έχουν δηλωθεί
(έμμεσα) να είναι inline και για αυτό δημιουργείται ένας
μικρός χαμός στο εκτελέσιμο!
Επόμενο Προηγούμενο Περιεχόμενα