Reverse Engineering σε περιβάλλον Linux, Μέρος 1 : Χρήση του GDB για assembly debugging
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 Πληροφορίες Συντάκτες Αναζήτηση Επικοινωνία
Επόμενο Προηγούμενο Περιεχόμενα ### 2. Χρήση του GDB για assembly debugging Στο μέρος αυτό θα εξετάσουμε τις δυνατότητες του GDB για assembly debugging. Το case-study πρόγραμμα θα είναι το ίδιο με την προηγούμενη φορά:
    #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 Αυτή τη φορά η παράμετρος "-g" δεν είναι απαραίτητη αλλά και να την χρησιμοποιήσουμε δε μας ενοχλεί. Το φόρτωμα του προγράμματος στον GDB γίνεται ως συνήθως:
>     bash$ gdb rce1
>     (gdb)
> Αν δοκιμάσουμε να δούμε τον πηγαίο κώδικα του προγράμματος χωρίς να έχουμε κάνει compile με -g:
>     (gdb) list
>     1       init.c: No such file or directory.
>             in init.c
>     (gdb)
> O GBD δε βρίσκει το αρχείο "init.c". Ε, και τι έγινε θα πείτε; Το δικό μας αρχείο είναι το "rce1.c"! Το πρόβλημα είναι ότι το εκτελέσιμο δεν περιλαμβάνει καμία πληροφορία για το ποιο είναι το πηγαίο αρχείο του και ο GDB υποθέτει το όνομα "init.c". To "init.c" είναι το αρχείο πηγαίου κώδικα που αντιστοιχεί στην αρχικοποίηση της libc. Αν δημιουργήσουμε ένα αρχείο με το όνομα "init.c", τότε η list θα μας δείξει το περιεχόμενο του αρχείου αυτού. Αλλά και πάλι δεν μπορούμε να κάνουμε δουλειά, διότι ο debugger δεν γνωρίζει ποιες εντολές assembly αντιστοιχούν σε ποιες γραμμές C κώδικα. Αν πχ έχουμε αντιγράψει το "rce1.c" σε "init.c":
>     bash$ cp rce1.c init.c
>     bash$ gdb -q rce1
>     (gdb) break main
>     Breakpoint 1 at 0x8048392
>     (gdb) r
>     Starting program: /home/alf/projects/magaz/issue1/rce1
>     Breakpoint 1, 0x08048392 in main ()
>     (gdb) n
>     Single stepping until exit from function main,which has no line number
>     information.
>     Usage: /home/alf/projects/magaz/issue1/rce1 <number>

>     Program exited with code 01.
>     (gdb)
> Όταν πήγαμε να προχωρήσουμε μία γραμμή πηγαίου κώδικα με την n, ο GDB παραπονέθηκε πως δεν έχει τις απαραίτητες πληροφορίες και αποφάσισε να προχωρήσει μέχρι το τέλος της main(). Για πλάκα μπορούμε να συγχύσουμε τον GDB (και τους εαυτούς μας) αν κάνουμε compile με το -g flag και μετά αντικαταστήσουμε το source αρχείο μας με ένα άσχετο :) Αφού λοιπόν δεν έχουμε τον πηγαίο κώδικα αυτό ήταν... ας πάμε να παίξουμε τάβλι καλύτερα. Αλλά μια φωνή μέσα μας (τουλάχιστον μέσα σε εμένα!) αρνείται να παραδώσει τα όπλα. The gate is now open, welcome to the world of RCE! ### 2.1 Εξετάζοντας τον κώδικα assembly και άλλα παρεμφερή Αφού λοιπόν δεν αποθαρυνθήκαμε, ας εξετάσουμε το assembly listing της main. Αυτό γίνεται (κυρίως) με την εντολή disassemble <διεύθυνση> [<τελική διεύθυνση>]. Αν ορίσουμε μόνο μια παράμετρο, τότε εμφανίζεται ο κώδικας όλης της συνάρτησης στην οποία ανήκει η διεύθυνση. Το πρόβλημα είναι πως αν ο GDB δε γνωρίζει σε ποια συνάρτηση ανήκει η διεύθυνση (πχ όταν δεν υπάρχουν σύμβολα στο εκτελέσιμο), είτε θα παραπονεθεί και δε θα τυπώσει τίποτα είτε θα συγχυστεί με προηγούμενα σύμβολα και θα μας εκτυπώσει κατεβατά ολόκληρα. Για παράδειγμα:
>     (gdb) disas main
>     Dump of assembler code for function main:
>     0x804838c <main>:       push   %ebp
>     0x804838d <main+1>:     mov    %esp,%ebp
>     0x804838f <main+3>:     sub    $0x8,%esp
>     0x8048392 <main+6>:     and    $0xfffffff0,%esp
>     0x8048395 <main+9>:     mov    $0x0,%eax
>     0x804839a <main+14>:    sub    %eax,%esp
>     0x804839c <main+16>:    cmpl   $0x1,0x8(%ebp)
>     0x80483a0 <main+20>:    jg     0x80483c1 <main+53>
>     0x80483a2 <main+22>:    sub    $0x8,%esp
>     0x80483a5 <main+25>:    mov    0xc(%ebp),%eax
>     0x80483a8 <main+28>:    pushl  (%eax)
>     0x80483aa <main+30>:    push   $0x8048464
>     0x80483af <main+35>:    call   0x80482ac <printf>
>     0x80483b4 <main+40>:    add    $0x10,%esp
>     0x80483b7 <main+43>:    sub    $0xc,%esp
>     0x80483ba <main+46>:    push   $0x1
> Οι εντολές:
>     (gdb) disas main+44
>     (gdb) disas 0x80483f7
> και γενικά όσες διευθύνσεις περιέχονται στη main θα έχουν ως αποτέλεσμα να εκτυπωθεί όλη η main (ακριβώς όπως παραπάνω). Προσοχή πως το "main" είναι απλώς ένα σύμβολο που αντιστοιχεί σε κάποια διεύθυνση, εδώ την 0x804838c. Η έκφραση main+44 είναι και αυτή μια διεύθυνση (0x80483b8). Δεν έχει σημασία που δεν αποτελεί την αρχή κάποιας εντολής ( είναι το δεύτερο byte της sub $0xc,%esp), αρκεί που ανήκει μέσα στη συνάρτηση main. Η ερώτηση που τίθεται είναι η εξής: που ξέρει ο GDB που αρχίζει και τελειώνει μια συνάρτηση; Και η απάντηση: ο GDB ξέρει μόνο που αρχίζει η συνάρτηση και υποθέτει ότι συνεχίζει η ίδια συνάρτηση μέχρι να βρει κάποιο άλλο σύμβολο που είναι σύμβολο συνάρτησης. Αν τα σύμβολα στο εκτελέσιμο κατά σειρά αύξουσας διεύθυνσης είναι:
>     0804838c  main
>     08048406  alf
>     0804841c  __do_global_ctors_aux
> O GDB θεωρεί πως ό,τι βρίσκεται μεταξύ των διευθύνσεων main και alf ανήκει στη συνάρτηση main, ό,τι βρίσκεται μεταξύ των alf και __do_global_ctors_aux ανήκει στη συνάρτηση alf κτλ. Το γεγονός πως τα όρια της κάθε συνάρτησης (για την ακρίβεια το τέλος) δεν είναι γνωστά, προκαλεί το πρόβλημα που αναφέρθηκε παραπάνω (ο GDB δε μπορεί να βρει σε ποια συνάρτηση ανήκει η διεύθυνση ή κάνει λάθος). Ας δούμε το πρόβλημα στην πράξη:
>     bash$ strip -s rce1
>     bash$ gdb rce1
>     (no debugging symbols found)...
>     (gdb)
> H εντολή strip "απογυμνώνει" ένα object αρχειο από όλα τα σύμβολα που μπορεί. Γράφω "μπορεί", διότι υπάρχουν μερικά που δε έχει νόημα να αφαιρέσει, όπως για παράδειγμα αυτά που αναφέρονται σε εξωτερικές συναρτήσεις και δεδομένα. Ο λόγος είναι ότι στο στάδιο του linking (είτε αυτό είναι dynamic είτε όχι) δε θα μπορέσει να βρει τις διευθύνσεις τους αν δε γνωρίζει το όνομα τους!
>     (gdb) disas main
>     No symbol table is loaded.  Use the "file" command.
> Το σύμβολο main δε βρέθηκε αλλά εμείς ξέρουμε τη διεύθυνση του.
>     (gdb) disas 0x804838c
>     Dump of assembler code for function atoi:
>     0x80482cc <atoi>:       jmp    *0x804958c
>     0x80482d2 <atoi+6>:     push   $0x18
>     0x80482d7 <atoi+11>:    jmp    0x804828c
>     0x80482dc <atoi+16>:    xor    %ebp,%ebp
>     0x80482de <atoi+18>:    pop    %esi
> Και ιδού... Ο GDB τα "πήρε" :) Το μόνο σύμβολο που υπάρχει αμέσως πριν τη διεύθυνση της main είναι το atoi οπότε ο debugger θεωρεί πως η διεύθυνση 0x804838c ανήκει στη συνάρτηση atoi(). Το σύμβολο atoi δείχνει σε μια εξωτερική συνάρτηση για αυτό και δεν αφαιρέθηκε. Σε αυτές τις περιπτώσεις είναι χρήσιμη η εναλλακτική μορφή της disassemble στην οποία ορίζουμε τόσο την αρχική όσο και την τελική διεύθυνση για το disassembly :
>     (gdb) disas 0x804838c 0x80483a0
>     Dump of assembler code from 0x804838c to 0x80483a0:
>     0x804838c <atoi+192>:   push   %ebp
>     0x804838d <atoi+193>:   mov    %esp,%ebp
>     0x804838f <atoi+195>:   sub    $0x8,%esp
>     0x8048392 <atoi+198>:   and    $0xfffffff0,%esp
>     0x8048395 <atoi+201>:   mov    $0x0,%eax
>     0x804839a <atoi+206>:   sub    %eax,%esp
>     0x804839c <atoi+208>:   cmpl   $0x1,0x8(%ebp)
>     End of assembler dump.
> Μπορεί ο GDB να πιστεύει πως βρισκόμαστε 192 bytes από την αρχή της atoi αλλά εμείς ξέρουμε πως ουσιαστικά είμαστε στην αρχή της main! Κλείνοντας αυτό το κομμάτι θα ασχοληθούμε λίγο με τη μορφή του listing. Όσοι έχετε ασχοληθεί με assembly στον x86 η σύνταξη των προηγούμενων listing ίσως σας φανεί λίγο παράξενη. Αυτή ονομάζεται AT&T syntax και ένα βασικό χαρακτηριστικό της είναι ότι στις εντολές της έχει ανάποδα την πηγή και τον προορισμό, σε σχέση με την άλλη μορφή την Intel syntax. Πχ για να μετακινήσουμε το περιεχόμενο του καταχωρητή ebx στον eax :
>     mov %ebx, %eax  AT&T
>     mov eax, ebx    Intel
> Βέβαια υπάρχουν και άλλες διαφορές αλλά δε θα μας απασχολήσουν εδώ. Επίσης υπάρχουν και παραλλαγές των παραπάνω όπως η σύνταξη που χρησιμοποιεί ο Nasm (Netwide Assembler) η οποία βασίζεται στην Intel αλλά κατά τη γνώμη είναι πιο ξεκάθαρη Παρακάτω θα χρησιμοποιήσουμε τη σύνταξη της Intel διότι είναι γενικά πιο διαδεδομένη για τους επεξεργαστές της. Στον GDB η σύνταξη ορίζεται στην εσωτερική μεταβλητή disassembly-flavor:
>     (gdb) set disassembly-flavor intel
>     (gdb) disas main
>     Dump of assembler code for function main:
>     0x804838c <main>:       push   ebp
>     0x804838d <main+1>:     mov    ebp,esp
>     0x804838f <main+3>:     sub    esp,0x8
>     0x8048392 <main+6>:     and    esp,0xfffffff0
>     0x8048395 <main+9>:     mov    eax,0x0
>     0x804839a <main+14>:    sub    esp,eax
>     0x804839c <main+16>:    cmp    DWORD PTR [ebp+8],0x1
>     0x80483a0 <main+20>:    jg     0x80483c1 <main+53>
>     0x80483a2 <main+22>:    sub    esp,0x8
>     0x80483a5 <main+25>:    mov    eax,DWORD PTR [ebp+12]
>     0x80483a8 <main+28>:    push   DWORD PTR [eax]
>     0x80483aa <main+30>:    push   0x8048464
>     0x80483af <main+35>:    call   0x80482ac <printf>
>     0x80483b4 <main+40>:    add    esp,0x10
>     0x80483b7 <main+43>:    sub    esp,0xc
>     0x80483ba <main+46>:    push   0x1
>     0x80483bc <main+48>:    call   0x80482bc <exit>
>     0x80483c1 <main+53>:    sub    esp,0xc
>     0x80483c4 <main+56>:    mov    eax,DWORD PTR [ebp+12]
>     0x80483c7 <main+59>:    add    eax,0x4
>     0x80483ca <main+62>:    push   DWORD PTR [eax]
>     0x80483cc <main+64>:    call   0x8048406 <alf>
>     0x80483d1 <main+69>:    add    esp,0x10
>     0x80483d4 <main+72>:    mov    DWORD PTR [ebp-4],eax
>     0x80483d7 <main+75>:    cmp    DWORD PTR [ebp-4],0xa
>     0x80483db <main+79>:    jle    0x80483ef <main+99>
>     0x80483dd <main+81>:    sub    esp,0xc
>     0x80483e0 <main+84>:    push   0x8048478
>     0x80483e5 <main+89>:    call   0x80482ac <printf>
>     0x80483ea <main+94>:    add    esp,0x10
>     0x80483ed <main+97>:    jmp    0x80483ff <main+115>
>     0x80483ef <main+99>:    sub    esp,0xc
>     0x80483f2 <main+102>:   push   0x804847d
>     0x80483f7 <main+107>:   call   0x80482ac <printf>
>     0x80483fc <main+112>:   add    esp,0x10
>     0x80483ff <main+115>:   mov    eax,0x1
>     0x8048404 <main+120>:   leave
>     0x8048405 <main+121>:   ret
>     End of assembler dump.
>     (gdb)
> ### 2.2 Εξετάζοντας τα δεδομένα Τα δεδομένα που μας ενδιαφέρουν όταν ασχολούμαστε με low-level debugging μπορούν να βρίσκονται είτε σε κάποιον καταχωρητή είτε στη μνήμη. #### Εξετάζοντας καταχωρητές Ο βασικός τρόπος για να δούμε τα περιεχόμενα των καταχωρητών είναι με την info registers/ i r [reg]. Χωρίς όρισμα εκτυπώνει όλους τους ακέραιους καταχωρητές με τα περιεχόμενα τους σε δεκαεξαδική και δεκαδική μορφή, αλλιώς τυπώνει μόνο αυτόν που ορίσαμε.
>     (gdb) i r
>     eax            0x0      0
>     ecx            0x4      4
>     edx            0x4014f1ec       1075114476
>     ebx            0x40153234       1075130932
>     esp            0xbffff730       0xbffff730
>     ebp            0xbffff738       0xbffff738
>     esi            0x40014020       1073823776
>     edi            0xbffff794       -1073743980
>     eip            0x804839c        0x804839c
>     eflags         0x386    902
>     cs             0x23     35
>     ss             0x2b     43
>     ds             0x2b     43
>     es             0x2b     43
>     fs             0x0      0
>     gs             0x0      0
>     fctrl          0x37f    895
>     fstat          0x0      0
>     ftag           0xffff   65535
>     fiseg          0x0      0
>     fioff          0x0      0
>     foseg          0x0      0
>     fooff          0x0      0
>     fop            0x0      0
>     mxcsr          0x1f80   8064
>     orig_eax       0xffffffff       -1
>     (gdb) i r edx
>     edx            0x4014f1ec       1075114476
>     (gdb)
> Υπάρχει και η εντολή info all-registers η οποία τυπώνει όλους τους καταχωρητές ( integer και floating-point και ΜΜΧ και ΧΜΜ για x86). To πρόβλημα με την εντολή i r είναι πως μας επιτρέπει μόνο να δούμε τις τιμές των καταχωρητών, ενώ αρκετά συχνά θέλουμε να τις αλλάξουμε ή να τις χρησιμοποιήσουμε σε κάποια έκφραση. Στον GDB για κάθε καταχωρήτη υπάρχει μια ψευδο-μεταβλητή της οποία το όνομα αποτελείται από το '$' και το όνομα του καταχωρήτη πχ $eax. Ο μηχανισμός αυτός προσφέρει μεγάλη ευελιξία και ευκολία:
>     (gdb) print $eip
>     $1 = (void *) 0x804839c
>     (gdb) set $eip=$eip+1
>     (gdb) print $eip
>     $2 = (void *) 0x804839d
>     (gdb) set $eip--
>     (gdb) print $eip
>     $3 = (void *) 0x804839c
> #### Εξετάζοντας τη μνήμη Η προσπέλαση στη μνήμη γίνεται με την εντολή x. Η πλήρης σύνταξη είναι x/FMT όπου FMT είναι μια ακολουθία τριών στοιχείων <repeat count><size><format>. To πρώτο ορίζει πόσα αντικείμενα να εκτυπωθούν, το δεύτερο δηλώνει τι μέγεθος θα έχει το κάθε αντικείμενο ( b(byte 1), h(halfword 2) , w(word 4) ,g(giant 8) και τέλος το format σε τι μορφή να εκτυπωθούν ( o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char) και s(string). ) Πχ Εκτύπωσε 5 bytes σε οκταδική μορφή αρχίζοντας από τη διεύθυνση main
>     (gdb) x/5bo main
>     0x804838c <main>:       0125    0211    0345    0203    0354
> Το ίδιο σε hex
>     (gdb) x/5bx main
>     0x804838c <main>:       0x55    0x89    0xe5    0x83    0xec
> Εκτύπωσε 3 λέξεις (4 bytes η κάθε μία) σε δεκαεξαδική μορφή αρχίζοντας από τη διεύθυνση main
>     (gdb) x/3wx main
>     0x804838c <main>:       0x83e58955      0xe48308ec      0x0000b8f0
> Εκτύπωσε τις πρώτες δέκα εντολές της main( όχι τις γνωστές δέκα...)
>     (gdb) x/10i main
>     0x804838c <main>:       push   %ebp
>     0x804838d <main+1>:     mov    %esp,%ebp
>     0x804838f <main+3>:     sub    $0x8,%esp
>     0x8048392 <main+6>:     and    $0xfffffff0,%esp
>     0x8048395 <main+9>:     mov    $0x0,%eax
>     0x804839a <main+14>:    sub    %eax,%esp
>     0x804839c <main+16>:    cmpl   $0x1,0x8(%ebp)
>     0x80483a0 <main+20>:    jg     0x80483c1 <main+53>
>     0x80483a2 <main+22>:    sub    $0x8,%esp
>     0x80483a5 <main+25>:    mov    0xc(%ebp),%eax
> Η τελευταία εντολή έχει το ίδιο αποτέλεσμα με την εντολή disassemble main main+25. Συχνά χρειάζεται να εξετάζουμε συνέχεια μια θέση μνήμης και καταντάει κουραστικό να γράφουμε την εντολή x/FMT . Σε τέτοιες περιπτώσεις βολεύει η εντολή display που συντάσεται ακριβώς όπως η x/FMT και μαζί με κάθε gdb prompt μας εμφανίζει τα δεδομένα που της ζητήσαμε. Ένα πολύ χρήσιμο παράδειγμα της display είναι το παρακάτω:
>     (gdb) display/5i $eip
>     (gdb) break main
>     Breakpoint 1 at 0x8048392
>     (gdb) r
>     Starting program: /home/alf/projects/magaz/issue1/rce1

>     Breakpoint 1, 0x08048392 in main ()
>     2: x/5i $eip
>     0x8048392 <main+6>:     and    $0xfffffff0,%esp
>     0x8048395 <main+9>:     mov    $0x0,%eax
>     0x804839a <main+14>:    sub    %eax,%esp
>     0x804839c <main+16>:    cmpl   $0x1,0x8(%ebp)
>     0x80483a0 <main+20>:    jg     0x80483c1 <main+53>
>     (gdb) ni
>     0x08048395 in main ()
>     2: x/5i $eip
>     0x8048395 <main+9>:     mov    $0x0,%eax
>     0x804839a <main+14>:    sub    %eax,%esp
>     0x804839c <main+16>:    cmpl   $0x1,0x8(%ebp)
>     0x80483a0 <main+20>:    jg     0x80483c1 <main+53>
>     0x80483a2 <main+22>:    sub    $0x8,%esp
> Μετά από κάθε ni (next instruction, δείτε λίγο παρακάτω) εμφανίζονται αυτόματα οι 5 επόμενες εντολές assembly. Για να εξετάσουμε ποια auto-display έχουμε στο σύστημα μας γράφουμε info display ενώ για να ακυρώσουμε μια εντολή display χρησιμοποιούμε την undisplay [#num] ή delete display [#n]. ### 2.3 Εκτελώντας τον κώδικα Είναι ωραίο να βλέπουμε τον κώδικα του προγράμματος μας και τα δεδομένα του αλλά είναι καλύτερο να μπορούμε και να το εκτελούμε! Για το σκοπό αυτό οι εντολές που χρειαζόμαστε είναι οι ni/nexti (next instruction) και si/stepi. Αυτές λειτουργούν αντίστοιχα με τις next και step αλλά σε επίπεδο εντολών assembly και όχι σε επίπεδο γραμμών πηγαίου κώδικα. Οι nexti και stepi εκτελούν την επόμενη εντολή assembly αλλά η nexti δεν ακολουθεί τις κλήσεις συναρτήσεων.
>     (gdb) display/1i $eip
>     (gdb) ni
>     0x080483ca in main ()
>     2: x/i $eip
>     0x80483ca <main+62>:       push   DWORD PTR [eax]
>     (gdb) ni
>     0x080483cc in main ()
>     2: x/i $eip
>     0x80483cc <main+64>:       call   0x8048406 <alf>
>     (gdb) ni
>     0x080483d1 in main ()
>     2: x/i $eip
>     0x80483d1 <main+69>:       add    esp,0x10
>     (gdb) ni
>     0x080483d4 in main ()
>     2: x/i $eip
>     0x80483d4 <main+72>:       mov    DWORD PTR [ebp-4],eax
>     (gdb) ni
>     0x080483ca in main ()
>     2: x/i $eip
>     0x80483ca <main+62>:       push   DWORD PTR [eax]
>     (gdb) ni
>     0x080483cc in main ()
>     2: x/i $eip
>     0x80483cc <main+64>:       call   0x8048406 <alf>
>     (gdb) si
>     0x08048406 in alf ()
>     2: x/i $eip
>     0x8048406 <alf>:           push   ebp
>     (gdb) ni
>     0x08048407 in alf ()
>     2: x/i $eip
>     0x8048407 <alf+1>:         mov    ebp,esp
>     (gdb) ni
>     0x08048409 in alf ()
>     2: x/i $eip
>     0x8048409 <alf+3>:         sub    esp,0x8
>     (gdb) ni
>     0x0804840c in alf ()
>     2: x/i $eip
>     0x804840c <alf+6>:         sub    esp,0xc
> Breakpoints-Watchpoints Για τα breakpoints και τα watchpoints ισχύουν όσα είχαν γραφτεί στο προηγούμενο τεύχος. Απλώς λόγω έλλειψης συμβόλων χρησιμοποιείται αρκετά η μορφή που περιέχει απλή διεύθυνση πχ break *0x8045333. ### 2.4 Πληροφορίες για το εκτελέσιμο Η εντολή info files τυπώνει πληροφορίες για το εκτελέσιμο:
>     (gdb) info files
>     Symbols from "/home/alf/projects/magaz/issue1/rce1".
>     Local exec file:
>             `/home/alf/projects/magaz/issue1/rce1', file type elf32-i386.
>             Entry point: 0x8048330
>             0x080480f4 - 0x08048107 is .interp
>             0x08048108 - 0x08048128 is .note.ABI-tag
>             0x08048128 - 0x08048160 is .hash
>             0x08048160 - 0x080481f0 is .dynsym
>             0x080481f0 - 0x0804824e is .dynstr
>             0x0804824e - 0x08048260 is .gnu.version
>             0x08048260 - 0x08048280 is .gnu.version_r
>             0x08048280 - 0x08048290 is .rel.dyn
>             0x08048290 - 0x080482b8 is .rel.plt
>             0x080482b8 - 0x080482cf is .init
>             0x080482d0 - 0x08048330 is .plt
>             0x08048330 - 0x080484ac is .text
>             0x080484ac - 0x080484c7 is .fini
>             0x080484c8 - 0x080484f6 is .rodata
>             0x080494f8 - 0x08049504 is .data
>             0x08049504 - 0x08049508 is .eh_frame
>             0x08049508 - 0x080495d0 is .dynamic
>             0x080495d0 - 0x080495d8 is .ctors
>             0x080495d8 - 0x080495e0 is .dtors
>             0x080495e0 - 0x080495e4 is .jcr
>             0x080495e4 - 0x08049608 is .got
>             0x08049608 - 0x08049610 is .bss
>     (gdb)
> Καταρχάς δίνεται το path και το είδος του εκτελέσιμου. Αμέσως μετά δίνεται το entry point, δηλαδή η διεύθυνση της πρώτης εντολής που θα εκτελεστεί όταν αρχίσει το πρόγραμμα. Προσοχή: αυτή συνήθως δεν είναι η διεύθυνση της main αλλά είναι η αρχή του κώδικα που αρχικοποιεί την libc! Όλα τα υπόλοιπα είναι τα sections του ELF και οι διεύθυνσεις μνήμης που καταλαμβάνουν. ### 2.5 Καλώντας συναρτήσεις και ψάχνοντας για σύμβολα Εδώ θα αναφερθούμε σε δύο αρκετά χρήσιμες εντολές που μάλλον θα ταίριαζαν περισσότερο στο προηγούμενο άρθρο. Η πρώτη εντολή είναι η call η οποία χρησιμοποιείται για να καλέσουμε μια οποιαδήποτε συνάρτηση. Η σύνταξη που χρησιμοποιεί εξαρτάται από την τρέχουσα γλώσσα και στη C είναι γνωστή η call func(arg1,arg2,...).
>     bash$ gdb rce1
>     (gdb) call alf("123")
>     evaluation of this expression requires the target program to be active
>     (gdb) break main
>     Breakpoint 1 at 0x80483f0: file rce1.c, line 7.
>     (gdb) r
>     Starting program: /home/alf/magaz/issue1/rce1

>     Breakpoint 1, main (argc=1, argv=0xbffff7a4) at rce1.c:7
>     7           if (argc<2) {
>     (gdb) call alf("123")
>     $1 = 123
>     (gdb) call alf("747")
>     $2 = 747
>     (gdb) print $1
>     $3 = 123
>     (gdb) print $+1
>     $4 = 124
> Η τιμή που επιστρέφει η συνάρτηση αποθηκεύεται στη μεταβλητή που μας δείχνει ο GDB πχ $1. Η μεταβλητή $ περιέχει την τελευταία τιμή που παράχθηκε. Η επόμενη εντολή είναι η info functions [name]. Η εντολή αυτή εμφανίζει όλες τις συναρτήσεις που ξέρει ο GDB μαζί με τη διεύθυνση τους (αμέτρητες!). Αν καθορίσουμε κάποιο όνομα θα εμφανιστούν μόνο οι συναρτήσεις που περιέχουν αυτό το όνομα. Αν και γενικά τα πράγματα είναι απλά, υπάρχουν μερικά σκοτεινά σημεία που ίσως σας προβληματίσουν. Για παράδειγμα έστω ότι έχουμε ένα πρόγραμμα που καλεί την fprintf(). Αφου το φορτώσουμε στο GDB έχουμε:
>     (gdb) info functions fprintf
>     All functions matching regular expression "fprintf":

>     Non-debugging symbols:
>     0x080482e0  fprintf
>     (gdb) break main
>     Breakpoint 1 at 0x80483f0: file rce1.c, line 7.
>     (gdb) r
>     Starting program: /home/alf/tst

>     Breakpoint 1, main (argc=1, argv=0xbffff7a4) at rce1.c:7
>     7           if (argc<2) {
>     (gdb) info functions fprintf
>     All functions matching regular expression "fprintf":

>     Non-debugging symbols:
>     0x080482e0  fprintf
>     0x4006e5c0  _IO_vfprintf
>     0x4006e5c0  _IO_vfprintf_internal
>     0x4006e5c0  __GI_vfprintf
>     0x4006e5c0  vfprintf
>     0x40072b40  buffered_vfprintf
>     0x400788f0  _IO_fprintf
>     0x400788f0  __GI_fprintf
>     0x400788f0  fprintf
>     0x4007c8d0  buffered_vfprintf
> Αρχικά (πριν εκτελέσουμε το πρόγραμμα) το μόνο σύμβολο fprintf που υπήρχε ήταν στη διεύθυνση 0x080483e0. Η διεύθυνση αυτή ανήκει στο δικό μας πρόγραμμα και επομένως δεν μπορεί να είναι η αρχή της fprintf()! Στην πραγματικότητα είναι ένα jmp προς την κανονική fprintf(). Αφού τρέξουμε το πρόγραμμα, ο GDB συνειδητοποιεί πως φορτώθηκαν βιβλιοθήκες και προσθέτει τα σύμβολα των συναρτήσεων στη λίστα του. Μια από αυτές που φορτώθηκε ήταν η libc έτσι προστέθηκε μεταξύ άλλων το σύμβολο fprintf στη διεύθυνση 0x400788f0 που αποτελεί και τη πραγματική αρχή της συνάρτησης.
>     bash$ gdb tst
>     (gdb) break fprintf
>     Breakpoint 1 at 0x80482e0
>     (gdb) r
>     Starting program: /home/alf/tst
>     Breakpoint 1 at 0x400788f6

>     Breakpoint 1, 0x400788f6 in fprintf () from /lib/libc.so.6
>     (gdb)
Παρατηρήστε ότι ενώ το αρχικό breakpoint ήταν στη διεύθυνση 0x80482e0 το break έγινε στη διεύθυνση 0x400788f6. Ο GDB αναγνωρίζει ότι το σύμβολο fprintf στη διεύθυνση 0x80482e0 έχει ειδική σημασία, και όταν φορτωθεί η πραγματική fprintf() ανανεώνει το breakpoint. Περισσότερα για το θέμα στο επόμενο τεύχος όταν εξετάσουμε το ELF. Επόμενο Προηγούμενο Περιεχόμενα