Reverse Engineering σε περιβάλλον Linux, Μέρος 2: Hands-on Παράδειγμα - Υπό πίεση
Description: This is issue 34 of Magaz, the Hellenic LinuxMagazine
Keywords: magaz hellenic greek hellas greece hellug linux magazine

Magaz, The Greek Linux Magazine
Τεύχος #34
Περιληπτικά Περιεχόμενα Αναλυτικά Περιεχόμενα Mirrors Links Πληροφορίες Συντάκτες Αναζήτηση Επικοινωνία
> Επόμενο Προηγούμενο Περιεχόμενα ### 6. Hands-on Παράδειγμα - Υπό πίεση ### 6.1 Πράξη 1η Αυτή τη φορά θα ασχοληθούμε με το 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. Θα μπορούσαμε να το αγνοήσουμε, όμως, όπως έχουμε αναφέρει, οι ευκολίες που προσφέρει είναι ανεκτίμητες. Οπότε, στο επόμενο κομμάτι θα κάνουμε ότι μπορούμε για να φέρουμε το εκτελέσιμο όσο πιο κοντά γίνεται στην αυθεντική του μορφή. ### 6.2 Επιστροφή σε Κανονικές Συνθήκες πίεσης και θερμοκρασίας. (ΣΗΜΕΙΩΣΗ: Αν δεν έχετε διαβάσει τις πληροφορίες για το 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
> Αυτό ήταν, τώρα πια έχουμε το εκτελέσιμο στην αυθεντική του μορφή! ### 6.3 Σταματώντας τον χρόνο Αφού αποσυμπιέσαμε ο εκτελέσιμο, ήρθε η ώρα να δούμε πως μπορούμε να απενεργοποιήσουμε την προστασία. Αυτή τη φορά αντί για τον 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 μέρες ακόμα! ### 6.4 To patch Αν θέλουμε να πειράξουμε το πρόγραμμα για να λειτουργεί για πάντα (σχεδόν...), μια επιλογή είναι να αντικαταστήσουμε την
        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 χρόνια περίπου :) ### 6.5 Ασκήσεις για το σπίτι :) Πως φτάσαμε στο συμπέρασμα πως η εντολή 80489e7 ! mov [ebp-18h], eax τοποθετεί στη διεύθυνση [ebp-18h] την ώρα τελευταίας τροποποίησης του εκτελέσιμου; Εκτός από τον χρονικό έλεγχο για την εκτύπωση του "Ready>"/"Not Ready>" γίνεται χρονικός έλεγχος και σε κάποιο άλλο σημείο. Που είναι αυτό και τι επιπτώσεις έχει στο πρόγραμμα; Ο πηγαίος κώδικας του προγράμματος: rce2-files/hands-on.cpp.gz. Το πρόγραμμα έχει γραφεί επίτηδες ώστε να χρησιμοποιεί στοιχεία της C++ τα οποία στην προκειμένη περίπτωση δεν αποτελούν την καλύτερη, σχεδιαστικά, επιλογή. Για παράδειγμα, όλες οι μέθοδοι των κλάσεων έχουν δηλωθεί (έμμεσα) να είναι inline και για αυτό δημιουργείται ένας μικρός χαμός στο εκτελέσιμο! Επόμενο Προηγούμενο Περιεχόμενα