hacktricks/binary-exploitation/arbitrary-write-2-exec/www2exec-atexit.md

16 KiB
Raw Blame History

WWW2Exec - atexit(), TLS Storage & Άλλοι μπερδεμένοι Δείκτες

{% hint style="success" %} Μάθετε & εξασκηθείτε στο Hacking του AWS:Εκπαίδευση HackTricks AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο Hacking του GCP: Εκπαίδευση HackTricks GCP Red Team Expert (GRTE)

Υποστηρίξτε το HackTricks
{% endhint %}

Δομές __atexit

{% hint style="danger" %} Σήμερα είναι πολύ παράξενο να εκμεταλλεύεστε αυτό! {% endhint %}

Το atexit() είναι μια συνάρτηση στην οποία άλλες συναρτήσεις περνιούνται ως παράμετροι. Αυτές οι συναρτήσεις θα εκτελεστούν κατά την εκτέλεση μιας exit() ή την επιστροφή του κύριου προγράμματος.
Αν μπορείτε να τροποποιήσετε τη διεύθυνση οποιασδήποτε από αυτές τις συναρτήσεις ώστε να δείχνει σε ένα shellcode για παράδειγμα, τότε θα κερδίσετε έλεγχο της διεργασίας, αλλά αυτό είναι προς το παρόν πιο περίπλοκο.
Προς το παρόν οι διευθύνσεις των συναρτήσεων που πρόκειται να εκτελεστούν είναι κρυμμένες πίσω από αρκετές δομές και τελικά η διεύθυνση στην οποία δείχνει δεν είναι οι διευθύνσεις των συναρτήσεων, αλλά είναι κρυπτογραφημένες με XOR και μετατοπίσεις με ένα τυχαίο κλειδί. Έτσι αυτός ο διανυσματικός επιθετικός δεν είναι πολύ χρήσιμος τουλάχιστον σε x86 και x64_86.
Η συνάρτηση κρυπτογράφησης είναι PTR_MANGLE. Άλλες αρχιτεκτονικές όπως m68k, mips32, mips64, aarch64, arm, hppa... δεν υλοποιούν τη συνάρτηση κρυπτογράφησης επειδή επιστρέφουν το ίδιο με αυτό που λαμβάνουν ως είσοδο. Έτσι αυτές οι αρχιτεκτονικές θα μπορούσαν να είναι επιθετικές με αυτό το διάνυσμα.

Μπορείτε να βρείτε μια λεπτομερή εξήγηση για το πώς λειτουργεί αυτό στο https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html

Όπως εξηγείται σε αυτήν την ανάρτηση, Αν το πρόγραμμα εξέρχεται χρησιμοποιώντας return ή exit() θα εκτελέσει το __run_exit_handlers() το οποίο θα καλέσει τους εγγεγραμμένους καταστροφείς.

{% hint style="danger" %} Αν το πρόγραμμα εξέρχεται μέσω της συνάρτησης _exit(), θα καλέσει τη κλήση συστήματος exit και οι χειριστές εξόδου δεν θα εκτελεστούν. Έτσι, για να επιβεβαιώσετε ότι το __run_exit_handlers() εκτελείται μπορείτε να ορίσετε ένα σημείο ανακοπής σε αυτό. {% endhint %}

Ο σημαντικός κώδικας είναι (πηγή):

ElfW(Dyn) *fini_array = map->l_info[DT_FINI_ARRAY];
if (fini_array != NULL)
{
ElfW(Addr) *array = (ElfW(Addr) *) (map->l_addr + fini_array->d_un.d_ptr);
size_t sz = (map->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)));

while (sz-- > 0)
((fini_t) array[sz]) ();
}
[...]




// This is the d_un structure
ptype l->l_info[DT_FINI_ARRAY]->d_un
type = union {
Elf64_Xword d_val;	// address of function that will be called, we put our onegadget here
Elf64_Addr d_ptr;	// offset from l->l_addr of our structure
}

Σημειώστε πώς map -> l_addr + fini_array -> d_un.d_ptr χρησιμοποιείται για να υπολογιστεί η θέση του πίνακα συναρτήσεων προς κλήση.

Υπάρχουν μερικές επιλογές:

  • Αντικαταστήστε την τιμή του map->l_addr ώστε να δείχνει σε ένα ψεύτικο fini_array με οδηγίες για την εκτέλεση αυθαίρετου κώδικα
  • Αντικαταστήστε τις καταχωρήσεις l_info[DT_FINI_ARRAY] και l_info[DT_FINI_ARRAYSZ] (που είναι περίπου διαδοχικές στη μνήμη), ώστε να κάνουν δείχνουν σε μια πλαστή δομή Elf64_Dyn που θα κάνει ξανά το array να δείχνει σε μια ζώνη μνήμης που ελέγχεται από τον επιτιθέμενο.
  • Αυτή η ανάλυση αντικαθιστά το l_info[DT_FINI_ARRAY] με τη διεύθυνση μιας ελεγχόμενης μνήμης στο .bss που περιέχει ένα ψεύτικο fini_array. Αυτός ο ψεύτικος πίνακας περιέχει πρώτα μια διεύθυνση one gadget που θα εκτελεστεί και στη συνέχεια τη διαφορά μεταξύ της διεύθυνσης αυτού του ψεύτικου πίνακα και της τιμής του map->l_addr έτσι ώστε το *array να δείχνει στο ψεύτικο πίνακα.
  • Σύμφωνα με την κύρια ανάρτηση αυτής της τεχνικής και αυτή την ανάλυση το ld.so αφήνει ένα δείκτη στη στοίβα που δείχνει στο δυαδικό link_map στο ld.so. Με ένα αυθαίρετο γράψιμο είναι δυνατό να το αντικαταστήσετε και να το κάνετε να δείχνει σε ένα ψεύτικο fini_array που ελέγχεται από τον επιτιθέμενο με τη διεύθυνση ενός one gadget για παράδειγμα.

Ακολουθώντας τον προηγούμενο κώδικα μπορείτε να βρείτε μια άλλη ενδιαφέρουσα ενότητα με τον κώδικα:

/* Next try the old-style destructor.  */
ElfW(Dyn) *fini = map->l_info[DT_FINI];
if (fini != NULL)
DL_CALL_DT_FINI (map, ((void *) map->l_addr + fini->d_un.d_ptr));
}

Σε αυτήν την περίπτωση θα ήταν δυνατό να αντικατασταθεί η τιμή του map->l_info[DT_FINI] που δείχνει σε ένα πλαστό ElfW(Dyn) δομή. Βρείτε περισσότερες πληροφορίες εδώ.

Αντικατάσταση TLS-Storage dtor_list στο __run_exit_handlers

Όπως εξηγείται εδώ, αν ένα πρόγραμμα τερματίσει μέσω return ή exit(), θα εκτελέσει το __run_exit_handlers() το οποίο θα καλέσει οποιαδήποτε συνάρτηση καταστροφέων που έχει καταχωρηθεί.

Κώδικας από το _run_exit_handlers():

/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS.  */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors.  */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__call_tls_dtors ();

Κώδικας από το __call_tls_dtors():

typedef void (*dtor_func) (void *);
struct dtor_list //struct added
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};

[...]
/* Call the destructors.  This is called either when a thread returns from the
initial function or when the process exits via the exit function.  */
void
__call_tls_dtors (void)
{
while (tls_dtor_list)		// parse the dtor_list chained structures
{
struct dtor_list *cur = tls_dtor_list;		// cur point to tls-storage dtor_list
dtor_func func = cur->func;
PTR_DEMANGLE (func);						// demangle the function ptr

tls_dtor_list = tls_dtor_list->next;		// next dtor_list structure
func (cur->obj);
[...]
}
}

Για κάθε εγγεγραμμένη λειτουργία στο tls_dtor_list, θα αποκωδικοποιήσει το δείκτη από το cur->func και θα το καλέσει με το όρισμα cur->obj.

Χρησιμοποιώντας τη λειτουργία tls από αυτό το fork του GEF, είναι δυνατό να δούμε ότι πραγματικά η λίστα dtor_list είναι πολύ κοντά στο stack canary και το PTR_MANGLE cookie. Έτσι, με ένα υπερχείλιση σε αυτό θα ήταν δυνατό να αντικατασταθεί το cookie και το stack canary.
Αν αντικατασταθεί το PTR_MANGLE cookie, θα ήταν δυνατό να παρακάμψει τη λειτουργία PTR_DEMANLE με το να το ορίσει σε 0x00, πράγμα που σημαίνει ότι το xor που χρησιμοποιείται για να πάρει την πραγματική διεύθυνση είναι ακριβώς η διεύθυνση που έχει ρυθμιστεί. Έπειτα, γράφοντας στη dtor_list είναι δυνατό να αλυσιδωθούν αρκετές λειτουργίες με τη διεύθυνση της λειτουργίας και το όρισμά της.

Τέλος, παρατηρήστε ότι ο αποθηκευμένος δείκτης όχι μόνο θα γίνει xor με το cookie αλλά θα περιστραφεί και κατά 17 bits:

0x00007fc390444dd4 <+36>:	mov    rax,QWORD PTR [rbx]      --> mangled ptr
0x00007fc390444dd7 <+39>:	ror    rax,0x11		        --> rotate of 17 bits
0x00007fc390444ddb <+43>:	xor    rax,QWORD PTR fs:0x30	--> xor with PTR_MANGLE

Έτσι πρέπει να λάβετε υπόψη αυτό πριν προσθέσετε μια νέα διεύθυνση.

Βρείτε ένα παράδειγμα στην αρχική δημοσίευση.

Άλλες παραμορφωμένες δείκτες στο __run_exit_handlers

Αυτή η τεχνική εξηγείται εδώ και εξαρτάται ξανά από το πρόγραμμα που τερματίζει καλώντας return ή exit() έτσι ώστε να κληθεί το __run_exit_handlers().

Ας ελέγξουμε περισσότερο κώδικα αυτής της συνάρτησης:

while (true)
{
struct exit_function_list *cur;

restart:
cur = *listp;

if (cur == NULL)
{
/* Exit processing complete.  We will not allow any more
atexit/on_exit registrations.  */
__exit_funcs_done = true;
break;
}

while (cur->idx > 0)
{
struct exit_function *const f = &cur->fns[--cur->idx];
const uint64_t new_exitfn_called = __new_exitfn_called;

switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
void *arg;

case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
arg = f->func.on.arg;
PTR_DEMANGLE (onfct);

/* Unlock the list while we call a foreign function.  */
__libc_lock_unlock (__exit_funcs_lock);
onfct (status, arg);
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_at:
atfct = f->func.at;
PTR_DEMANGLE (atfct);

/* Unlock the list while we call a foreign function.  */
__libc_lock_unlock (__exit_funcs_lock);
atfct ();
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_cxa:
/* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
we must mark this function as ef_free.  */
f->flavor = ef_free;
cxafct = f->func.cxa.fn;
arg = f->func.cxa.arg;
PTR_DEMANGLE (cxafct);

/* Unlock the list while we call a foreign function.  */
__libc_lock_unlock (__exit_funcs_lock);
cxafct (arg, status);
__libc_lock_lock (__exit_funcs_lock);
break;
}

if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
/* The last exit function, or another thread, has registered
more exit functions.  Start the loop over.  */
goto restart;
}

*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element.  */
free (cur);
}

__libc_lock_unlock (__exit_funcs_lock);

Η μεταβλητή f δείχνει στη δομή initial και ανάλογα με την τιμή του f->flavor θα κληθούν διαφορετικές συναρτήσεις.
Ανάλογα με την τιμή, η διεύθυνση της συνάρτησης που θα κληθεί θα βρίσκεται σε διαφορετική θέση, αλλά θα είναι πάντα αποκωδικοποιημένη.

Επιπλέον, στις επιλογές ef_on και ef_cxa είναι επίσης δυνατό να ελέγξετε ένα όρισμα.

Είναι δυνατό να ελέγξετε τη δομή initial σε μια συνεδρία εντοπισμού σφαλμάτων με το GEF τρέχοντας gef> p initial.

Για να εκμεταλλευτείτε αυτό, πρέπει είτε να διαρρεύσετε ή να διαγράψετε το PTR_MANGLE cookie και στη συνέχεια να αντικαταστήσετε μια εγγραφή cxa στο initial με system('/bin/sh').
Μπορείτε να βρείτε ένα παράδειγμα αυτού στην αρχική ανάρτηση στο blog σχετικά με την τεχνική.