682 γραμμές
38 KiB
Markdown
682 γραμμές
38 KiB
Markdown
+++
|
||
title = 'Reverse Engineering σε Περιβάλλον Linux, Μέρος 0'
|
||
date = '2003-04-01T00:00:00Z'
|
||
description = ''
|
||
author = 'Φραντζής Αλέξανδρος (aka Alf)'
|
||
issue = ['Magaz 32']
|
||
issue_weight = 5
|
||
+++
|
||
|
||
----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
*Reverse Engineering, από τη σκοπιά του απλού χρήστη :-)*
|
||
|
||
----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
||
**1. Μέρος 0 - Εισαγωγή στο Reverse Code Engineering**
|
||
-----------------------------------------------------------------------------
|
||
|
||
**2. Γιατί RCE**
|
||
---------------------------------------
|
||
|
||
**3. Tools of the trade.**
|
||
-------------------------------------------------
|
||
|
||
**4. GDB - Ο παρεξηγημένος debugger**
|
||
------------------------------------------------------------
|
||
|
||
- [4.1 GDB - Τα βασικά](#ss4.1)
|
||
|
||
**5. Πρόκληση 0**
|
||
----------------------------------------
|
||
|
||
|
||
### [1. Μέρος 0 - Εισαγωγή στο Reverse Code Engineering]{#s1}
|
||
|
||
\"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 διότι αν και χρησιμοποιεί τις ίδιες τεχνικές έχει πιο περιορισμένο σκοπό και σπάνια απαιτεί την πλήρη
|
||
κατανόηση του λογισμικού.
|
||
|
||
|
||
### [2. Γιατί RCE]{#s2}
|
||
|
||
- 1\. **Γνώση**: Η παρατήρηση της εσωτερικής δομής των προγραμμάτων οδηγεί σε καλύτερη κατανόηση της λειτουργίας τους αλλά και του υπολογιστή γενικότερα.
|
||
- 2\. **Σιγουριά**: Γνωρίζοντας την εσωτερική λειτουργία ενός προγράμματος είσαι πια σίγουρος πως το λογισμικό κάνει αυτό που λέει, τίποτα λιγότερο και τίποτα
|
||
περισσότερο.
|
||
- 3\. **Ευτυχία**: Η διαδικασία του RE είναι ένα πνευματικό παιχνίδι. Αν βγεις νικητής δεν μπορείς παρά να νιώσεις ευτυχισμένος.
|
||
|
||
|
||
### [3. Tools of the trade.]{#s3}
|
||
|
||
Βασική προϋπόθεση για την επιτυχία μιας προσπάθειας στο 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 που κλήθηκαν από ένα πρόγραμμα.
|
||
|
||
|
||
### [4. GDB - Ο παρεξηγημένος debugger]{#s4}
|
||
|
||
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\...
|
||
|
||
### [4.1 GDB - Τα βασικά]{#ss4.1}
|
||
|
||
Βασική δυνατότητα ενός 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"
|
||
>
|
||
>
|
||
|
||
#### Breakpoints
|
||
|
||
Μετά από όλα αυτά, ήρθε επιτέλους η ώρα να ασχοληθούμε με ένα από τα πιο σημαντικά στοιχεία ενός debugger, τα breakpoints. Όπως δηλώνει και το όνομα τους είναι
|
||
σημεία στον κώδικα όπου διακόπτεται η εκτέλεση και ο έλεγχος επιστρέφει στον debugger. H βασική εντολή στον GDB για να τεθεί ένα BP είναι η **break** ή **b**.
|
||
Δέχεται (στη βασική της μορφή) μία παράμετρο: το σημείο όπου θα διακοπεί η εκτέλεση. Η παράμετρος έχει τις εξής μορφές:
|
||
|
||
- 1\. Το αρχείο(προαιρετικά) και τη γραμμή του πηγαίου κώδικα \[file:\]line
|
||
- 2\. Το όνομα μιας συνάρτησης πχ main
|
||
- 3\. Μια θέση μνήμης πχ \*0x8048560 (προσέξτε το \'\*\')
|
||
|
||
Χρήσιμες εντολές για τα 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
|
||
|
||
Τα 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 και πιο συγκεκριμένα για το πως αυτά υλοποιούνται σε χαμηλό επίπεδο.
|
||
|
||
|
||
### [5. Πρόκληση 0]{#s5}
|
||
|
||
Σε κάθε άρθρο θα υπάρχει ένα πρόβλημα-πρόκληση για να ασχοληθούν όσοι επιθυμούν. Εδώ τα πράγματα είναι κάπως απλά (αλλά όχι πολύ) αφού το εκτελέσιμο είναι
|
||
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!
|
||
>
|
||
>
|
||
|
||
Μπορείτε να κατεβάσετε το εκτελέσιμο από [εδώ](./challenge0.bz2).
|
||
|
||
Στείλτε απαντήσεις, σχόλια, διορθώσεις, προσθήκες στο alf82 at freemail.gr. Καλό θα ήταν το subject του email να είναι της μορφής magaz-rce-\... ή κάτι τέτοιο,
|
||
για να τα ξεχωρίζω εύκολα!
|
||
|
||
Καλό RCE!
|
||
|