1280 γραμμές
90 KiB
Markdown
1280 γραμμές
90 KiB
Markdown
+++
|
||
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 παραμέτρους)
|
||
|
||

|
||
|
||
Οι τοπικές μεταβλητές αποθηκεύονται και αυτές στο σωρό σε χώρο που δεσμεύεται μετά τη διεύθυνση επιστροφής της συνάρτησης. Αυτό γίνεται απλά με την μείωση του
|
||
stack pointer esp (δείκτη στην \"κορυφή\" του σωρού) κατά τόσες θέσεις όσες τα bytes που χρειαζόμαστε για τις τοπικές μεταβλητές.
|
||
|
||

|
||
|
||
Για να προσπελάσουμε κάποιο δεδομένο χρησιμοποιούμε τον esp και τη σχετική απόσταση του από αυτόν. Για παράδειγμα η τελευταία τοπική μεταβλητή (σε σχέση με τη
|
||
σειρά που τις έχουμε δηλώσει στον κώδικα) βρίσκεται στη διεύθυνση esp+0.Το πρόβλημα με το παραπάνω σχέδιο είναι ότι κανείς δεν εγγυάται πως ο esp δεν θα αλλάξει
|
||
τιμή κατά τη διάρκεια της συνάρτησης. Για παράδειγμα αν εκτελεστεί μία εντολή push eax, η τελευταία τοπική μεταβλητή είναι πια στη θέση esp+4 και όχι esp+0.
|
||
Έτσι ο compiler πρέπει να φροντίσει να ακολουθεί τις αλλαγές και να παράγει σωστά offsets για τις τοπικές μεταβλητές.
|
||
|
||

|
||
|
||
Μια πιο βολική προσέγγιση χρησιμοποιεί την έννοια του \"πλαισίου\". Με κάθε κλήση συνάρτησης δημιουργείται στο σωρό ένα \"πλαίσιο\" (frame) που περιέχει τα
|
||
δεδομένα της συγκεκριμένης κλήσης (παραμέτρους, τοπικές μεταβλητές και διεύθυνση επιστροφής) και επίσης τη διεύθυνση του πλαισίου της προηγούμενης συνάρτησης
|
||
(αυτή που κάλεσε την τρέχουσα). Μέσα σε κάθε πλαίσιο ο τρόπος πρόσβασης στα τοπικά δεδομένα είναι ανεξάρτητος από τις αλλαγές του δείκτη του σωρού. Η δημιουργία
|
||
του πλαισίου απαιτεί 4 μικρές αλλαγές σε σχέση με τη προηγούμενη προσέγγιση:
|
||
|
||
- 1.Ένας καταχωρητής δεσμεύεται καθολικά για να συγκρατεί την τρέχουσα διεύθυνση πλαισίου. Στους x86 αυτός είναι ο ebp.
|
||
- 2.Πριν τη δέσμευση χώρου για τις τοπικές μεταβλητές αποθηκεύεται στο σωρό η διεύθυνση του προηγούμενου πλαισίου.
|
||
- 3.Μετά το (1) η διεύθυνση που περιέχει ο esp (κορυφή του σωρού) γίνεται η διεύθυνση του τρέχοντος πλαισίου και αποθηκεύεται στον ebp.
|
||
- 4.Πριν την επιστροφή της συνάρτησης επανατοποθετείται στον ebp η διεύθυνση του προηγούμενου πλαισίου.
|
||
|
||
Σχηματικά:
|
||
|
||

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