16 KiB
WWW2Exec - atexit(), TLS Storage & Άλλοι μπερδεμένοι Δείκτες
{% hint style="success" %}
Μάθετε & εξασκηθείτε στο Hacking του AWS:Εκπαίδευση HackTricks AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο Hacking του GCP: Εκπαίδευση HackTricks GCP Red Team Expert (GRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα χάκερ υποβάλλοντας PRs στα HackTricks και HackTricks Cloud αποθετήρια του github.
Δομές __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
link_map
Όπως εξηγείται σε αυτήν την ανάρτηση, Αν το πρόγραμμα εξέρχεται χρησιμοποιώντας 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 σχετικά με την τεχνική.