💾 Archived View for magaz.hellug.gr › 33 › 05_rce2 › index.gmi captured on 2024-08-18 at 17:37:18. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2024-02-05)
-=-=-=-=-=-=-
Φραντζής Αλέξανδρος (aka Alf) alf82 at freemail dot gr Ιουν 2003
1. Εισαγωγή
2. Χρήση του GDB για assembly debugging
3. Άλλα χρήσιμα εργαλεία
4. Υλοποίηση των breakpoints
5. Χρήσιμες έως πολύ χρήσιμες πληροφορίες
6. Hands-on Παράδειγμα
7. Πρόκληση
Το πρώτο μέρος του άρθρου (2-3) έχει βασικό σκοπό την παρουσίαση των πιο κοινών εργαλείων που υπάρχουν στο linux και που μπορούν να ενισχύσουν την προσπάθεια κατανόησης της λειτουργίας ενός εκτελέσιμου. Βέβαια, αυτά τα εργαλεία δεν είναι τα μόνα που υπάρχουν και μια αναζήτηση στο διαδίκτυο θα εμφανίσει πολλούς θησαυρούς. Το πρόβλημα με τα περισσότερα από τα προγράμματα που ίσως βρείτε, είναι ότι πρόκειται περί "diamonds in the rough". Θα σας ταλαιπωρήσουν μέχρι να τα στήσετε σωστά και μετά ποιος ξέρει τι άλλα προβλήματα θα εμφανιστούν.
Στο δεύτερο μέρος (4-6) θα ασχοληθούμε με πιο γενικές πληροφορίες σε σχέση με το RCE. Προσοχή: δεν πρόκειται για περιττές πληροφορίες αλλά για βασικές γνώσεις, χωρίς τις οποίες θα δυσκολευτείτε να κατανοήσετε τι πραγματικά συμβαίνει.
Για την καλύτερη κατανόηση του άρθρου είναι επιθυμητή μια στοιχειώδης, το λιγότερο, γνώση της γλώσσας assembly. Θα προσπαθήσω να εξηγώ όπου χρειάζεται, όμως σίγουρα δεν πρόκειται να μετατρέψω το κείμενο σε assembly tutorial. Πολλοί ίσως να θεωρούν την assembly παρωχημένη γλώσσα άλλα να είστε σίγουροι πως RCE χωρίς assembly δε νοείται. Μια αναζήτηση για "x86 assembly tutorial" στο google θα σας δώσει πληροφορίες που θα σας απασχολήσουν για πολύ καιρό.
Τέλος όπως κάθε φορά υπάρχει η πρόκληση του μήνα. Στόχος είναι να σας επιτρέψει να εξασκήσετε τις ικανότητές σας, να γνωρίσετε τα όρια σας και να χαρείτε από πρώτο χέρι τη διαδικασία του RCE!
Επίσης οτιδήποτε σχόλιο δεκτό: θεωρείτε τις προκλήσεις πολύ εύκολες/δύσκολες, θα θέλατε να καλυφθεί κάποιο θέμα ή να καλυφθεί εκτενέστερα;
Στο επόμενο τεύχος θα συνεχίσουμε με τη δομή των ELF, το objdump, το /proc filesystem, περισσότερα hands-on παραδείγματα και ποιος ξέρει τι άλλο :)
Καλό RCE και κυρίως καλό καλοκαίρι!
Στο μέρος αυτό θα εξετάσουμε τις δυνατότητες του 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!
Αφού λοιπόν δεν αποθαρυνθήκαμε, ας εξετάσουμε το 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_asm} Η ερώτηση που τίθεται είναι η εξής: που ξέρει ο 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)
Τα δεδομένα που μας ενδιαφέρουν όταν ασχολούμαστε με 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 για κάθε καταχωρήτη υπάρχει μια ψευδο-μεταβλητή της οποία το όνομα αποτελείται από το '