683 γραμμές
38 KiB
Markdown
683 γραμμές
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!
|
|||
|
|