Magaz, The Greek Linux Magazine
Magaz Logo

Επόμενο  Προηγούμενο  Περιεχόμενα

4. GDB - Ο παρεξηγημένος debugger

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 - Τα βασικά

Βασική δυνατότητα ενός 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 και πιο συγκεκριμένα για το πως αυτά υλοποιούνται σε χαμηλό επίπεδο.

Επόμενο  Προηγούμενο  Περιεχόμενα


Valid HTML 4.01!   Valid CSS!