Stack Canary Esperimento di eliminazione

Feb 27 2023
Ispirato da Walter Harold Bishop, in questo post cercherò di trovare un modo migliore per proteggere l'integrità degli indirizzi di ritorno rispetto ai canary stack. Tieni presente che non credo che ciò che avevo in mente sia la soluzione perfetta.

Ispirato da Walter Harold Bishop, in questo post cercherò di trovare un modo migliore per proteggere l'integrità degli indirizzi di ritorno rispetto ai canary stack. Tieni presente che non credo che ciò che avevo in mente sia la soluzione perfetta. Ci sono molte cose a cui non ho ancora riflettuto a fondo. Ad ogni modo, puoi pensare a questo come "Tentativo n. 1".

Non mi piacciono i canary stack perché in realtà non risolvono la causa principale del problema. Anche la pagina di Wikipedia lo mette nella giusta prospettiva. Permettetemi di citare:

Questa tecnica può aumentare notevolmente la difficoltà di sfruttare un overflow del buffer dello stack

“Aumentare la difficoltà di sfruttamento”. Non risolve il problema. Aumenta la difficoltà di sfruttamento.

Al momento della stesura di questo post, l'anno è il 2023. Puoi ancora fare quanto segue:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *payload =
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA"
        "AAAAAAAAAAAAAAAA";
    char buffer[32];
    memcpy(buffer, payload, (int)strlen(payload));
    printf("%s\n", buffer);
}

(lldb) target create "./test"
Current executable set to '/Users/zsoltimre/DEVEL/EXPERIMENT/Virtual-CPU/test' (arm64).
(lldb) r
Process 15890 launched: '/Users/zsoltimre/DEVEL/EXPERIMENT/Virtual-CPU/test' (arm64)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Process 15890 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x19c4c4bb0)
    frame #0: 0x000000019c4c4bb0 libsystem_c.dylib`__chk_fail_overflow + 24
libsystem_c.dylib`:
->  0x19c4c4bb0 <+24>: brk    #0x1
libsystem_c.dylib`:
    0x19c4c4bb4 <+0>:  pacibsp 
    0x19c4c4bb8 <+4>:  stp    x29, x30, [sp, #-0x10]!
    0x19c4c4bbc <+8>:  mov    x29, sp
Target 0: (test) stopped.

Ho deciso di indagare e sperimentare qualcos'altro. E se rimuovessimo completamente gli indirizzi di ritorno dallo stack? Ad alto livello, significherebbe che gli aggressori non sarebbero in grado di alterare il flusso di esecuzione o mandare in crash il programma modificando l'indirizzo di ritorno. Ammetto che una soluzione del genere ha un prezzo, ma risolve il problema.

Quindi, la mia idea e la dimostrazione di concetto troppo semplificata in poche parole:

  • Gli indirizzi di ritorno sono memorizzati in un'area di memoria dedicata (RSTACK) che si comporta come lo stack. RSTACK è di sola lettura per il codice utente. Solo la CPU può eseguire il push e il pop dei valori da RSTACK. Nel PoC questo viene applicato utilizzando un array dedicato .
  • C'è un registro di collegamento dedicato (Return Pointer / RP) che punta alla parte superiore di RSTACK. Questo registro è di sola lettura per il codice utente. Solo la CPU può modificarne il valore.
  • Le istruzioni CALL* e RET* della CPU gestiscono RSTACK.
  • Nessuna delle altre istruzioni può scrivere su RSTACK.
Lo stato iniziale della CPU

Vediamo come appaiono lo stato della CPU e la memoria subito dopo l'istruzione CALL.

Quindi, se lasciamo eseguire le istruzioni dall'indirizzo 7 e corrompiamo lo stack durante l'esecuzione dell'istruzione RET finale, atterreremo esattamente dove volevamo, all'indirizzo 4. Non importa cosa facciamo con lo stack, possiamo sempre tornare e continuare l'esecuzione.

In un'applicazione reale, è abbastanza probabile che la corruzione dei valori nello stack causi altri problemi (ad esempio la divisione per zero o la dereferenza del puntatore nullo) che porterebbero al crash del programma. Pertanto, questa soluzione non risolve tutti i problemi di Denial of Service. Ma,

  • Impedisce perfettamente a un utente malintenzionato di controllare il flusso di esecuzione.
  • Anche gli attacchi di forza bruta contro il canarino vengono completamente eliminati perché non ci sono più canarini.
  • Un motivo in meno per l'arresto anomalo di un programma a causa della corruzione.

Ad ogni modo, mi sono divertito a giocare e spero che tu ti sia divertito a leggere questo post. Arrivederci alla prossima!