magaz/content/articles/33/05_rce2.md
2022-03-23 20:14:33 +02:00

1280 γραμμές
90 KiB
Markdown

Αυτό το αρχείο περιέχει ασαφείς χαρακτήρες Unicode

Αυτό το αρχείο περιέχει χαρακτήρες Unicode που μπορεί να συγχέονται με άλλους χαρακτήρες. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.

+++
title = 'Reverse Engineering σε περιβάλλον Linux, Μέρος 1'
date = '2003-06-01T00:00:00Z'
description = ''
author = 'Φραντζής Αλέξανδρος (aka Alf) alf82 at freemail dot gr'
issue = ['Magaz 33']
issue_weight = 5
+++
**1. Εισαγωγή**
--------------------------------------
**2. Χρήση του GDB για assembly debugging**
------------------------------------------------------------------
- [2.1 Εξετάζοντας τον κώδικα assembly και άλλα παρεμφερή](#ss2.1)
- [2.2 Εξετάζοντας τα δεδομένα](#ss2.2)
- [2.3 Εκτελώντας τον κώδικα](#ss2.3)
- [2.4 Πληροφορίες για το εκτελέσιμο](#ss2.4)
- [2.5 Καλώντας συναρτήσεις και ψάχνοντας για σύμβολα](#ss2.5)
**3. Άλλα χρήσιμα εργαλεία**
---------------------------------------------------
- [3.1 strace](#ss3.1)
- [3.2 ltrace](#ss3.2)
**4. Υλοποίηση των breakpoints**
-------------------------------------------------------
- [4.1 Software Breakpoints](#ss4.1)
- [4.2 Hardware Breakpoints](#ss4.2)
- [4.3 Hints and Tips - Home-made Traps](#ss4.3)
**5. Χρήσιμες έως **πολύ** χρήσιμες πληροφορίες**
------------------------------------------------------------------------
- [5.1 The C calling convention](#ss5.1)
- [5.2 System Calls](#ss5.2)
**6. Hands-on Παράδειγμα**
-------------------------------------------------
**7. Πρόκληση**
--------------------------------------
- [7.1 Προηγούμενη πρόκληση - Λύση και Hall Of Fame](#ss7.1)
- [7.2 Πρόκληση \#1](#ss7.2)
### [1. Εισαγωγή]{#s1}
Το πρώτο μέρος του άρθρου (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 και κυρίως **καλό καλοκαίρι**!
### [2. Χρήση του GDB για assembly debugging]{#s2}
Στο μέρος αυτό θα εξετάσουμε τις δυνατότητες του 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 και άλλα παρεμφερή]{#ss2.1}
Αφού λοιπόν δεν αποθαρυνθήκαμε, ας εξετάσουμε το 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)
### [2.2 Εξετάζοντας τα δεδομένα]{#ss2.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 Εκτελώντας τον κώδικα]{#ss2.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 Πληροφορίες για το εκτελέσιμο]{#ss2.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 Καλώντας συναρτήσεις και ψάχνοντας για σύμβολα]{#ss2.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.
### [3. Άλλα χρήσιμα εργαλεία]{#s3}
Παρακάτω παρουσιάζονται εν συντομία δύο πολύ χρήσιμα εργαλεία που υπάρχουν σε κάθε σύγχρονο linux σύστημα. Βασίζονται και τα δύο στο ptrace() system call που
επιτρέπει τον έλεγχο διεργασιών στο linux.
### [3.1 strace]{#ss3.1}
To strace (system call trace) καταγράφει τα system calls ενός προγράμματος. Από default καταγράφει όλα τα syscalls αλλά υπάρχει η δυνατότητα να προσδιοριστούν
μόνο κάποια συγκεκριμένα. Δέχεται πολλές παραμέτρους και ρυθμίσεις αλλά εδώ θα ασχοληθούμε μόνο με τις πιο βασικές (εξάλλου δεν υπάρχει λόγος να επαναλαμβάνουμε
τη manpage!)
Η βασική σύνταξη είναι **strace \[options\] \[-o outputfile\] \[objfile \[args\]\]**.\
Αν δεν καθορίσουμε κάποιο αρχείο για output τα syscalls εμφανίζονται στo stderr.
Χρήσιμες παράμετροι είναι:\
**-e call1,call2,\...** ή **-e trace=call1,call2,\...,callΝ**\
Καθορίζει ποια syscalls να παρακολουθήσει το πρόγραμμα. Αν πριν από κάποιο όνομα υπάρχει το \'!\' σημαίνει να μην παρακολουθηθεί το syscall αυτό. H default τιμή
είναι -e trace=all.\
**-p pid**\
καθορίζει σε ποια διεργασία να \"αγκιστρωθεί\" το πρόγραμμα ώστε να αρχίσει να παρακολουθεί. Προφανώς στην περίπτωση αυτή είναι περιττό να καθοριστεί το
objfile.\
**-i**\
Πριν από κάθε syscall εμφανίζεται η τιμή του IP (instruction pointer) τη στιγμή της κλήσης. Η επιλογή αυτή είναι λιγότερο χρήσιμη από ότι φαίνεται, διότι τα
syscalls καλούνται μέσα από wrapper συναρτήσεις που βρίσκονται σε βιβλιοθήκες, και έτσι οι διευθύνσεις που θα λαμβάνουμε δεν θα είναι και πολύ χρήσιμες.
πχ
> bash$ strace -e trace=write rce1
> write(1, "Usage: rce1 <number>\n", 21Usage: rce1 <number>
> ) = 21
Το παραπάνω είναι λίγο μπερδεμένο διότι το strace αρχίζει να καταγράφει το syscall write στο stderr(που από default είναι η οθόνη), ύστερα εκτυπώνεται το
κείμενο μας και μετά τελειώνει η καταγραφή με την τιμή επιστροφής της write. Αν είχαμε χρησιμοποιήσει το option -ο δε θα υπήρχε πρόβλημα.
> bash$ strace -o rce1.trace -e trace=write rce1
> Usage: rce1 <number>
> bash$ cat rce1.trace
> write(1, "Usage: rce1 <number>\n", 21) = 21
Ας δούμε τώρα το πλήρες trace από το αγαπημένο μας rce1.
> bash$ strace -o rce1.trace rce1
> Usage: rce1 <number>
> bash$ cat -n rce1.trace
> 1 execve("./rce1", ["rce1"], [/* 51 vars */]) = 0
> 2 brk(0) = 0x8049598
> 3 open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or
> directory)
> 4 open("/etc/ld.so.cache", O_RDONLY) = 3
> 5 fstat64(3, {st_mode=S_IFREG|0644, st_size=64466, ...}) = 0
> 6 old_mmap(NULL, 64466, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40015000
> 7 close(3) = 0
> 8 open("/lib/libc.so.6", O_RDONLY) = 3
> 9 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\\\1\000"...,
> 1024) = 1024
> 10 fstat64(3, {st_mode=S_IFREG|0755, st_size=1435624, ...}) = 0
> 11 old_mmap(NULL, 1256740, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) =
> 0x40025000
> 12 mprotect(0x4014f000, 36132, PROT_NONE) = 0
> 13 old_mmap(0x4014f000, 20480, PROT_READ|PROT_WRITE,
> MAP_PRIVATE|MAP_FIXED, 3, 0x12a000) = 0x4014f000
> 14 old_mmap(0x40154000, 15652, PROT_READ|PROT_WRITE,
> MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40154000
> 15 close(3) = 0
> 16 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,
> -1, 0) = 0x40158000
> 17 munmap(0x40015000, 64466) = 0
> 18 fstat64(1, {st_mode=S_IFCHR|0700, st_rdev=makedev(136, 0), ...}) = 0
> 19 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,
> -1, 0) = 0x40015000
> 20 write(1, "Usage: rce1 <number>\n", 21) = 21
> 21 munmap(0x40015000, 4096) = 0
> 22 semget(1, 4096, IPC_CREAT|0x40150140|0400) = -1 ENOSYS (Function not
> implemented)
> 23 _exit(1) = ?
**1:** Εδώ καλείται η execve() ώστε να εκτελεστεί το πρόγραμμα μας. Η επόμενη γραμμή είναι πιο ενδιαφέρουσα.
**2:** Η brk() χρησιμοποιείται για να αλλάξει το μέγεθος του data segment. Παίρνει ως παράμετρο το νέο τέλος του data segment και επιστρέφει 0 αν όλα πήγαν καλά
και -1 σε περίπτωση λάθους (όλα αυτά σύμφωνα με την man page που έχει ξεμείνει απο το linux 0.9.11). Εδώ η παράμετρος είναι 0 και η τιμή επιστροφής είναι ένας
μια διεύθυνση. Μόλις μάθαμε ότι η brk(NULL) επιστρέφει το τρέχον τέλος του data segment :)
Τώρα ο dynamic linker αναλαμβάνει δράση.\
**3-4:** Παρατηρούμε μια προσπάθεια να ανοίξουν δύο αρχεία: το \"/etc/ld.so.preload\" και το \"/etc/ld.so.cache\". Το πρώτο περιέχει μια λίστα με το ποιες
βιβλιοθήκες να φορτωθούν πριν από οποιαδήποτε βιβλιοθήκη του συγκεκριμένου εκτελέσιμου και στο δικό μου σύστημα δεν υπάρχει (θα ήταν ύποπτο αν υπήρχε\...). Το
δεύτερο περιέχει μια λίστα με όλες τις βιβλιοθήκες που γνωρίζει ο dynamic linker (δες man ldconfig) και ανοίγει επιτυχώς με file descriptor 3.
**5-7:** Με την fstat ο linker πληροφορείται για το αρχείο που μόλις άνοιξε. Η πληροφορία που τον ενδιαφέρει πιο πολύ είναι το μέγεθος st\_size=64466 (δες man
fstat). Ύστερα το αρχείο γίνεται mapped(αντιστοιχείται) στη μνήμη στη διεύθυνση 0x40015000 (δες man mmap) και το αρχείο κλείνει.
**8-15:** Ο linker βλέπει πως το εκτελέσιμο χρειάζεται τη βιβλιοθήκη libc.so.6. Έχοντας τις πληροφορίες από το προηγούμενο βήμα βρίσκει το path και το ανοίγει.
Ύστερα διαβάζει τα πρώτα 1024 bytes που περιέχουν τον ELF(Executable and Linkable Format) header του αρχείου και αφού επιβεβαιώσει ότι όντως πρόκειται για
shared βιβλιοθήκη φορτώνει τα διάφορα sections της.
**16-17:** Γίνονται map ανώνυμα 4Kbytes και ελευθερώνεται(unmapped) ο χώρος που αντιστοιχεί στο \"/etc/ld.so.cache\".
**18-19:** Λαμβάνονται πληροφορίες για τον file descriptor 1 (stdout) και γίνονται map ανώνυμα άλλα 4Kbytes
**20:** Εδώ γράφεται στον fd 1 το κείμενο μας. Εμείς δεν χρησιμοποιήσαμε την write κατευθείαν αλλά η printf τελικά καλεί την write.
**21-23:** Ελευθερώνεται το block που δεσμεύτηκε στη γραμμή 19, καλείται η semget() η οποία δεν έχει υλοποιηθεί(!) ακόμα στον δικό μου πυρήνα (2.4.20) και το
πρόγραμμα τερματίζει.
### [3.2 ltrace]{#ss3.2}
To ltrace είναι αντίστοιχο με το strace αλλά όπως δηλώνει και το όνομα του (library trace) παρακολουθεί κλήσεις συναρτήσεων από βιβλιοθήκες. Πρόκειται για ένα
αρκετά χρήσιμο πρόγραμμα που δίνει μια εποπτική εικόνα της ροής του προγράμματος. Βέβαια όπως και το strace μας δίνει απλώς μια ακολουθία από συμβάντα και
καθόλου πληροφορίες για τα κομβικά σημεία της διεργασίας (πχ κάποιος έλεγχος).
H σύνταξη είναι : **ltrace \[options\] \[-o outputfile\] \[objfile \[args\]\]**
Χρήσιμες παράμετροι είναι:\
**-e call1,call2,\...**\
Το πρόγραμμα παρακολουθεί μόνο τις call1,call2\... Αν πριν από κάποια συνάρτηση υπάρχει \'!\' τότε να μην παρακολουθηθεί.\
**-p pid**\
Καθορίζει σε ποια διεργασία να \"αγκιστρωθεί\" το πρόγραμμα ώστε να αρχίσει να παρακολουθεί. Προφανώς στην περίπτωση αυτή είναι περιττό να καθοριστεί το
objfile.\
**-i**\
Πριν από κάθε call εμφανίζεται η τιμή του IP (instruction pointer) τη στιγμή της κλήσης. Σε αντίθεση με την strace η επιλογή είναι πολύ χρήσιμη, διότι οι
κλήσεις συναρτήσεων βιβλιοθήκης γίνονται συνήθως κατευθείαν από τον κώδικα που μας ενδιαφέρει και όχι από κάποια μυστική γωνιά κάποιας απέραντης βιβλιοθήκης.\
**-l libfilename**\
Καθορίζει τις συναρτήσεις ποιων βιβλιοθηκών να παρακολουθεί η ltrace. Για περισσότερες από μια βιβλιοθήκες πρέπει να επαναληφθεί το \'-l\' πχ -l lib1 -l lib2
\...
> πχ
> bash$ ltrace -orce1.ltrace -i rce1
> Usage: rce1 <number>
> bash$ cat rce1.ltrace
> [080482fd] __libc_start_main(0x0804838c, 1, 0xbffff784, 0x08048274, 0x08048440
> <unfinished ...>
> [080483b4] printf("Usage: %s <number<\n", "rce1")
> = 21
> [080483c1] exit(1 <unfinished ...>
> [ffffffff] +++ exited (status 1) +++
### [4. Υλοποίηση των breakpoints]{#s4}
Τα breakpoints, όπως είχαμε πει και στο προηγούμενο τεύχος, είναι σημεία στον κώδικα όπου διακόπτεται η εκτέλεση του προγράμματος και ο έλεγχος επιστρέφει στον
debugger. Όσον αφορά στον τρόπο υλοποίησης τους μπορούν να χωριστούν σε δύο κατηγορίες: τα software και τα hardware.
### [4.1 Software Breakpoints]{#ss4.1}
Είναι το είδος που απαντάται πιο συχνά διότι δεν απαιτεί κάποια υποστήριξη από τον επεξεργαστή. Όταν ορίζουμε ένα 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 μετά από τον [πρόλογο της
συνάρτησης](05_rce2-5.html#c_call).
### [4.2 Hardware Breakpoints]{#ss4.2}
Τα 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]{#ss4.3}
Συχνά χρειάζεται να σταματήσουμε ένα κομμάτι κώδικα πριν εκτελεστεί αλλά δεν μπορούμε να το κάνουμε με τη 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 που θέλουμε εμείς!
>
### [5. Χρήσιμες έως **πολύ** χρήσιμες πληροφορίες]{#s5}
### [5.1 The C calling convention]{#ss5.1}
Στο κομμάτι αυτό θα εξετάσουμε τον τρόπο με τον οποίο γίνεται το πέρασμα των παραμέτρων στις συναρτήσεις αλλά και πως υλοποιούνται οι τοπικές μεταβλητές.
Ύστατος σκοπός είναι η εξοικείωση με τον υλοποίηση των συναρτήσεων σε χαμηλό επίπεδο ώστε η μελέτη των assembly listings να είναι γρήγορη και οδηγεί σε ξεκάθαρα
συμπεράσματα.
Υπάρχουν βέβαια διάφορες επιλογές αλλά εδώ θα δούμε την πιο κοινή, η οποία χρησιμοποιείται στα προγράμματα της C (και όχι μόνο). Η κλήση μιας συνάρτησης έχει ως
αποτέλεσμα τη μεταφορά του ελέγχου σε κάποιο αυθαίρετο κομμάτι κώδικα. Τα μεγάλα ερωτήματα που τίθενται είναι τα εξής:
- 1\. πως ξέρει αυτό το κομμάτι που βρίσκονται οι παράμετροι που περιμένει;
- 2.που αποθηκεύει τις τοπικές μεταβλητές;
Μια πρώτη προσέγγιση θα ήταν να παίρνει τα δεδομένα του (παραμέτρους και τοπικές μεταβλητές) από συγκεκριμένες απόλυτες διευθύνσεις μνήμης. Όλα ωραία και καλά
μέχρι να χρειαστεί να υλοποιήσουμε αναδρομικές συναρτήσεις (συναρτήσεις που καλούν τον εαυτό τους). Για παράδειγμα η f η οποία εκτυπώνει τους αριθμούς από το 1
μέχρι το x:
> void f(int x)
> {
> if (x>1)
> f(x-1);
> printf("%d ",x);
> }
και έστω πως η συνάρτηση περιμένει την παράμετρο x στη διεύθυνση 100 (τι πρωτότυπους αριθμούς που χρησιμοποιώ!)
Αν έχουμε την κλήση f(2) εμείς περιμένουμε το αποτέλεσμα \"1 2\". Ας δούμε πιο αναλυτικά τι όντως θα συμβεί: Όταν κληθεί για πρώτη φορά η f όλα είναι όπως
πρέπει, η διεύθυνση 100 περιέχει το αρχικό x (το 2). Το 2 είναι μεγαλύτερο από το 1 οπότε καλείται η f(x-1) δηλαδή η f(1). H διεύθυνση 100 περιέχει τώρα τον
αριθμό 1. Το τρέχον x δεν είναι μεγαλύτερο του 1 οπότε απλώς εκτελείται η printf και εκτυπώνεται το \"1\". Η συνάρτηση f(1) επιστρέφει και εκτελείται η printf η
οποία εκτυπώνει \... πάλι \"1\". Η προηγούμενη τιμή του x (το 2) έχει χαθεί :(
Η μαγική λέξη για την αποφυγή τέτοιων προβλημάτων είναι ο σωρός (stack). Κάθε πρόγραμμα διατηρεί το δικό του σωρό, ο οποίος αρχίζει από υψηλές διευθύνσεις και
μεγαλώνει προς της χαμηλές. Η σύμβαση της C για την κλήση συναρτήσεων λέει πως οι παράμετροι \"σπρώχνονται\" στο σωρό από τα δεξιά προς τα αριστερά. Επίσης η
συνάρτηση που έκανε την κλήση είναι υπεύθυνη για να καθαρίσει τον σωρό (να τον φέρει στην αρχική κατάσταση). Για παράδειγμα η κλήση της συνάρτησης f(x,y,z)
μεταφράζεται σε:
> push z
> push y
> push x
> call f -------> f: ...
> ...
> ret
> add esp,12 (4bytes * 3 παραμέτρους)
![](/33/img/stack01.png)
Οι τοπικές μεταβλητές αποθηκεύονται και αυτές στο σωρό σε χώρο που δεσμεύεται μετά τη διεύθυνση επιστροφής της συνάρτησης. Αυτό γίνεται απλά με την μείωση του
stack pointer esp (δείκτη στην \"κορυφή\" του σωρού) κατά τόσες θέσεις όσες τα bytes που χρειαζόμαστε για τις τοπικές μεταβλητές.
![](/33/img/stack02.png)
Για να προσπελάσουμε κάποιο δεδομένο χρησιμοποιούμε τον esp και τη σχετική απόσταση του από αυτόν. Για παράδειγμα η τελευταία τοπική μεταβλητή (σε σχέση με τη
σειρά που τις έχουμε δηλώσει στον κώδικα) βρίσκεται στη διεύθυνση esp+0.Το πρόβλημα με το παραπάνω σχέδιο είναι ότι κανείς δεν εγγυάται πως ο esp δεν θα αλλάξει
τιμή κατά τη διάρκεια της συνάρτησης. Για παράδειγμα αν εκτελεστεί μία εντολή push eax, η τελευταία τοπική μεταβλητή είναι πια στη θέση esp+4 και όχι esp+0.
Έτσι ο compiler πρέπει να φροντίσει να ακολουθεί τις αλλαγές και να παράγει σωστά offsets για τις τοπικές μεταβλητές.
![](/33/img/stack03.png)
Μια πιο βολική προσέγγιση χρησιμοποιεί την έννοια του \"πλαισίου\". Με κάθε κλήση συνάρτησης δημιουργείται στο σωρό ένα \"πλαίσιο\" (frame) που περιέχει τα
δεδομένα της συγκεκριμένης κλήσης (παραμέτρους, τοπικές μεταβλητές και διεύθυνση επιστροφής) και επίσης τη διεύθυνση του πλαισίου της προηγούμενης συνάρτησης
(αυτή που κάλεσε την τρέχουσα). Μέσα σε κάθε πλαίσιο ο τρόπος πρόσβασης στα τοπικά δεδομένα είναι ανεξάρτητος από τις αλλαγές του δείκτη του σωρού. Η δημιουργία
του πλαισίου απαιτεί 4 μικρές αλλαγές σε σχέση με τη προηγούμενη προσέγγιση:
- 1.Ένας καταχωρητής δεσμεύεται καθολικά για να συγκρατεί την τρέχουσα διεύθυνση πλαισίου. Στους x86 αυτός είναι ο ebp.
- 2.Πριν τη δέσμευση χώρου για τις τοπικές μεταβλητές αποθηκεύεται στο σωρό η διεύθυνση του προηγούμενου πλαισίου.
- 3.Μετά το (1) η διεύθυνση που περιέχει ο esp (κορυφή του σωρού) γίνεται η διεύθυνση του τρέχοντος πλαισίου και αποθηκεύεται στον ebp.
- 4.Πριν την επιστροφή της συνάρτησης επανατοποθετείται στον ebp η διεύθυνση του προηγούμενου πλαισίου.
Σχηματικά:
![](/33/img/stack04.png)
Τώρα σε κάθε συνάρτηση ο κώδικας για την προσπέλαση των τοπικών δεδομένων είναι ο ίδιος. Αν υποθέσουμε πως έχουμε επεξεργαστή και λειτουργικό 32-bit (πχ linux
:) ) τότε η πρώτη παράμετρος της συνάρτησης βρίσκεται στη θέση ebp+8, η δεύτερη στην ebp+12 κτλ Ομοίως η πρώτη τοπική μεταβλητή βρίσκεται στη θέση ebp-4, η
δεύτερη στη θέση ebp-8.Αυτό ισχύει ακόμα και όταν οι παράμετροι και οι τοπικές μεταβλητές είναι πιο μικροί από 4 bytes (πχ char). O compiler προτιμά να
σπαταλήσει λίγη μνήμη για λόγους ομοιογένειας στον παραγόμενο κώδικα αλλά κυρίως για λόγους απόδοσης, αφού με αυτόν τον τρόπο όλα τα δεδομένα είναι
ευθυγραμμισμένα (aligned) σε όρια των 4bytes (ανοίξτε κάποιο βιβλίο αρχιτεκτονικής υπολογιστών για να μάθετε γιατί\...). Βέβαια, υπάρχουν και προφανείς
εξαιρέσεις όταν για παράδειγμα έχουμε \"δεδομένα\" μεγαλύτερα των 4 bytes όπως structs.
Οι περισσότεροι compilers δίνουν επιλογή πιο από τους δύο τρόπους να χρησιμοποιήσουν. Ο gcc χρησιμοποιεί πλαίσια ως default επιλογή και με το flag
\"-fomit-frame-pointer\" προσπαθεί να το αποφύγει όπου γίνεται. Από το info:
> -fomit-frame-pointer
> Don't keep the frame pointer in a register for functions that
> don't need one. This avoids the instructions to save, set up and
> restore frame pointers; it also makes an extra register available
> in many functions. *It also makes debugging impossible on some
> machines.*
Mια συνάρτηση που χρησιμοποιεί frames τυπικά αρχίζει με την ακολουθία: []{#c_call}
> push ebp
> mov ebp,esp ; Function prologue
> sub esp, M
και τελειώνει:
> mov esp, ebp
> pop ebp ; Function epilogue
> ret
>
> ή
>
> leave
> ret
### [5.2 System Calls]{#ss5.2}
Τα σύγχρονα λειτουργικά συστήματα που σέβονται τον εαυτό τους, φροντίζουν να ξεχωρίζουν το \"χώρο\" του πυρήνα (kernel space) από το \"χώρο\" τον χρηστών (user
space). Αυτό γίνεται ώστε να μη μπορεί οποιοσδήποτε τυχαίος χρήστης να πειράξει τον πυρήνα(ή να εκτελεί κώδικα του πυρήνα αυθαίρετα) και να θέσει σε κίνδυνο την
ασφάλεια του συστήματος. Ο χωρισμός αυτός υλοποιείται με τη χρήση μηχανισμών που προσφέρει ο εκάστοτε επεξεργαστής (πχ paging, segmentation).
Βέβαια, με κάποιο τρόπο πρέπει οι εφαρμογές να επικοινωνούν με τον πυρήνα για διάφορες εργασίες (πχ Ι/Ο). Η λύσεις είναι τα λεγόμενα call gates (όχι colgates,
αυτά είναι οδοντόκρεμες\...) και τα software interrupts. Και τα δύο έχουν ως σκοπό να ορίσουν διακριτά σημεία εισόδου στον πυρήνα. Μπορείτε να τα σκεφτείτε ως
\"gateways\" για το τοπικό δίκτυο του πυρήνα.
Στο linux χρησιμοποιούνται τα call gates για εκτελέσιμα που έρχονται από άλλα λειτουργικά πχ solaris και το software interrupt 0x80 για native εφαρμογές. Στο
κείμενο αυτό θα ασχοληθούμε μόνο με την δεύτερη τεχνική (για την πρώτη θα έπρεπε πρώτα να αναφερθεί όλος ο μηχανισμός των descriptors στους x86). Το interrupt
0x80 όταν κληθεί μεταφέρει τον έλεγχο στον πυρήνα, μαζί με πληροφορίες για την εργασία που πρέπει να εκτελέσει.
Κατά την κλήση ενός syscall (για την αρχιτεκτονική x86) στον eax υπάρχει ο αριθμός του syscall και στους ebx, ecx, edx, esi, edi, ebp(πυρήνες 2.4 και πάνω) οι
μέχρι έξι παράμετροι που δέχεται το συγκεκριμένο syscall. Έτσι μια τυπική κλήση είναι:
> mov ebx, 0
> mov eax, 1 ; syscall 1: exit
> int 0x80
Φυσικά, επειδή το να γράφουμε τέτοιο κώδικα κάθε φορά που θέλουμε κάτι από τον πυρήνα δεν είναι και πολύ ευχάριστο, η libc έχει φροντίσει να δημιουργήσει τις
αντίστοιχες wrapper συναρτήσεις. Έτσι αντί για το παραπάνω εμείς αρκεί να γράφουμε exit(0).
### [6. Hands-on Παράδειγμα]{#s6}
Έστω ότι μια μέρα πέφτει στα χέρια σας το παρακάτω εκτελέσιμο : [rce2.bz2 (1.7k)](rce2.bz2)
Όλο χαρά το εκτελείτε:
> bash$ rce2
> Say the password: sesame
> What???
> bash$ rce2
> Say the password: kuku
> What???
Ποιο να είναι άραγε το μυστικό password;
Ας ετοιμαστούμε λοιπόν για αντιμετωπίσουμε ατελείωτα listing με εκατομμύρια γραμμές ακατανόητου κώδικα! Η ας δοκιμάσουμε κάτι πιο απλό:
> bash$ strings rce2
> ...
Η **strings** τυπώνει όλες τις εκτυπώσιμες ASCII ακολουθίες που υπάρχουν στο εκτελέσιμο και έχουν μήκος πάνω από 4 χαρακτήρες (default).
Μέσα στη λίστα με τα strings θα παρατηρήσετε κάποιο πιο ενδιαφέρον από τα υπόλοιπα :) Σπάνια βέβαια συμβαίνει να υπάρχουν plain-text κωδικοί μέσα στο εκτελέσιμο
αλλά δε χάνουμε τίποτα να δοκιμάσουμε!
Για εκπαιδευτικούς σκοπούς, θεωρήστε ότι η προηγούμενη διαδικασία δε απέδωσε καρπούς. Ας δούμε τι μπορούμε να μάθουμε για τη ροή του προγράμματος.
> bash$ ltrace -i -o rce2.ltr rce2
> Say the password: sesame
> What???
> bash$ cat rce2.ltr
> [080483b5] __libc_start_main(0x0804851c, 1, 0xbffff784, 0x0804830c, 0x08048594
> <unfinished ...>
> [080484d5] printf("%s: ", "Say the password") =
> 18
> [080484e1] fflush(0x40150340) = 0
> [080484f5] fgets("sesame\n", 20, 0x401501e0) =
> 0xbffff700
> [0804849c] isspace(10, 0x40153234, 0xbffff700, 0xbffff784, 0xbffff6e8) =
> 8192
> [0804849c] isspace(101, 0x40153234, 0xbffff700, 0xbffff784, 0xbffff6e8) = 0
> [08048557] puts("What???") = 8
> [ffffffff] +++ exited (status 0) +++
Μόλις αποκτήσαμε δύο σπουδαίες πληροφορίες.\
Κοιτάξτε καλά τη \_\_libc\_start\_main().\
Συνεχίστε να την κοιτάζετε.\
H \_\_libc\_start\_main(), εκτός των άλλων, φροντίζει να κληθεί και το δικό μας κυρίως πρόγραμμα.\
Κοιτάξτε λίγο ακόμα\...\
H \_\_libc\_start\_main() πρέπει να γνωρίζει που βρίσκεται ο δικός μας κώδικας. Επομένως\...\
Για να σας βγάλω από την αγωνία, σας λέω πως η πρώτη παράμετρος της \_\_libc\_start\_main() πρόκειται για τη διεύθυνση της main (0x0804851c)!
Αυτή η πρώτη πληροφορία οδηγεί στη δεύτερη. Παρατηρήστε ότι όλες οι διευθύνσεις μετά την \_\_libc\_start\_main() και μέχρι πριν την puts() είναι μικρότερες από
την αρχή της main(). Άρα είναι ασφαλές να υποθέσουμε πως δεν ανήκουν σε αυτή αλλά σε κάποια άλλη συνάρτηση.
Το τρίτο πράγμα που μάθαμε, είναι ότι στη διεύθυνση 0x08048557 έχει ήδη αποφασιστεί αν το password μας είναι σωστό ή όχι.
Ας χρησιμοποιήσουμε το βαρύ πυροβολικό\...
> bash$ gdb rce2
> (no debugging symbols found)...(gdb) break main
> Function "main" not defined.
> (gdb)
Ουπς. Το εκτελέσιμο δεν περιέχει σύμβολα αλλά εμείς ξέρουμε τη διεύθυνση της main()!
> (gdb) break *0x804851c
> Breakpoint 1 at 0x804851c
> (gdb) r
> Starting program: /home/alf/projects/magaz/issue1/rce2
> (no debugging symbols found)...
> Breakpoint 1, 0x0804851c in printf ()
> (gdb) x/30i $eip
> 0x804851c <printf+408>: push ebp
> 0x804851d <printf+409>: mov ebp,esp
> 0x804851f <printf+411>: push edi
> 0x8048520 <printf+412>: push esi
> 0x8048521 <printf+413>: sub esp,0x20
> 0x8048524 <printf+416>: and esp,0xfffffff0
> 0x8048527 <printf+419>: push esi
> 0x8048528 <printf+420>: push 0x13
> 0x804852a <printf+422>: lea esi,[ebp-40]
> 0x804852d <printf+425>: push esi
> 0x804852e <printf+426>: push 0x80485bd
> 0x8048533 <printf+431>: call 0x80484bc <printf+312>
> 0x8048538 <printf+436>: mov edi,0x80485ce
> 0x804853d <printf+441>: mov ecx,0xb
> 0x8048542 <printf+446>: cld
> 0x8048543 <printf+447>: add esp,0x10
> 0x8048546 <printf+450>: repz cmps ds:[esi],es:[edi]
> 0x8048548 <printf+452>: jne 0x8048564 <printf+480>
> 0x804854a <printf+454>: sub esp,0xc
> 0x804854d <printf+457>: push 0x80485d9
> 0x8048552 <printf+462>: call 0x8048354 <puts>
> 0x8048557 <printf+467>: add esp,0x10
> 0x804855a <printf+470>: lea esp,[ebp-8]
> 0x804855d <printf+473>: pop esi
> 0x804855e <printf+474>: xor eax,eax
> 0x8048560 <printf+476>: pop edi
> 0x8048561 <printf+477>: leave
> 0x8048562 <printf+478>: ret
> 0x8048563 <printf+479>: nop
> 0x8048564 <printf+480>: sub esp,0xc
O GDB νομίζει πως βρισκόμαστε 408 bytes από την αρχή της printf για τους [γνωστούς λόγους](05_rce2-2.html#gdb_asm). Πριν αρχίσουμε σαν παλαβοί να κάνουμε single
step ας δούμε τι μπορούμε να συνάγουμε από το listing.
push ebp
mov ebp,esp
push edi
push esi
Καταρχάς η main είναι [frame-based](05_rce2-5.html#c_call) κάτι που φαίνεται από το γνώριμο πρόλογο. Επίσης σώζονται στο σωρό οι καταχωρητές esi και edi ώστε να
μπορούν να ανακτηθούν οι τιμές τους πριν το τέλος της συνάρτησης. Το γιατί γίνεται αυτό πρόκειται για μια εσωτερική υπόθεση του compiler (o gcc διατηρεί τους
ebx, esi και edi κατά τις κλήσεις διότι τους χρησιμοποιεί για δικά του θέματα (πχ προσωρινή αποθήκευση τιμών).
sub esp,0x20
and esp,0xfffffff0
Μετά δεσμεύεται χώρος στο σωρό για 0x20 bytes και επίσης ο esp μειώνεται(πιθανότατα) και άλλο ώστε να έρθει σε όριo των 16 bytes. Η όλη διαδικασία είναι
αποτέλεσμα του optimization και δεν είναι απαραίτητο πως το πρόγραμμα θα χρησιμοποιήσει όλη τη μνήμη που δεσμεύτηκε τελικά.
push esi <--- ??
push 0x13
lea esi,[ebp-40]
push esi
push 0x80485bd
call 0x80484bc <printf+312>
...
add esp,0x10
Ακολουθεί ένα μυστηριώδες \"push esi\" και ύστερα μια κλασική κλήση συνάρτησης. Η συνάρτηση δέχεται τρεις παραμέτρους: func\_0x804845c( 0x80485bd, 0x13,
ebp-40). H τελευταία παράμετρος είναι η διεύθυνση μιας τοπικής μεταβλητής, αφού έχει αρνητικό offset από τον ebp. Το αναμενόμενο \"add esp, X\" (διόρθωση του
σωρού) βρίσκεται λίγες γραμμές πιο κάτω. Ο λόγος που δε βρίσκεται αμέσως μετά την call, είναι ότι ο gcc αποφάσισε να αλλάξει τη σειρά των εντολών για λόγους
optimization (έχει να κάνει με τα pipelines του pentium). Όλα εντάξει;
Ελπίζω να μην απαντήσατε \"ναι\"! Κάτι δεν πάει καλά σε όσα έχω πει ως τώρα: γιατί δέχτηκα με τόση σιγουριά πως η συνάρτηση δέχεται τρεις παραμέτρους, ενώ όχι
μόνο γίνονται τέσσερα push πριν από αυτή αλλά κυρίως, ο σωρός διορθώνεται κατά 0x10 = 4\*4 bytes (4 bytes για κάθε παράμετρο). Λοιπόν, εκτός από το γεγονός ότι
εγώ έγραψα το πρόγραμμα και το ξέρω :), υπάρχει ένας επιπλέον λόγος. Όταν γίνεται το πρώτο \"push esi\", ο esi δεν έχει αρχικοποιηθεί μέσα στη main() και έτσι
έχει μια άγνωστη τιμή. Γιατί να περάσουμε σε μια συνάρτηση μια παράμετρο με άγνωστη τιμή;
Η απάντηση για ακόμα μια φορά είναι το optimization (περιττό να σας πω ότι το πρόγραμμα έγινε compile με -Ο2). Ο compiler θέλει να κρατάει τον esp σε
διευθύνσεις πολλαπλάσιες των 16 bytes! Το κόστος προσπέλασης σε μη aligned διευθύνσεις είναι τόσο σημαντικό, ώστε ο gcc εισάγει dummy εντολές για να το
αποφύγει!
mov edi,0x80485ce
mov ecx,0xb
cld
...
repz cmps ds:[esi],es:[edi]
jne 0x8048564 <printf+480>
Εδώ στον edi φορτώνεται μια διεύθυνση μνήμης και στον edi η τιμή 11. Μετά \"καθαρίζεται\" το direction flag. Ακολουθεί η εντολή \"repz cmps
ds:\[esi\],es:\[edi\]\", η οποία με λίγα λόγια λέει: όσο τα bytes που βρίσκονται στις διευθύνσεις που δείχνουν esi και edi είναι ίσα και ο ecx δεν είναι 0
αύξησε (αν το direction flag είναι 0/clear) τους esi και edi κατά ένα byte και έλεγξε ξανά (η μαγεία των CISC επεξεργαστων\...). Ουσιαστικά κάνει ακριβώς την
ίδια δουλειά με μια κλήση strncmp(esi,edi,ecx), ελέγχει αν τα πρώτα \#ecx bytes δύο string που αρχίζουν στις διευθύνσεις esi και edi είναι ίσα.
Ωραία, ξέρουμε την τιμή του edi αλλά ο esi τη τιμή έχει; Λίγο πιο πάνω υπάρχει η \"lea esi,\[ebp-40\]\" και επειδή ξέρουμε ότι ο gcc φροντίζει να μην αλλάζει ο
esi από συναρτήσεις, είμαστε σίγουροι ότι έχει ακόμα την ίδια τιμή.
Για να συνοψίσουμε, το πρόγραμμα καλεί μια συνάρτηση της οποίας μια παράμετρος είναι η διεύθυνση μιας τοπικής μεταβλητής (ebp-40) και μετά συγκρίνει τα bytes
που βρίσκονται εκεί με κάποια άλλα που βρίσκονται σε μια σταθερή θέση. Δε ξέρω τι λέτε εσείς αλλά εμένα μου φαίνεται πως εδώ γίνεται ο έλεγχος του password!
Η επόμενη εντολή είναι η jne (jump if not equal/zero). Αν η σύγκριση είναι επιτυχής τότε το zero flag έχει ενεργοποιηθεί από το προηγούμενο βήμα και έτσι
**δεν** ακολουθούμε το άλμα. Αν το password είναι λάθος το ZF=0 και το άλμα γίνεται.
Για να δούμε ποίο είναι το password:
> (gdb) break *0x8048546
> Breakpoint 2 at 0x8048546
> (gdb) c
> Continuing.
> Say the password: sesame
>
> Breakpoint 2, 0x08048546 in printf ()
> (gdb) x/5i $eip
> 0x8048546 <printf+450>: repz cmps ds:[esi],es:[edi]
> 0x8048548 <printf+452>: jne 0x8048564 <printf+480>
> 0x804854a <printf+454>: sub esp,0xc
> 0x804854d <printf+457>: push 0x80485d9
> 0x8048552 <printf+462>: call 0x8048354 <puts>
> (gdb) x/s $esi
> 0xbffff720: "sesame"
> (gdb) x/s $edi
> 0x80485ce <_IO_stdin_used+26>: .......... xe xe!
Η παραπάνω τεχνική, όπου βρίσκουμε ένα σωστό password/serial εντοπίζοντας το σημείο που γίνεται η σύγκριση με αυτό που έχουμε εισάγει εμείς, λέγεται
**password/serial fishing**
Εντάξει, μάθαμε το password, γιατί όμως να σταματήσουμε εδώ; Γιατί να μην πειράξουμε το πρόγραμμα ώστε να δέχεται ως σωστό κάθε password; Για να το πετύχουμε,
αρκεί να μην ακολουθούμε ποτέ το άλμα. Ξέρουμε ότι η jne (σε αυτή τη μορφή) καταλαμβάνει στη μνήμη 2 bytes, διότι η επόμενη εντολή αρχίζει δύο bytes πιο μετά.
> (gdb) x/2b 0x8048548
> 0x8048548 <printf+452>: 0x75 0x1a
Το 0x75 είναι το opcode της εντολής ενώ το 0x1a η (προσημασμένη) απόσταση του άλματος. Αν γίνει το άλμα ο έλεγχος θα μεταφερθεί 0x1a bytes από το τέλος της
εντολής(αρχή της επόμενης), εδώ 0x804854a + 0x1a = 0x8048564.
Για να πετύχουμε το σκοπό μας, αρκεί να αντικαταστήσουμε τα δύο αυτά bytes με nop (no operation, opcode 0x90). Φυσικά δεν έχει νόημα να το κάνουμε αυτό μόνο στη
μνήμη αλλά στο image του εκτελέσιμου που βρίσκεται στο αρχείο. Το πρόβλημα είναι να εντοπίσουμε σε ποιο σημείο του αρχείου βρίσκεται ο κώδικας που θέλουμε να
πειράξουμε. Ο ELF header έχει όλες τις πληροφορίες που χρειαζόμαστε αλλά αφού δεν έχουμε αναφερθεί ακόμα σε αυτόν θα κάνουμε κάτι άλλο: θα ψάξουμε το αρχείο για
να βρούμε την αρχική ακολουθία από bytes. Υπάρχει περίπτωση η ίδια ακολουθία να υπάρχει σε πολλά σημεία και πρέπει να είμαστε σίγουροι πως πειράζουμε το σωστό.
Για αυτό χρειαζόμαστε αρκετά bytes γύρω από την εντολή (το context).
> (gdb) x/10b 0x8048548-5
> 0x8048543 <printf+447>: 0x83 0xc4 0x10 0xf3 0xa6 0x75
> 0x1a 0x83
> 0x804854b <printf+455>: 0xec 0x0c
Αν ψάξετε για αυτά τα bytes στο αρχείο(με έναν hexeditor πχ του mc) θα τα βρείτε μόνο μια φορά και το offset του 0x75 0x1a είναι 0x548. Αντικαταστήστε τα δύο
αυτά bytes με 0x90 0x90.
> bash$ ./rce2
> Say the password: qrwrwr
> Hooray!
> bash$ ./rce2
> Say the password: sdfeg4453
> Hooray!
Συγχαρητήρια, μόλις \"σπάσατε\" το πρόγραμμα!
### [7. Πρόκληση]{#s7}
### [7.1 Προηγούμενη πρόκληση - Λύση και Hall Of Fame]{#ss7.1}
Στην προηγούμενη πρόκληση ο σκοπός ήταν να βρείτε ποιος κωδικός αντιστοιχεί στο όνομα σας. Το μεγάλο πρόβλημα που είχα ήταν ότι για να υπάρξει source listing
πρέπει εκτός από το compilation με το -g να είναι διαθέσιμο και το αντίστοιχο source αρχείο. Θα μπορούσα απλώς να σας το δώσω και να σας προτρέψω να μην το
κοιτάξετε. Κάτι μου λέει όμως ότι πολλοί δε θα μπορούσαν να αντισταθούν στο πειρασμό ;) Για αυτό το λόγο το κυρίως αρχείο είναι απλώς ένα container που περιέχει
τόσο το εκτελέσιμο challenge όσο και τον κώδικα του (στοιχειωδώς κρυπτογραφημένο με xor). Κατά την εκτέλεση τα κάνει dump σε δύο κρυφά αρχεία(\".ch0src\" και
\".alfch0\") στον τρέχοντα κατάλογο και τρέχει το \".alfch0\" ή τον gdb. Τα αρχεία αυτά διαγράφονται μόλις τελειώσει το πρόγραμμα.
Το challenge αυτό καθεαυτό είναι σχετικά απλό, για αυτό και άλλαξα τον πηγαίο κώδικα ώστε να έχει ανούσια ονόματα μεταβλητών και συναρτήσεων. Αρχικά προτρέπει
τον χρήστη για το όνομα και τον κωδικό (συνάρτηση \"f\"), και μετά παράγει δύο αριθμούς, έναν για το όνομα(συνάρτηση \"f1\") και έναν για τον κωδικό(συνάρτηση
\"f45\"). Aν οι δύο αριθμοί συμπίπτουν και το μήκος το ονόματος και του κωδικού είναι μη μηδενικά τότε όλα ΟΚ!
Η συνάρτηση f1 περιέχει μια πιθανώς one-way hash function (σημ: δεν έχει αποδειχτεί ότι τέτοιες συναρτήσεις υπάρχουν) ενώ η f45 είναι σαφώς
αντιστρέψιμη(υπολογιστικά πάντα). Αν και η f45 ήταν δύσκολα αντιστρέψιμη, τότε η πρόκληση θα ήταν πρακτικά άλυτη. Για όσους αρέσκονται στoυς τύπους:
Ν=f1(name)\
P=f45(pass)
Εμείς γνωρίζουμε τo name, την f1(), την f45() και μπορούμε να υπολογίσουμε το Ν. Το πρόβλημα είναι να βρούμε το pass ώστε το P να είναι ίσο με το N.
Η f45() είναι:
> long int f45(char *s,int l)
> {
> unsigned long int h;
>
> h=strtoul(s,NULL,16);
> if (h!=0) {
> h^=0x55555555;
> if (h&1)
> h^=0x00badbad;
> else
> h^=0x00dabdab;
> }
> return h;
> }
Η αντιστρεψιμότητα(υπολογιστική) της f45() βασίζεται στο γεγονός ότι η xor είναι συμμετρική και εδώ έχουμε μόνο xor. Επίσης με το if(h&1) {\...} υπάρχει
κίνδυνος να κάνουμε την f45() μη αντιστρέψιμη(μαθηματικά) αλλά οι τιμές είναι επιλεγμένες ώστε να μη συμβαίνει αυτό. Βέβαια η μαθηματική μη αντιστρεψιμότητα δεν
είναι πρόβλημα, απλώς θα υπήρχαν περισσότεροι από ένας σωστοί κωδικοί για κάθε όνομα.
Επομένως η f\_45() (αντίστροφη):
> unsigned long f_45(unsigned long h)
> {
> unsigned long r;
>
> r=h;
>
> if (r!=0) {
> r^=0x55555555;
> if (r&1)
> r^=0x00badbad;
> else
> r^=0x00dabdab;
> }
>
> return r;
> }
**Hall Of Fame**
Συγχαρητήρια στους:
1\. Λαμπής Μιχαήλ\
2. Γιώργος Πρέκας\
3. Αρχισυντάκτης (\...με ανάγκασε να τον συμπεριλάβω στο hall of fame!)
Για τις δωρεάν συνδρομές ενοχλείστε τον αρχισυντάκτη :)
Ο κώδικας του προηγούμενου challenge: [magaz-ch0-src.tar.bz2 (15k)](magaz-ch0-src.tar.bz2)
### [7.2 Πρόκληση \#1]{#ss7.2}
Αυτή τη φορά τα πράγματα είναι κάπως πιο πολύπλοκα. Το εκτελέσιμο δεν περιέχει debugging πληροφορίες. Σκοπός είναι το σύστημα να σας πει ότι το authentication
έγινε με επιτυχία. Καλή εξερεύνηση\...
Το εκτελέσιμο: [challenge1.bz2 (6.3k)](challenge1.bz2)
Όσοι επιθυμούν ας μου στείλουν ένα mail εξηγώντας συνοπτικά πως λειτουργεί το πρόγραμμα και τα δεδομένα-απαντήσεις. Αν κάποιος έχει όρεξη ας φτιάξει και έναν
key generator! Ως συνήθως όσοι τα καταφέρουν θα μπουν στο επόμενο Hall Of Fame!
Στείλτε σχόλια, διορθώσεις, προσθήκες στο alf82 at freemail dot gr.