💾 Archived View for magaz.hellug.gr › 32 › 05_rce1 › index.gmi captured on 2024-08-31 at 12:22:57. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2024-02-05)
-=-=-=-=-=-=-
Φραντζής Αλέξανδρος (aka Alf) Απρ 2003
Reverse Engineering, από τη σκοπιά του απλού χρήστη :-)
1. Μέρος 0 - Εισαγωγή στο Reverse Code Engineering
2. Γιατί RCE
3. Tools of the trade.
4. GDB - Ο παρεξηγημένος debugger
5. Πρόκληση 0
"In the beginning the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move."- HGG Book 2
Τη δεκαετία του '60-'70 τα προγράμματα άρχισαν να μεγαλώνουν και να γίνονται χαώδη (βλέπε ΙΒΜ OS/360). Ο Dikjstra προσπάθησε να βάλει τάξη αλλά ήταν ήδη αργά :) Πολλοί προγραμματιστές βρέθηκαν στη δύσκολη κατάσταση να λαμβάνουν τόνους παρατημένου κώδικα (σε assembly βέβαια) που έπρεπε να χρησιμοποιήσουν. Τα σχόλια που συνόδευαν τα προγράμματα ήταν συχνά λειψά και στη χειρότερη περίπτωση λανθασμένα(!). Οι επιλογές ήταν δύο· να ξαναγραφεί ο κώδικας από την αρχή ή να χρησιμοποιηθεί (με τα χίλια ζόρια) όπως είναι. Οποιοσδήποτε, όμως, δρόμος απαιτούσε την πλήρη κατανόηση του ήδη υπάρχοντος κώδικα. Ήταν μια δύσκολη δουλειά αλλά κάποιος έπρεπε να την κάνει. Έτσι λοιπόν γεννήθηκε το Reverse Code Engineering.
Σήμερα τα πράγματα είναι πολύ διαφορετικά από τη μεριά των developers. Οι εφαρμογές αναπτύσσονται σχεδόν αποκλειστικά σε γλώσσες υψηλού επιπέδου (και με εργαλεία ακόμα πιο υψηλού επιπέδου). Οι ίδιοι οι προγραμματιστές, τις περισσότερες φορές, δεν έρχονται σε επαφή με τον object κώδικα που παράγουν οι compilers. Τα έτοιμα modules, που καλούνται να χρησιμοποιήσουν, είναι σε μορφή πηγαίου κώδικα ή σε βιβλιοθήκες με καλά καθορισμένη διεπαφή.
Μπορεί η διαδικασία παραγωγής να έχει αλλάξει, ο τελικός χρήστης όμως έχει στα χέρια του ότι και παλιότερα: μια ακατανόητη, για την πλειονότητα, σειρά από bytes (εξαιρούνται τα open source) κρυμμένα πίσω από στρώματα αφαιρετικότητας (πχ εικονίδιο). Τώρα πια η τέχνη του RCE εξασκείται κυρίως από περίεργους χρήστες και σπάνια από developers.
Τώρα έφτασε η μαγική στιγμή για να ορίσουμε το Reverse Code Engineering. Σε ελεύθερη μετάφραση στα ελληνικά ονομάζεται Αντίστροφη Μηχανική Κώδικα και είναι η διαδικασία κατά την οποία εξάγεται η λειτουργικότητα ενός προγράμματος από τον κώδικα του, ο οποίος συνήθως βρίσκεται σε κάποια "δύσπεπτη" (συνήθως assembly) μορφή.
Σε αυτό το σημείο θα ήταν σκόπιμο να αναφερθούμε λίγο στη έννοια cracker. Σε κάποιον εκτός των πραγμάτων ίσως να θυμίζει τα μπισκότα που δίνουμε στους παπαγάλους (Poly wanna cracker?). Συχνά συνδέεται με τον κόσμο των δικτύων και αναφέρεται στον κακόβουλο hacker ο οποίος σπέρνει την καταστροφή στο πέρασμα του :) Στον σύμπαν του RCE το cracking αναφέρεται στη διαδικασία, κατά την οποία προσπαθούμε να ξεπεράσουμε ένα σύστημα ασφαλείας στο επίπεδο του λογισμικού (πχ copy protection). Είναι ουσιαστικά υποκατηγορία του RCE διότι αν και χρησιμοποιεί τις ίδιες τεχνικές έχει πιο περιορισμένο σκοπό και σπάνια απαιτεί την πλήρη κατανόηση του λογισμικού.
Βασική προϋπόθεση για την επιτυχία μιας προσπάθειας στο RCE είναι η γνώση των εργαλείων που υπάρχουν, των λεγόμενων tools of the trade. Τα εργαλεία χωρίζονται σε δύο βασικές κατηγορίες ανάλογα με την προσέγγιση που χρησιμοποιούν. Από τη μία υπάρχουν τα εργαλεία που επιτρέπουν την παρακολούθηση της δυναμικής εκτέλεσης του κώδικα και αποτελούν την live προσέγγιση. Τέτοια εργαλεία είναι οι debuggers με πιo χαρακτηριστικά παραδείγματα το πραγματικά πανίσχυρο Numega Softice για Windows και το περιβόητο :) GDB για το linux. Από την άλλη υπάρχουν εργαλεία που παρουσιάζουν τον κώδικα σε στατική μορφή (dead listing). Αυτά είναι disassemblers όπως w32Dasm, IDA (τρέχει σε windows αλλά υποστηρίζει και ELF-linux εκτελέσιμα) και biew, ldasm (linux). Στο linux οι περισσότεροι disassemblers είναι scripts που χρησιμοποιούν την έξοδο του objdump, που περιέχεται στα binutils. Τέλος αξίζει να αναφερθούμε σε μια υβριδική κατηγορία εργαλείων τα οποία εγώ ονομάζω undead. Αυτά παρουσιάζουν την δυναμική εκτέλεση του κώδικα αλλά δεν δίνουν τη δυνατότητα τροποποίησης της εξέλιξης του. Συνήθως οι πληροφορίες που δίνουν είναι πιο υψηλού επιπέδου από assembly. Παράδειγμα τέτοιου εργαλείου είναι το strace που καταγράφει τα system calls που κλήθηκαν από ένα πρόγραμμα.
O GDB (GNU DeBugger) αποτελεί πνευματικό παιδί του Richard Stallman, ιδρυτή του FSF (Free Software Foundation). Υποστηρίζει πολλές αρχιτεκτονικές (x86, alpha, MIPS...) και γλώσσες υψηλού επιπέδου (C, C++, Fortran, Modula-2, Pascal, CHILL). Υποστηρίζει (conditional, hardware) breakpoints, remote debugging. Έχει, λοιπόν, όλα εκείνα τα χαρακτηριστικά που τον καθιστούν έναν πολύ ισχυρό debugger. Ποίο είναι το πρόβλημα λοιπόν;
Όπως δηλώνει και ο τίτλος, ο GDB είναι ο ορισμός του παρεξηγημένου debugger. Κατά καιρούς έχει χαρακτηριστεί με επίθετα όπως "brain-damaged", άδικα κατά την ταπεινή μου γνώμη. Το βασικό επιχείρημα των πολέμιων του GDB είναι το user interface. Και όντως, το UI καμία σχέση δεν έχει με το γραφικό περιβάλλον πχ του M$ Visual Studio. Εδώ έχουμε να κάνουμε με command line σε όλο της το μεγαλείο! Όσοι έχουν ασχοληθεί με το Softice στα windows καταλαβαίνουν τι εννοώ. Βέβαια πολλοί έσπευσαν να βελτιώσουν την κατάσταση και έτσι σήμερα υπάρχει μία πληθώρα από front-ends: το built-in Text User Interface (TUI) σε curses, DataDisplayDebugger (DDD) για Χ11/Motif, Kdbg gia KDE κ.α. Στο κείμενο αυτό θα ασχοληθούμε με την απλή μορφή του GDB (άντε και με το TUI :) ). Θα αρχίσουμε με source-code debugging...
Βασική δυνατότητα ενός debugger είναι η παρακολούθηση της εκτέλεσης ενός άλλου προγράμματος και η εν δυνάμει αλλαγή της εξέλιξης του είτε άμεσα, είτε έμμεσα μέσω της αλλαγής των δεδομένων του. Στο κομμάτι αυτό θα χρησιμοποιηθεί ως παράδειγμα ο παρακάτω C κώδικας:
#include <stdio.h> int main(int argc, char **argv) { int num; if (argc < 2) { printf("Usage: %s <number>\n",argv[0]); exit(1); } num=alf(argv[1]); if (num > 10) printf("Ok!\n"); else printf("Failed!\n"); } int alf(char *s) { return atoi(s); }
Κάντε compile με : gcc -g -o rce1 rce1.c
To flag -g λέει στον compiler να περιλάβει στο εκτελέσιμο αρχείο εκτός από το symbol table, πληροφορίες που χρειάζεται ο GDB για source-code debugging. Αν ένα πρόγραμμα δεν έχει τέτοιες πληροφορίες τότε μπορούμε μόνο να δούμε τον assembly κώδικα (και τα σύμβολα).
Το παραπάνω (παντελώς άχρηστο) πρόγραμμα το μόνο που κάνει είναι να ελέγχει αν η πρώτη παράμετρος στη γραμμή εντολής είναι μεγαλύτερη από 10 και τυπώνει το κατάλληλο μήνυμα.
Καταρχάς πρέπει να φορτώσουμε το πρόγραμμα στο GDB:
bash$ gdb -q rce1 (gdb)
Το switch -q/--quiet λέει στον gdb να μη δείχνει τα εισαγωγικά μηνύματα. Από εδώ και πέρα θα εννοείται ακόμα και αν δεν γράφεται (πχ alias gdb ="gdb -q").
Το (gdb) prompt δηλώνει πως ο debugger έχει σταματήσει το πρόγραμμα και είναι έτοιμος να δεχτεί εντολές. Παρατηρήστε πως ο GDB δεν έγραψε κάποιο μήνυμα επιβεβαίωσης ότι έγινε σωστά το φόρτωμα του rce1. Εφόσον δεν υπάρχει μήνυμα λάθους η διαδικασία ολοκληρώθηκε επιτυχώς.
H έξοδος από τον debugger γίνεται με την "quit"/"q"
(gdb) q bash$
Μια εναλλακτική μέθοδος για να φορτώνουμε αρχεία είναι με την εντολή file του GDB. Η file φορτώνει το εκτελέσιμο στη μνήμη ΚΑΙ το symbol table στον GDB. Υπάρχει και η exec-file η οποία φορτώνει μόνο τo εκτελέσιμο στη μνήμη.
bash$ gdb (gdb) file rce1 Reading symbols from rce1...done. (gdb)
Σημείωση: Ο GDB περιλαμβάνει ένα αρκετά πλήρες σύστημα βοήθειας με την εντολή help.
(gdb) help file Use FILE as program to be debugged. It is read for its symbols, for getting the contents of pure memory, and it is the program executed when you use the `run' command. If FILE cannot be found as specified, your execution directory path ($PATH) is searched for a command of that name. No arg means to have no executable file and no symbols. (gdb)
Για να δούμε τον κώδικα που έχουμε φορτώσει χρησιμοποιούμε τη εντολή list. Η εντολή έχει διάφορες μορφές. Χωρίς παραμέτρους εμφανίζει 10 γραμμές πηγαίου κώδικα γύρω από την τρέχουσα ή τις πρώτες 10 γραμμές αν το πρόγραμμα δεν εκτελείται.
(gdb) list 1 #include <stdio.h> 2 3 int main(int argc, char **argv) 4 { 5 int num; 6 7 if (argc<2) { 8 printf("Usage: %s <number>\n",argv[0]); 9 exit(1); 10 } (gdb)
Αν η list έχει μία παράμετρο τότε εμφανίζει 10 γραμμές κώδικα γύρω από αυτή ενώ μπορούμε να προσδιορίσουμε και ένα διάστημα *list x,y*
(gdb) list 8 3 int main(int argc, char **argv) 4 { 5 int num; 6 7 if (argc<2) { 8 printf("Usage: %s <number>\n",argv[0]); 9 exit(1); 10 } 11 12 num=alf(argv[1]); (gdb) list 9,14 9 exit(1); 10 } 11 12 num=alf(argv[1]); 13 14 if (num>10) (gdb)
Αφού το πρόγραμμα έχει φορτωθεί μπορούμε να το εκτελέσουμε με την εντολή run ή r. Η run δέχεται ως παραμέτρους τα command-line arguments που θέλουμε να περάσουμε στο πρόγραμμα.
(gbd) r Starting program: /home/alf/temp/rce1 Usage: /home/alf/temp/rce1 <number>Program exited with code 01. (gdb) r 42 Starting program: /home/alf/temp/rce1 42 Ok! Program exited with code 04. (gdb) r 3 Starting program: /home/alf/temp/rce1 3 Failed! Program exited with code 07. (gdb) r Starting program: /home/alf/temp/rce1 3 Failed! Program exited with code 07.
Παρατηρήστε ότι στην απλή r τα command-line arguments παραμένουν από την προηγούμενη εκτέλεση. Αυτά είναι αποθηκευμένα στην εσωτερική μεταβλητή του GDB "args". Υπάρχει μια πληθώρα από εσωτερικές μεταβλητές που μπορούν να προσπελαστούν με τις show και set (hint: μη ξεχνάτε το help...).
(gdb) show args Argument list to give program being debugged when it is started is "3". (gdb) set args 666 7 (gdb) r Starting program: /home/alf/temp/rce1 666 7 Ok! Program exited with code 04. (gdb)
Βέβαια ως εδώ το μόνο που έχουμε κάνει είναι... τίποτα! Τα ίδια και με πιο απλό τρόπο θα μπορούσαν να γίνουν από τo command line ενώ εμε��ς θέλουμε να ελέγχουμε το πρόγραμμα βήμα προς βήμα.
Για να γίνει αυτό, πρέπει να φροντίσουμε ο έλεγχος να επιστρέψει πίσω στον debugger όταν αρχίσει το πρόγραμμα. Για την ώρα δεχτείτε αυτή την εντολή χωρίς εξηγήσεις (λίγο υπομονή βρε παιδιά!)
(gdb) break main Breakpoint 1 at 0x8048466: file rce1.c, line 7. (gdb) r Starting program: /home/alf/projects/rce1 Breakpoint 1, main (argc=1, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb)
Αυτό που κάναμε ήταν να πούμε στον GDB να σταματήσει την εκτέλεση του προγράμματος όταν μπει στη συνάρτηση main. Τώρα είμαστε πριν την εκτέλεση της πρώτης εντολής της main και ο GDB περιμένει οδηγίες. Για να εκτελέσουμε την τρέχουσα εντολή χρησιμοποιούμε την εντολή next ή n:
(gdb) next 8 printf("Usage: %s <number>\n",argv[0]); (gdb) n Usage: /home/alf/projects/rce1 <number>9 exit(1); (gdb) n Program exited with code 01.
Επειδή δεν περάσαμε παραμέτρους στο πρόγραμμα, το argc ήταν μικρότερο του 2 και εκτυπώθηκε ο τρόπος χρήσης του προγράμματος. Ας ξαναδοκιμάσουμε:
(gdb) r 123 Starting program: /home/alf/projects/rce1 123 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb) n 12 num=alf(argv[1]); (gdb) n 14 if (num>10) (gdb) n 15 printf("Ok!\n"); (gdb) n Ok! 18 } (gdb) n 0x4003abb4 in __libc_start_main () from /lib/libc.so.6 (gdb) n Single stepping until exit from function __libc_start_main, which has no line number information. Program exited with code 04. (gdb)
Παρατηρήστε ότι μετά τη γραμμή 18 το πρόγραμμα δεν κάνει exit αλλά επιστρέφουμε σε μια συνάρτηση που ανήκει στην libc.so.6. Επειδή δεν έχουμε debugging πληροφορίες για αυτή, η next απλώς προχωράει μέχρι να τελειώσει η συνάρτηση. Αυτό που συμβαίνει ακριβώς είναι ότι με την next προχωράμε μια γραμμή κώδικα αλλά o GDB δεν έχει πληροφορίες για ποίες εντολές assembly αντιστοιχούν σε κάθε γραμμή, οπότε δεν ξέρει πόσο να προχωρήσει. H __libc_start_main() είναι στην πραγματικότητα η πρώτη συνάρτηση που έχει κληθεί από το πρόγραμμα μας και έχει στόχο να αρχικοποιήσει την libc και μετά να καλέσει τη δική μας main (περισσότερα για αυτό στο επόμενο μέρος, όταν θα ασχοληθούμε με την assembly μορφή του κώδικα).
Αν ενώ είμαστε στον GDB θέλουμε να συνεχίσει κανονικά η εκτέλεση του προγράμματος μπορούμε να χρησιμοποιήσουμε την εντολή continue ή c. To πρόγραμμα συνεχίζει μέχρι να συναντήσει κάποιο breakpoint ή να τερματιστεί.
(gdb) r 123 Starting program: /home/alf/projects/rce1 123 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb) n 12 num=alf(argv[1]); (gdb) c Continuing. Ok! Program exited with code 01. (gdb)
Εκτός από την next υπάρχει και η step ή s η οποία κάνει ότι και η next με τη διαφορά ότι αν η τρέχουσα εντολή είναι κλήση συνάρτησης η step μπαίνει μέσα στον κώδικα της συνάρτησης.
(gdb) r 123 Starting program: /home/alf/projects/rce1 123 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb) n 12 num=alf(argv[1]); (gdb) s alf (s=0xbffff94f "123") at rce1.c:25 25 return atoi(s); (gdb) n 26 } (gdb) n main (argc=2, argv=0xbffff7e4) at rce1.c:14 14 if (num>10) (gdb) c Continuing. Ok! Program exited with code 01. (gdb)
Ας δοκιμάσουμε μερικές ακόμη εντολές:
(gdb) r 123 abc Starting program: /home/alf/projects/rce1 123 abc Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb) print argc $1 = 3 (gdb) set argc=6 (gdb) print argc $2 = 6 (gdb) set argc=argc-2 (gdb) print argc $3 = 4
Παραπάνω είδαμε δύο σημαντικές εντολές για να εξετάζουμε δεδομένα, την print και την set (η οποία όπως είδαμε χρησιμοποιείται και για εσωτερικές μεταβλητές). Είναι πολύ βασικό να σημειωθεί πως ό,τι ακολουθεί τις print και set είναι έκφραση της C, γεγονός που μας δίνει ιδιαίτερη ευελιξία O GDB αναγνωρίζει αυτόματα τον τύπο της έκφρασης και παρουσιάζει τα δεδομένα με τον κατάλληλο τρόπο.
(gdb) print &argc $4 = (int *) 0xbffff790 (gdb) print &argc + 1 $5 = (int *) 0xbffff794 (gdb) print (char *)&argc + 1 $6 = 0xbffff791 ""
Στην πρώτη εντολή λέμε στον GDB να τυπώσει την διεύθυνση της μεταβλητής argc. Το αποτέλεσμα της δεύτερης εντολής ίσως να φαίνεται λίγο παράξενο. Πολλοί θα περίμεναν να είναι 0xbffff791. Επειδή η argc είναι τύπου int που στη συγκεκριμένη περίπτωση είναι 4 bytes το &argc + 1 δείχνει 4 bytes μπροστά. Γενικά, αν p είναι δείκτης σε τύπο Τ, το p + n δείχνει στη θέση μνήμης p+n*sizeof(T) ( εδώ &argc + 1*sizeof(int) ). Αν κάνουμε cast το &argc σε (char *) το αποτέλεσμα είναι το αναμενόμενο, διότι ο char είναι εξ ορισμού 1 byte.
(gdb) print argv[1] $7 = 0xbffff94a "123" (gdb) print argv[2] $8 = 0xbffff94d "abc" (gdb) print argv[0] $9 = 0xbffff932 "/home/alf/projects/rce1"
Ο GDB αναγνωρίζει πως οι μεταβλητές πρόκειται για strings (char *) και εμφανίζει το περιεχόμενο τους. Ας παίξουμε λίγο με τα strings :)
(gdb) set argv[1]="555" (gdb) print argv[1] $10 = 0x8049588 "555"
Παρατηρείστε πως ο GDB έκανε μία σοφή κίνηση: δέσμευσε μόνος του χώρο για το καινούργιο string και άλλαξε τον δείκτη argv[1] να δείχνει στον καινούργιο χώρο. O παλιός έμεινε όπως είναι:
(gdb) print (char *)0xbffff94a $11 = 0xbffff94a "123"
Μετά από όλα αυτά, ήρθε επιτέλους η ώρα να ασχοληθούμε με ένα από τα πιο σημαντικά στοιχεία ενός debugger, τα breakpoints. Όπως δηλώνει και το όνομα τους είναι σημεία στον κώδικα όπου διακόπτεται η εκτέλεση και ο έλεγχος επιστρέφει στον debugger. H βασική εντολή στον GDB για να τεθεί ένα BP είναι η break ή b. Δέχεται (στη βασική της μορφή) μία παράμετρο: το σημείο όπου θα διακοπεί η εκτέλεση. Η παράμετρος έχει τις εξής μορφές:
Χρήσιμες εντολές για τα BPs είναι η info break η οποία είναι εμφανές τι κάνει :), η delete [n] η οποία σβήνει το BP #n (η όλα αν δεν προσδιορίσουμε αριθμό), η disable [n] η οποία απενεργοποιεί προσωρινά το BP #n (η όλα...) και η αντίθετη της, η enable [n].
bash$ gdb rce1 (gdb) break main Breakpoint 1 at 0x804839c: file rce1.c, line 7. (gdb) break alf Breakpoint 2 at 0x804840c: file rce1.c, line 25. (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x0804839c in main at rce1.c:7 2 breakpoint keep y 0x0804840c in alf at rce1.c:25 (gdb) disable 1 (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep n 0x0804839c in main at rce1.c:7 2 breakpoint keep y 0x0804840c in alf at rce1.c:25 (gdb) r 1 Starting program: /home/alf/projects/rce1 1 Breakpoint 2, alf (s=0xbffff951 "1") at rce1.c:25 25 return atoi(s); (gdb) n 26 } (gdb) n main (argc=2, argv=0xbffff7e4) at rce1.c:14 14 if (num>10) (gdb) c Continuing. Failed! Program exited with code 01. (gdb)
Μια εναλλακτική (και πολύ χρήσιμη) μορφή της break είναι η break ... if expr, με τη οποία η εκτέλεση διακόπτεται μόνο αν η έκφραση expr είναι αληθής.
bash$ gdb rce1 (gdb) list 10 5 int num; 6 7 if (argc<2) { 8 printf("Usage: %s <number>\n",argv[0]); 9 exit(1); 10 } 11 12 num=alf(argv[1]); 13 14 if (num>10) (gdb) break 14 if (num==10) Breakpoint 1 at 0x80483d7: file rce1.c, line 14. (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x080483d7 in main at rce1.c:14 stop only if num == 10 (gdb) r 4 Starting program: /home/alf/projects/rce1 4 Failed! Program exited with code 01. (gdb) r 10 Starting program: /home/alf/projects/rce1 10 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:14 14 if (num>10) (gdb) c Continuing. Failed! Program exited with code 01. (gdb)
Για να αλλάξουμε τη συνθήκη ενός breakpoint υπάρχει η εντολή cond n [expr] η οποία αλλάζει τη συνθήκη του BP #n σε expr(ή τίποτα).
Επίσης είναι δυνατόν να καθορίσουμε μια σειρά από ενέργειες που θα εκτελούνται όταν "χτυπάει" ένα BP. Αυτό γίνεται με την
commands n list end
Ένα δείγμα του τι δυνατότητες μας δίνει το σύστημα:
(gdb) break 14 if (numi<=10) Breakpoint 1 at 0x80483d7: file rce1.c, line 14. (gdb) commands 1 Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >set num=13 >c >end (gdb) r 20 Starting program: /home/alf/projects/rce1 20 Ok! Program exited with code 01. (gdb) r 3 Starting program: /home/alf/projects/rce1 3 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:14 14 if (num>10) Ok! Program exited with code 01. (gdb)
Το breakpoint "χτυπάει" αλλά συνεχίζει αυτόματα, διότι η τελευταία εντολή στο command list είναι η continue. Καταφέραμε με αυτόν τον τρόπο να κάνουμε το πρόγραμμα να τυπώνει πάντα Ok!, ανεξάρτητα από την τιμή της παραμέτρου στη γραμμή εντολής! Βέβαια, αυτό γίνεται μόνο όταν τρέχουμε το πρόγραμμα μέσα από το GDB. Υπομονή μερικά τεύχη για μια καλύτερη λύση...
Μια παραλλαγή είναι η tbreak (temporary breakpoint) που έχει ακριβώς την ίδια σύνταξη με την break αλλά εκτελείται μόνο μια φορά (γίνεται disabled μετά). Πρακτικά είναι ισοδύναμη με την ακολουθία:
break xyz commands 3 --> Αν υποθέσουμε πως το breakpoint είναι το #3 disable 3 end
Τα watchpoints είναι breakpoints τα οποία δεν ενεργοποιούνται με κριτήριο την εκτέλεση μιας εντολής αλλά την αλλαγή μιας θέσης μνήμης. Για να θέσουμε ένα watchpoint χρησιμοποιούμε την εντολή watch!
(gdb) watch num No symbol "num" in current context. (gdb) break main Breakpoint 1 at 0x804839c: file rce1.c, line 7. (gdb) r 11 Starting program: /home/alf/projects/rce1 11 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb) watch num Hardware watchpoint 2: num (gdb) c Continuing. Hardware watchpoint 2: num Old value = 1075130932 New value = 11 main (argc=2, argv=0xbffff7e4) at rce1.c:14 14 if (num>10) (gdb) c Continuing. Failed! Watchpoint 2 deleted because the program has left the block in which its expression is valid. 0x4003abb4 in __libc_start_main () from /lib/libc.so.6 (gdb) c Continuing. Program exited with code 01. (gdb)
Η πρώτη εντολή (watch num) απέτυχε διότι η num είναι τοπική μεταβλητή και έχει νόημα μόνο μέσα στη main(). Οπότε πρέπει να είμαστε στη main() για να αναφερθούμε σε αυτή. Τελικά ο GDB μας ενημέρωσε πως η μεταβλητή num άλλαξε τιμή σε 11. Παρατηρήστε πως ο έλεγχος γύρισε σε εμάς αμέσως μετά την εντολή που προκάλεσε την αλλαγή, η οποία προφανώς δεν είναι η if (num < 10) αλλά η προηγούμενη num=alf(argv[1]) που δε φαίνεται πουθενά. Ύστερα ο GDB μας λέει πως το watchpoint διαγράφηκε. Αυτό έγινε διότι η num ως τοπική μεταβλητή αποθηκεύεται στο σωρό (stack) και μετά την έξοδο από τη συνάρτηση στην οποία βρισκόταν (τη main()) ο σωρός ελευθερώνεται (δε γίνεται ακριβώς έτσι, όταν εξετάσουμε τον κώδικα σε πιο χαμηλό επίπεδο θα δούμε την διαδικασία επακριβώς)
Παρόμοια με την watch είναι η rwatch η οποία ενεργοποιείται όχι σε αλλαγή της μνήμης αλλά σε απλή ανάγνωση.
(gdb) break main Breakpoint 1 at 0x804839c: file rce1.c, line 7. (gdb) r 12 Starting program: /home/alf/projects/rce1 12 Breakpoint 1, main (argc=2, argv=0xbffff7e4) at rce1.c:7 7 if (argc<2) { (gdb) rwatch num Hardware read watchpoint 2: num (gdb) c Continuing. Hardware read watchpoint 2: num Value = 12 0x080483db in main (argc=2, argv=0xbffff7e4) at rce1.c:14 14 if (num>10) (gdb) c Continuing. Ok! Watchpoint 2 deleted because the program has left the block in which its expression is valid. 0x4003abb4 in __libc_start_main () from /lib/libc.so.6 (gdb) c Continuing. Program exited with code 01.
Ουφφ! Τελειώσαμε... για την ώρα :)
Αυτό το πρώτο άρθρο δεν είναι και πολύ hardcore RCE αλλά ήταν απαραίτητο ώστε να τεθούν κάποιες βάσεις για αυτά που θα ακολουθήσουν. Στο επόμενο άρθρο θα ασχοληθούμε με τη χρήση του GDB για assembly debugging και θα ρίξουμε μια πιο προσεκτική ματιά στα υπόλοιπα εργαλεία. Επίσης θα μιλήσουμε λίγο περισσότερο για τα breakpoints και πιο συγκεκριμένα για το πως αυτά υλοποιούνται σε χαμηλό επίπεδο.
Σε κάθε άρθρο θα υπάρχει ένα πρόβλημα-πρόκληση για να ασχοληθούν όσοι επιθυμούν. Εδώ τα πράγματα είναι κάπως απλά (αλλά όχι πολύ) αφού το εκτελέσιμο είναι compiled με το -g flag, πρακτικά σας δίνω τον πηγαίο κώδικα δηλαδή. Πάντως είναι μια καλή ευκαιρία να ακονίσετε τα GDB skills και να πάρετε μια πρώτη γεύση από RCE!
Σκοπός της πρόκλησης είναι να βρείτε ποίος κωδικός αντιστοιχεί στο όνομα/ψευδώνυμο σας. Όποιοι θέλουν ας μου στείλουν τις απαντήσεις τους για να αναγραφούν στο hall of fame στο επόμενο άρθρο! Οι πρώτοι τρεις παίρνουν δώρο μια ετήσια συνδρομή στο magaz :P
πχ
bash$ challenge0 -h Magaz RCE Challenge 0 Use '-g' to load it into gdb bash$ challenge0 -g (gdb) bash$ challenge0 Name: alf82 Password: 089s33k4das Authentication failed! bash$ challenge0 Name: alf82 Password: 09d12iie78722 Authentication successful!
Μπορείτε να κατεβάσετε το εκτελέσιμο από εδώ[1].
Στείλτε απαντήσεις, σχόλια, διορθώσεις, προσθήκες στο alf82 at freemail.gr. Καλό θα ήταν το subject του email να είναι της μορφής magaz-rce-... ή κάτι τέτοιο, για να τα ξεχωρίζω εύκολα!
Καλό RCE!