LOAD_NAME / LOAD_CONST opcode OOB Read
Questa informazione è stata presa da questo articolo.
TL;DR
Possiamo utilizzare la funzionalità di lettura OOB nell'opcode LOAD_NAME / LOAD_CONST per ottenere alcuni simboli in memoria. Ciò significa utilizzare trucchi come (a, b, c, ... centinaia di simboli ..., __getattribute__) if [] else [].__getattribute__(...)
per ottenere un simbolo (come ad esempio il nome di una funzione) desiderato.
Poi basta creare il proprio exploit.
Panoramica
Il codice sorgente è piuttosto breve, contiene solo 4 righe!
Puoi inserire del codice Python arbitrario e verrà compilato in un oggetto codice Python. Tuttavia, co_consts
e co_names
di quell'oggetto codice verranno sostituiti con una tupla vuota prima di valutare quell'oggetto codice.
Quindi, in questo modo, tutte le espressioni che contengono costanti (ad es. numeri, stringhe, ecc.) o nomi (ad es. variabili, funzioni) potrebbero causare un errore di segmentazione alla fine.
Lettura fuori limite
Come avviene il segmentation fault?
Iniziamo con un esempio semplice, [a, b, c]
potrebbe essere compilato nel bytecode seguente.
Ma cosa succede se co_names
diventa una tupla vuota? L'opcode LOAD_NAME 2
viene comunque eseguito e cerca di leggere il valore da quell'indirizzo di memoria inizialmente previsto. Sì, questa è una "caratteristica" di lettura out-of-bound.
Il concetto principale per la soluzione è semplice. Alcuni opcode in CPython come LOAD_NAME
e LOAD_CONST
sono vulnerabili (?) alla lettura OOB.
Recuperano un oggetto dall'indice oparg
dalla tupla consts
o names
(che è come sono chiamati sotto il cofano co_consts
e co_names
). Possiamo fare riferimento al breve snippet seguente su LOAD_CONST
per vedere cosa fa CPython quando elabora l'opcode LOAD_CONST
.
In questo modo possiamo utilizzare la funzionalità OOB per ottenere un "nome" da un offset di memoria arbitrario. Per assicurarci di quale nome si tratti e qual è il suo offset, basta provare LOAD_NAME 0
, LOAD_NAME 1
... LOAD_NAME 99
... E potresti trovare qualcosa intorno a oparg > 700. Puoi anche provare ad utilizzare gdb per dare un'occhiata alla struttura della memoria ovviamente, ma non penso che sarebbe più facile?
Generazione dell'Exploit
Una volta che abbiamo recuperato quegli utili offset per i nomi / costanti, come possiamo ottenere un nome / costante da quell'offset e utilizzarlo? Ecco un trucco per te:
Supponiamo di poter ottenere un nome __getattribute__
dall'offset 5 (LOAD_NAME 5
) con co_names=()
, allora basta fare le seguenti operazioni:
Notare che non è necessario chiamarlo
__getattribute__
, puoi chiamarlo con un nome più corto o strano
Puoi capire il motivo semplicemente visualizzando il bytecode:
Si noti che LOAD_ATTR
recupera anche il nome da co_names
. Python carica i nomi dalla stessa posizione se il nome è lo stesso, quindi il secondo __getattribute__
viene comunque caricato dalla posizione=5. Utilizzando questa caratteristica possiamo utilizzare un nome arbitrario una volta che il nome è in memoria nelle vicinanze.
Per generare numeri dovrebbe essere banale:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Script di exploit
Non ho utilizzato costanti a causa del limite di lunghezza.
Ecco prima uno script per trovare quelle posizioni dei nomi.
E il seguente è per generare l'effettivo exploit Python.
Si limita fondamentalmente a fare le seguenti cose, per le stringhe che otteniamo dal metodo __dir__
:
Last updated