Reverse Engineering σε περιβάλλον Linux, Μέρος 1 : Υλοποίηση των breakpoints
Description: This is issue 33 of Magaz, the Hellenic LinuxMagazine
Keywords: magaz hellenic greek hellas greece hellug linux magazine

Magaz, The Greek Linux Magazine
Τεύχος #33
Περιληπτικά Περιεχόμενα Αναλυτικά Περιεχόμενα Mirrors Links Πληροφορίες Συντάκτες Αναζήτηση Επικοινωνία
> Επόμενο Προηγούμενο Περιεχόμενα ### 4. Υλοποίηση των breakpoints Τα breakpoints, όπως είχαμε πει και στο προηγούμενο τεύχος, είναι σημεία στον κώδικα όπου διακόπτεται η εκτέλεση του προγράμματος και ο έλεγχος επιστρέφει στον debugger. Όσον αφορά στον τρόπο υλοποίησης τους μπορούν να χωριστούν σε δύο κατηγορίες: τα software και τα hardware. ### 4.1 Software Breakpoints Είναι το είδος που απαντάται πιο συχνά διότι δεν απαιτεί κάποια υποστήριξη από τον επεξεργαστή. Όταν ορίζουμε ένα software breakpoint σε κάποια διεύθυνση μνήμης τότε ο debugger, αφού αποθηκεύσει την εντολή που βρίσκεται σε εκείνο το σημείο την αντικαθιστά με μια εντολή trap (software interrupt). Ο debugger έχει φροντίσει να αποκτήσει τον έλεγχο του trap αυτού. Επομένως, όταν εκτελεστεί η trap ο έλεγχος επιστρέφει στον debugger, ο οποίος επανατοποθετεί τα bytes της αρχικής εντολής και περιμένει οδηγίες. Αφού προχωρήσουμε στο πρόγραμμα τοποθετείται πάλι η trap, αν πρόκειται για μόνιμο breakpoint. Στους x86 επεξεργαστές ως trap χρησιμοποιείται η "int 3" με opcode 0xCC. Ας δούμε ένα παράδειγμα:
>     0x8048429       :       0xff 0x75 0xf0              push   DWORD PTR [ebp-16]
>     0x804842c       :       0xff 0x75 0xf4              push   DWORD PTR [ebp-12]
>     0x804842f       :       0xe8 0x8c 0xfe 0xff 0xff    call   0x80482c0
>     0x8048434       :       0x83 0xc4 0x10              add  esp,0x10
> Έστω ότι τοποθετούμε ένα breakpoint στην εντολή call 0x80482c0. Αυτό που συμβαίνει είναι ότι ο debugger αντικαθιστά το πρώτο byte της εντολής με "0xcc" αφού βέβαια σώσει κάπου το 0xe8. Οπότε τώρα έχουμε στην πραγματικότητα:
>     0x804842f       :       0xcc                        int 3
>     0x8048430       :       0x8c 0xfe 0xff 0xff     ... (σκουπίδια)
>     0x8048434       :       0x83 0xc4 0x10                  add  esp,0x10
Τα bytes που απέμειναν από την call (0x8c - 0xff) ίσως να σχηματίζουν μια καινούργια εντολή αλλά πάντως όχι μια που είναι επιθυμητή! Μην περιμένετε την παραπάνω εικόνα της μνήμης να τη δείτε ποτέ μέσα από τον debugger με τον οποίο έχετε τοποθετήσει το breakpoint. Αυτό διότι ο ίδιος φροντίζει να μας εμφανίζει το "αυθεντικό" περιεχόμενο της μνήμης όταν του το ζητάμε. Πάντως, παρόλο που δε μπορείτε να τα δείτε εύκολα, τα software breakpoints όντως υπάρχουν! Το παρακάτω πρόγραμμα είναι αρκετό για να πείσει ακόμα και τους πιο "δύσκολους" από εσάς:
    #include <stdio.h>

    int main(void)
    {

        if (*((unsigned char *)main+6)==0xcc)
                printf("Software Breakpoint detected at main()!\n");
        else
                printf("No software breakpoint at main()!\n");

        return 0;
    }
>     bash$ gcc -o bp_test bp_test.c
>     bash$ ./bp_test
>     No software breakpoint at main()!
>     bash$ gdb bp_test
>     (gdb) r
>     Starting program: /home/alf/projects/magaz/issue1/bp_test
>     No software breakpoint at main()!

>     Program exited with code 01.
>     (gdb) break main
>     Breakpoint 1 at 0x804832e
>     (gdb) r
>     Starting program: /home/alf/projects/magaz/issue1/bp_test

>     Breakpoint 1, 0x0804832e in main ()
>     (gdb) c
>     Continuing.
>     Software Breakpoint detected at main()!

>     Program exited with code 01.
> Ο λόγος που ο έλεγχος γίνεται στο 7 byte της main() (main+6) και όχι στο πρώτο (main), είναι ότι ο GDB τοποθετεί τα breakpoints μετά από τον πρόλογο της συνάρτησης. ### 4.2 Hardware Breakpoints Τα hardware breakpoints βασίζονται στο υλικό και για αυτό ο ακριβής τρόπος υλοποίησης διαφέρει ανάλογα με τον επεξεργαστή. Στην αρχιτεκτονική x86 (386+) μέσα στον επεξεργαστή υπάρχουν 4 debug registers DR0-DR3. Σε κάθε έναν από αυτούς μπορεί να ανατεθεί ο έλεγχος μιας θέσης μνήμης, ώστε να προκαλεί ένα interrupt όταν η διεύθυνση μνήμης που τον αφορά διαβαστεί, γραφτεί, διαβαστεί ή γραφτεί ή εκτελεστεί ως μέρος της τρέχουσας εντολής (R, W, RW, X). Στα τρία πρώτα modes έχουμε λειτουργία watchpoint και πρόκειται για τον μόνο πρακτικό τρόπο υλοποίησης τους. Με software, το μόνο που θα μπορούσαμε να κάνουμε είναι να ελέγχουμε κάθε εντολή για προσπελάσεις στη μνήμη, διαδικασία εξαιρετικά χρονοβόρα. Εκμεταλλευόμενοι το hardware, κάθε προσπέλαση στη μνήμη συγκρίνεται σιωπηλά με τα περιεχόμενα των DR's και αν υπάρξει κάποιο ταίριασμα επεμβαίνει ο debugger. Οι έλεγχοι γίνονται παράλληλα με την υπόλοιπη λειτουργία του επεξεργαστή και για αυτό δεν υπάρχει καμία καθυστέρηση. Για να χρησιμοποιήσουμε hardware breakpoints στον GDB αντί για την εντολή break χρησιμοποιούμε την hbreak. H σύνταξη είναι ακριβώς όμοια με την break. ### 4.3 Hints and Tips - Home-made Traps Συχνά χρειάζεται να σταματήσουμε ένα κομμάτι κώδικα πριν εκτελεστεί αλλά δεν μπορούμε να το κάνουμε με τη break του GDB διότι πχ ο κώδικας βρίσκεται σε μια shared βιβλιοθήκη που δεν έχει φορτωθεί ακόμα. Ένας τρόπος να πετύχουμε το σκοπό μας είναι να τοποθετήσουμε μια εντολή trap (0xCC για x86) στο σημείο που θέλουμε, αφού γράψουμε κάπου πιο byte ήταν πριν εκεί. Αυτό βέβαια θα πρέπει να γίνει στο αρχείο με έναν hex editor. Όταν τρέξει ο επιθυμητός κώδικας θα δούμε κάτι σαν το παρακάτω:
>     Program received signal SIGTRAP, Trace/breakpoint trap.
>     0x0804835d in function ()
>     (gdb) print $eip$1 = (void *) 0x804835d
>     (gdb) set *(--(char *)($eip))=0x55
>     (gdb) r
>      ...
> Αυτό που κάνουμε είναι ουσιαστικά ότι ακριβώς κάνει ο debugger όταν βρει ένα breakpoint που έχουμε τοποθετήσει με την break.Η εντολή
>     set *(--(char *)($eip))=0x55
> Πιo καθαρά θα μπορούσε να γραφτεί:
>     (gdb) set $eip=$eip-1
>     (gdb) set *(char *)$eip=0x55 ή (gdb) set {char}$eip=0x55
> Χρειάζεται να μειώσουμε τον eip, διότι όταν προκληθεί το trap ο έλεγχος επιστρέφει στον GDB με τον eip να δείχνει στην αμέσως επόμενη εντολή. Επομένως, πηγαίνουμε μια θέση πίσω στη μνήμη και γράφουμε το αυθεντικό byte (εδώ το 0x55, push ebp). Προσοχή στο casting σε (char *). Είναι απαραίτητο γιατί αλλιώς ο GDB νομίζει (από default) πως θέλουμε να γράψουμε τον ακέραιο 0x55 (0x00000055) και έτσι θα γράψει 4 bytes αντί 1 που θέλουμε εμείς!
Επόμενο Προηγούμενο Περιεχόμενα