Direzione dati inversa nello stack

4

Come ricordo dal mio corso di microcontrollore, lo stack si trova alla fine della memoria, mentre all'inizio ci sono alcuni puntatori di interrupt, codice di programma e dati successivi. Poiché lo stack si trova alla fine della memoria, cresce nella direzione verso indirizzi più piccoli. E a ogni funzione viene richiamato un nuovo stack frame. Che contiene i seguenti elementi, tra gli altri, nella seguente direzione (da piccolo indirizzo ad alto indirizzo)

local variables
return pointer
parameters

E per quanto ne so la maggior parte dei buffer overflow, prova a sovrascrivere lo spazio disponibile in una variabile locale per sovrascrivere, ad esempio il puntatore di ritorno.

Quindi un mio pensiero era, che un problema fondamentale è, che i dati sullo stack sono indirizzati allo stesso modo, quindi in altri punti della memoria. Se un puntatore a un buffer punta all'indirizzo più alto del buffer e l'ultimo elemento del buffer ha l'indirizzo più piccolo (quindi in ordine inverso), non sarebbe di aiuto?

Quindi le mie domande sono:

  • È un pensiero valido?
  • Ci sono stati pensieri simili?
  • Quali sono gli svantaggi di ciò?
posta Angelo.Hannes 17.01.2014 - 17:10
fonte

2 risposte

2

Ci sono alcune architetture che usano uno stack "crescente". In generale, questo rende il porting dei sistemi operativi un po 'più difficile; non sembra che sia molto utile in termini di sicurezza. La convenzione di far cadere gli stack giunge dall'era precedente all'avvento di MMU (senza MMU, RAM è un blocco; Lo stack cresce alla fine del blocco, gli elementi dei dati vengono allocati verso l'alto e la memoria viene esaurita quando l'heap e lo stack si incontrano).

Nel caso di overflow del buffer, succede così, empiricamente , che la maggior parte si verifica "sugli indirizzi alti": il buffer ha dimensione 1000 ma il codice buggy tenta di scrivere nel 1001 °, 1002 °. .. slot. Questo è legato al modo in cui molti programmatori implementano un loop: quando vogliono eseguire il looping dei 1000 elementi di un array, eseguono il loop up , con indice da 0 a 999, anziché il looping da 999 a 0 Per la macchina, entrambi sono equivalenti (in effetti, in alcuni vecchi computer a 8 bit, il looping era leggermente più veloce); ma i programmatori sono umani con abitudini umane, e questo include il conteggio anziché il basso, quando viene data la scelta.

Questo è importante con la direzione dello stack perché con uno stack "verso il basso", gli slot succosi che l'utente malintenzionato vuole riscrivere (in particolare l'indirizzo di ritorno) sono nel range di overflow "alti", che sono più comuni di overflow "bassi" . Tuttavia, gli overflow "bassi" (detti anche "underrun": il codice tenta di accedere agli slot -1, -2 ...) sono solo rari, non inauditi. L'inversione della direzione dello stack cambia solo i ruoli: ora, l'overflow basso diventa critico.

Inoltre, come spiega @Polynomial, anche i bassi overflow possono essere interessanti. L'indirizzo di ritorno della funzione è solo uno tra i numerosi slot che l'utente malintenzionato vorrebbe sovrascrivere.

Secondo me, il problema principale con un overflow del buffer è che si è verificato: il codice fa cose insensate e può procedere senza sosta. È come un rinoceronte evaso da uno zoo e imperversando per il centro della città; invertendo la pila è come: "spostiamo l'intera città, in modo che il rinoceronte sia più propenso a correre verso la campagna, dove farà meno danni". Non è certo un meccanismo di sicurezza "ragionevole". Sarebbe un'idea molto migliore concentrarsi sul non lasciare andare il rinoceronte.

    
risposta data 17.01.2014 - 21:07
fonte
1

Il problema principale nel far crescere lo stack è che deve sedersi all'inizio della memoria e avere un limite di dimensione superiore forzato, oppure sedersi nella parte superiore della memoria e avere una posizione forzata. Entrambe queste situazioni limitano l'utilizzo massimo della memoria. Se sia la pila che l'heap crescono l'una verso l'altra da estremità opposte, si esaurisce la memoria solo quando non ne rimane letteralmente nessuna. In altre configurazioni potresti esaurire lo stack o l'heap pur avendo memoria gratuita.

L'idea di "stack indietro" è stata lanciata da un po 'di tempo e generalmente non è vista come una soluzione solida. Per cominciare, ti imbatterai in problemi se stai modificando i buffer dello stack che si trovano nei frame precedenti. Inizialmente questo non sembra essere comune, ma considera di allocare i buffer per molte funzioni prima che le passi:

void foo(char* src)
{
    char dest[20];     // local allocated within frame of foo
    strcpy(dest, src); // new stack frame, so return ptr AFTER dest
    // blah blah rest of code...
    if (dest[0] == 'X')
        printf("First char is X\n");
}

Si noti che il frame dello stack per strcpy si posizionerà numericamente sopra il frame dello stack per foo . In un sistema tipico, strcpy scriverà oltre il buffer, sopra il puntatore di ritorno di foo , portando al controllo del puntatore di istruzioni dopo che foo ritorna. Con uno stack all'indietro, strcpy scriverà, attraverso la fine dello stack frame di foo , nella parte inferiore dello stack frame di strcpy , portando a una sovrascrittura dell'indirizzo di ritorno di strcpy , e di nuovo dando il controllo del puntatore di istruzioni.

Una soluzione considerata da molti abbastanza sicura consiste nell'utilizzare un'architettura con uno stack per locali e parametri (lo stack di dati) e un'altra per i frame dello stack e i puntatori di ritorno (lo stack di controllo). Questo isolamento garantisce che i puntatori di ritorno non possano essere sovrascritti casualmente quando un buffer locale viene sovraccaricato. Ha di nuovo problemi con la gestione della memoria, anche se solo uno stack deve avere una dimensione massima fissa - l'altro può crescere nell'heap.

Naturalmente, anche questa architettura non è completamente robusta dal punto di vista della sicurezza. Considera quanto segue:

int int_sorter( const void *val_a, const void *val_b )
{
    // this code isn't important here
    int first = *(int*)val_a;
    int second = *(int*)val_b;
    if ( first == second )
        return 0;
    else return (first < second) ? -1 : 1;
}

void bar(char* message, int (*sorter)(const void*,const void*))
{
    int array[10];
    char dest[32];
    // do something with array
    // ...
    strcpy(dest, message);
    qsort(array, 10, sizeof( int ), sorter);
}

In questo caso, supponendo una coppia di classiche pile in crescita in un'architettura a doppio stack, message sovrasta dest sullo stack di dati, copiando il puntatore di funzione sorter che è stato premuto come secondo parametro a bar . La chiamata a strcpy ritorna normalmente (lo stack di controllo è intatto), ma la chiamata a qsort contiene un'istruzione per passare alla funzione sorter , che di nuovo porta al controllo sul puntatore di istruzioni.

Alla fine della giornata, non troverai una soluzione completa al problema. Il meglio che puoi fare è usare buone pratiche di codifica (o insegnare ai tuoi sviluppatori di farlo) e abilitare protezioni come ASLR (a.k.a. PIE), DEP / NX, stack di canarini, ecc.

    
risposta data 17.01.2014 - 18:28
fonte

Leggi altre domande sui tag