LOAD_NAME / LOAD_CONST opcode OOB Read
Αυτές οι πληροφορίες προήλθαν από αυτήν την ανάλυση.
TL;DR
Μπορούμε να χρησιμοποιήσουμε τη δυνατότητα OOB read στον κωδικό LOAD_NAME / LOAD_CONST για να πάρουμε ορισμένο σύμβολο από τη μνήμη. Αυτό σημαίνει ότι χρησιμοποιούμε κόλπο όπως (a, b, c, ... εκατοντάδες σύμβολα ..., __getattribute__) if [] else [].__getattribute__(...)
για να πάρουμε ένα σύμβολο (όπως το όνομα μιας συνάρτησης) που θέλουμε.
Στη συνέχεια απλά δημιουργήστε την εκμετάλλευσή σας.
Επισκόπηση
Ο πηγαίος κώδικας είναι αρκετά σύντομος, περιέχει μόνο 4 γραμμές!
Μπορείτε να εισάγετε αυθαίρετο κώδικα Python και θα μεταγλωττιστεί σε ένα αντικείμενο κώδικα Python. Ωστόσο, τα co_consts
και co_names
αυτού του αντικειμένου κώδικα θα αντικατασταθούν με ένα κενό tuple πριν από την αξιολόγηση αυτού του αντικειμένου κώδικα.
Έτσι, όλες οι εκφράσεις που περιέχουν σταθερές (π.χ. αριθμούς, συμβολοσειρές κλπ.) ή ονόματα (π.χ. μεταβλητές, συναρτήσεις) μπορεί να προκαλέσουν σφάλμα σεγματοποίησης (segmentation fault) στο τέλος.
Ανάγνωση εκτός ορίων (Out of Bound Read)
Πώς συμβαίνει το σφάλμα σεγματοποίησης;
Ας ξεκινήσουμε με ένα απλό παράδειγμα, [a, b, c]
μπορεί να μεταγλωττιστεί στον ακόλουθο κώδικα bytecode.
Αλλά τι γίνεται αν τα co_names
γίνουν ένα άδειο tuple; Το opcode LOAD_NAME 2
εξακολουθεί να εκτελείται και προσπαθεί να διαβάσει την τιμή από τη διεύθυνση μνήμης που θα έπρεπε αρχικά να βρίσκεται. Ναι, αυτό είναι ένα "χαρακτηριστικό" out-of-bound read.
Η βασική έννοια για τη λύση είναι απλή. Ορισμένα opcodes στο CPython, όπως το LOAD_NAME
και το LOAD_CONST
, είναι ευάλωτα (?) σε OOB read.
Ανακτούν ένα αντικείμενο από τον δείκτη oparg
από το tuple consts
ή names
(αυτό που ονομάζεται co_consts
και co_names
κάτω από το καπό). Μπορούμε να ανατρέξουμε στο παρακάτω σύντομο απόσπασμα για το LOAD_CONST
για να δούμε τι κάνει το CPython όταν επεξεργάζεται το opcode LOAD_CONST
.
Με αυτόν τον τρόπο μπορούμε να χρησιμοποιήσουμε το χαρακτηριστικό OOB για να πάρουμε ένα "όνομα" από την αυθαίρετη μνήμη. Για να βεβαιωθούμε για το ποιο όνομα έχει και ποια είναι η μετατόπισή του, απλά δοκιμάζουμε τα LOAD_NAME 0
, LOAD_NAME 1
... LOAD_NAME 99
... Και μπορείτε να βρείτε κάτι για oparg > 700. Μπορείτε επίσης να δοκιμάσετε να χρησιμοποιήσετε το gdb για να ρίξετε μια ματιά στη διάταξη της μνήμης φυσικά, αλλά δεν νομίζω ότι θα ήταν πιο εύκολο;
Δημιουργία της Εκμετάλλευσης
Αφού ανακτήσουμε αυτές τις χρήσιμες μετατοπίσεις για ονόματα / σταθερές, πώς μπορούμε να πάρουμε ένα όνομα / σταθερά από αυτήν τη μετατόπιση και να το χρησιμοποιήσουμε; Εδώ έχουμε ένα κόλπο για εσάς:
Ας υποθέσουμε ότι μπορούμε να πάρουμε ένα όνομα __getattribute__
από τη μετατόπιση 5 (LOAD_NAME 5
) με co_names=()
, τότε απλά κάντε τα εξής:
Παρατηρήστε ότι δεν είναι απαραίτητο να το ονομάσετε ως
__getattribute__
, μπορείτε να το ονομάσετε με κάτι πιο σύντομο ή πιο παράξενο.
Μπορείτε να κατανοήσετε τον λόγο πίσω από αυτό απλά παρακολουθώντας το bytecode του:
Παρατηρήστε ότι το LOAD_ATTR
ανακτά επίσης το όνομα από το co_names
. Το Python φορτώνει ονόματα από την ίδια θέση αν το όνομα είναι το ίδιο, οπότε το δεύτερο __getattribute__
φορτώνεται ακόμα από τη θέση offset=5. Χρησιμοποιώντας αυτήν τη δυνατότητα, μπορούμε να χρησιμοποιήσουμε οποιοδήποτε όνομα αφού το όνομα βρίσκεται στη μνήμη κοντά.
Για τη δημιουργία αριθμών θα πρέπει να είναι απλό:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Σενάριο Εκμετάλλευσης
Δεν χρησιμοποίησα σταθερές λόγω του ορίου μήκους.
Πρώτα, εδώ υπάρχει ένα σενάριο για να βρούμε αυτές τις θέσεις των ονομάτων.
Και το παρακάτω είναι για τη δημιουργία του πραγματικού εκμεταλλευτή Python.
Βασικά, κάνει τα εξής πράγματα, για εκείνες τις συμβολοσειρές που τις παίρνουμε από τη μέθοδο __dir__
:
Last updated