Si può impedire lo stack overflow premendo prima l'indirizzo di ritorno?

2

Ritengo che si tratti di domande davvero stupide e ovvie, ma non ho trovato alcuna risposta online. Voglio dire che l'esempio da manuale di buffer overflow è fatto da memcpy in un buffer di dimensioni limitate nello stack. Funziona perché la chiamata di funzione alloca il buffer prima di premere l'indirizzo di ritorno. L'overflow del buffer sovrascriverà l'indirizzo di ritorno. Sembra abbastanza ovvio provare a cambiare l'ordine premendo prima l'indirizzo di ritorno e quindi allocare lo spazio di stack. Il compilatore sa quanto spazio locale ha bisogno ogni chiamata di funzione e dovrebbe essere in grado di far saltare lo stack alla funzione return per accedere all'indirizzo di ritorno. Sembra che ciò impedirebbe facilmente un sacco di problemi di overflow?

    
posta ios learner 12.12.2018 - 07:23
fonte

1 risposta

4

I motivi principali sono che è scomodo, produce un codice non ottimale, richiede una conoscenza preliminare delle dimensioni dello stack di funzione, limita le prestazioni, viola le specifiche di molte convenzioni di chiamata e non impedisce in realtà il superamento degli overflow.

Il solito modo in cui una funzione chiama in un'altra funzione, almeno su x86, è (ovviamente) con l'istruzione call . Questa istruzione spinge l'indirizzo di ritorno nello stack e reindirizza il puntatore di istruzioni all'indirizzo di destinazione. Lo stack è un concetto che il processore stesso comprende e facilita e non possiamo modificare il comportamento del processore.

Per facilitare l'inserimento del puntatore di ritorno prima delle allocazioni dello stack, dovremmo allocare lo spazio di stack della funzione prima l'istruzione call viene eseguita. Ciò richiede la funzione di chiamata (il chiamante) per allocare lo stack per la funzione chiamata (il chiamato), e quindi richiede implicitamente che il chiamante pulisca la pila del destinatario. Questo è possibile chiamando convenzioni come cdecl (anche alcuni casi con thiscall ), che utilizzano la pulizia del chiamante, ma è una violazione delle specifiche della convenzione di chiamata per stdcall , fastcall , vectorcall , pascal e un numero di altre convenzioni.

Ci si potrebbe chiedere perché è importante che l'allocazione dello stack sia fatta dal chiamante o dal chiamante. Bene, in generale, esiste una funzione perché contiene codice che viene utilizzato in più di una posizione (questo è il motivo per cui abbiamo ottimizzato le ottimizzazioni!). In un modello di pulizia del callee, il codice per impostare lo spazio di allocazione dello stack della funzione e pulirlo alla fine fa parte della funzione di chiamata stessa. La funzione di chiamata non ha bisogno di conoscere i requisiti di memoria dello stack della funzione chiamata. Questo è utile perché non si finisce per duplicare la logica di gestione dello stack per ogni chiamata in quella funzione. Immagina, ad esempio, un programma che ha chiamato memcpy in 10.000 posizioni diverse: per la pulizia di tutto, c'è solo un'istanza del codice di gestione dello stack per la funzione memcpy , mentre per la pulizia del chiamante ci sono ora 10.000 copie. Anche se questo è solo 10 o 20 byte di codice ogni volta che sono già centinaia di kilobyte di codice ridondante.

Un altro problema è la crescita dello stack. Hai detto che il compilatore sa quanta pila userà una funzione, questo non è assolutamente vero. Le allocazioni dello stack dinamico sono abbastanza comuni e il tuo modello proposto racchiude lo stack del callee tra il frame dello stack della funzione chiamante e l'indirizzo di ritorno del callee:

| return addr | saved *bp | stackalloc space | return addr | saved bp | ...
|  (callee)   | (callee)  |     (callee)     |  (caller)   | (caller) |
 *bp+0         *bp+N       *bp+2N             *bp+?N         ...

Questo significa che se la funzione richiede che il suo spazio di stack cresca dinamicamente, deve spostare il puntatore di ritorno e il puntatore dello stack frame (salvato *bp ) quando lo fa, e anche modificare il puntatore del frame dello stack nel processo. Questo è inutilmente complesso e introduce alcuni problemi di prestazioni.

Potresti anche aver capito perché questa non è una soluzione per i buffer overflow. Superando un buffer nello spazio stackalloc non sovrascrivi l'indirizzo di ritorno del callee, ma puoi sovrascrivere l'indirizzo di ritorno del chiamante. Questo non complica davvero lo sfruttamento.

    
risposta data 12.12.2018 - 10:10
fonte

Leggi altre domande sui tag