Sicurezza della memoria nelle applicazioni: quando le vulnerabilità diventano un problema

Le vulnerabilità che hanno un impatto diretto sulla sicurezza della memoria rappresentano un enorme macigno per le software house, gli sviluppatori, le aziende, i professioni e gli utenti finali. Di che cosa si tratta e perché è necessario intervenire senza indugio.

Le vulnerabilità legate alla sicurezza della memoria (memory safety vulnerabilities) sono un tipo di problema che si verifica quando un programma accede erroneamente o gestisce in modo improprio la memoria del dispositivo. La memoria RAM è uno spazio fondamentale in cui vengono archiviati dati e istruzioni per l’esecuzione dei programmi. Le vulnerabilità che mettono in discussione la sicurezza delle informazioni conservate in memoria, possono portare a comportamenti indesiderati, tra cui crash delle applicazioni, errori imprevisti e, nei casi più gravi, esecuzione di codice arbitrario.

Vulnerabilità legate alla sicurezza della memoria

Ci sono diverse problematiche, delle quali abbiamo ripetutamente parlato in tanti nostri articoli, che possono portare a problemi legati alla sicurezza della memoria, alla riservatezza e all’integrità delle informazioni in essa contenute, alla stabilità e al corretto funzionamento del sistema:

  • Buffer Overflow: si verifica quando un programma scrive dati oltre i limiti di un’area di memoria (buffer), sovrascrivendo così informazioni importanti o causando un comportamento imprevisto. Un’anomali che può essere sfruttata dagli aggressori per eseguire codice dannoso.
  • Use-After-Free: è un problema che si manifesta quando un programma continua ad accedere a una regione di memoria dopo che è stata liberata. Possono verificarsi comportamenti imprevedibili, in quanto la memoria potrebbe essere stata sovrascritta con nuovi dati.
  • Null Pointer Dereference: in questo caso un programma tenta di accedere o dereferenziare un puntatore nullo (un puntatore che non punta a nessun oggetto). La dereferenziazione è fondamentale quando si lavora con puntatori, poiché consente di accedere ai dati effettivi a cui punta il puntatore. Un problema come quello indicato, può causare crash dell’applicazione o comportamenti imprevedibili.
  • Memory Leaks: sebbene non rappresentino di solito un problema di sicurezza in senso stretto, si verificano quando un programma continua ad allocare memoria senza mai liberarla, portando a una graduale esaurimento delle risorse disponibili.

Quanto è grave il problema di queste vulnerabilità

Per dare un’idea di quanto incidano le vulnerabilità legate alla sicurezza della memoria, basti tenere presente che il 70% delle problematiche scoperte ogni anno lato software da parte dei tecnici Microsoft hanno a che fare proprio con questo tema.

Google comunica dati speculari: nell’ambito del progetto Chromium che, lo ricordiamo, costituisce la base per la stragrande maggioranza dei browser Web in circolazione, il 70% dei bug di sicurezza più gravi ha a che fare proprio con vulnerabilità a livello della sicurezza della memoria. Mozilla fa eco sostenendo che la maggior parte delle problematiche di sicurezza sono proprio legate alla gestione della memoria.

Non si tratta di vulnerabilità teoriche. I problemi di sicurezza a livello di memoria individuati nei software che tutti noi usiamo giornalmente, sono utilizzati per condurre attacchi informatici nei confronti di sistemi, organizzazioni e persone reali.

Il team di Google Project Zero spiega che il 67% delle vulnerabilità zero-day utilizzate dagli aggressori per lanciare attacchi particolarmente efficaci sono proprio problemi legati alla corruzione della memoria.

Contrasto ai problemi legati alla sicurezza della memoria

Nel corso degli anni, gli ingegneri del software hanno lavorato su un gran numero di soluzioni intelligenti, utili ad attenuare il problema. Si pensi agli strumenti che randomizzano le posizioni di memoria all’interno delle quali le applicazioni salvano i dati (in modo da non fornire appigli agli aggressori), le tecniche di sandboxing, i software antiexploit e le misure di protezione integrate a livello di sistema operativo. Allo stato attuale, checché se ne dica, non esiste una soluzione che permetta di proteggersi efficacemente da qualunque potenziale tentativo di aggressione che sfrutta i problemi nella gestione della memoria.

Le software house hanno investito tempo e denaro nella formazione dei loro sviluppatori affinché evitassero operazioni di memoria non sicure. Grazie a ingenti sforzi, si sta cercando di migliorare la situazione intervenendo ad esempio sulla sicurezza del codice C/C++ esistente. In questo senso, la filosofia DevSecOps aiuta tanto imponendo la necessità di uno sviluppo e aggiornamento continuo del software, ponendo particolare accento proprio sugli aspetti legati alla sicurezza.

Il progetto Capability Hardware Enhanced RISC Instructions (CHERI), sviluppato presso l’Università di Cambridge, si pone come obiettivo quello di riesaminare scelte progettuali fondamentali nell’hardware e nel software per migliorare in modo significativo la sicurezza del sistema. La caratteristica principale di CHERI è l’estensione delle architetture di istruzioni hardware convenzionali (ISA) con nuove funzionalità architetturali per abilitare una protezione della memoria più efficace. La protezione della memoria proposta da CHERI consente a linguaggi di programmazione storicamente insicuri come C e C++ di adattarsi per fornire una protezione forte contro molte vulnerabilità oggi ampiamente sfruttate.

Software e strumenti per rilevare e prevenire exploit basati sulle vulnerabilità nella gestione della memoria

In tanti nostri articoli abbiamo descritto le metodologie e i tool utili per riconoscere eventuali problemi nelle applicazioni software. Soltanto a mo’ di promemoria, ne ricordiamo qui alcuni:

  • Static Analysis Tools: Strumenti di analisi statica del codice possono individuare potenziali problemi di sicurezza durante la fase di sviluppo del software. Esempi di strumenti di analisi statica includono Cppcheck e Clang Static Analyzer. Entrambi sono strumenti software che, guarda caso, analizzano proprio il codice C/C++ alla ricerca di diverse tipologie di errori.
  • Dynamic Analysis Tools: Diversamente rispetto ai precedenti, questi strumenti eseguono il software e analizzano il suo comportamento in tempo reale, cercando di individuare eventuali problemi. Valgrind, ad esempio, svolge un’analisi dinamica per rileva la gestione errata della memoria e problemi come buffer overflow e memory leak.
  • Fuzz Testing: Il fuzzing è una tecnica di testing che consiste nel passare a un programma “target” dati di input casuali per individuare eventuali errori e comportamenti anomali.
  • Runtime Protection: Uno strumento come ASLR (Address Space Layout Randomization), ben noto agli utenti di Windows, introduce casualità nelle posizioni della memoria via via utilizzate, rendendo più difficile per gli attaccanti sfruttare eventuali vulnerabilità. Si affianca a DEP (Data Execution Prevention) che impedisce l’esecuzione di codice in alcune aree di memoria. La ratio è quella di contribuire a mitigare attacchi basati sulla sovrascrittura del contenuto della memoria.
  • Security Auditing: Sono strumenti software progettati per svolgere un audit del codice ovvero una verifica alla ricerca di vulnerabilità. OWASP Dependency-Check, per esempio, è un utile strumento che identifica le dipendenze di terze parti nei programmi alla ricerca di vulnerabilità note nelle librerie e nei componenti aggiuntivi.

Il ruolo dei linguaggi di programmazione moderni e di Rust, per superare gli storici problemi di C/C++

La maggior parte dei linguaggi di programmazione moderni diversi da C/C++ offrono già una maggiore sicurezza rispetto alle problematiche della gestione della memoria. Nel 2006, un ingegnere informatico di Mozilla iniziò a lavorare su un nuovo linguaggio di programmazione chiamato Rust. La versione 1.0 di Rust è stata annunciata ufficialmente nel 2015. Da allora, diverse importanti organizzazioni di software hanno iniziato a utilizzarla nei propri sistemi, tra cui Amazon, Facebook, Google, Microsoft, Mozilla e molti altri. È inoltre supportato nello sviluppo del kernel Linux e del kernel Windows. Perché? Perché Rust è progettato per fornire un alto livello di sicurezza della memoria, contrastando i problemi tradizionalmente associati a linguaggi come C e C++.

Le caratteristiche che rendono Rust una soluzione sicura per lo sviluppo software

Alcune caratteristiche che rendono Rust una soluzione eccellente per contrastare i problemi legati alla sicurezza della memoria:

  • Ownership e Borrowing: Rust utilizza un sistema che controlla l’accesso alla memoria a runtime. Ogni valore in Rust ha un proprietario, e solo il proprietario può modificarlo o deallocarlo. Questo aiuta a prevenire problemi come memory leak e use-after-free.
  • Gestione dei tipi: Il sistema di tipizzazione alla base di Rust è potente ed espressivo. Il compilatore Rust controlla la correttezza dei tipi a runtime, evitando molte categorie di errori comuni, come buffer overflow e buffer underrun.
  • Nessun problema di Null Pointer Dereference: In Rust, il concetto di puntatore nullo non esiste. Rust garantisce che i puntatori possano essere dereferenziati solo quando sono sicuri e validi.
  • Borrow Checker: Si tratta di un meccanismo che analizza il codice per garantire che non vi siano violazioni delle regole di ownership e borrowing. Evita potenziali problemi di sincronizzazione e race condition. I problemi di sincronizzazione si verificano quando ci sono più thread o processi che accedono e manipolano dati condivisi in modo concorrente, e non c’è una sincronizzazione appropriata tra di essi. Una race condition si verifica quando il comportamento di un programma dipende dall’ordine di esecuzione delle operazioni tra thread o processi concorrenti: il risultato del programma può variare a seconda di quale thread completa la sua esecuzione per primo.
  • Safe e Unsafe Code: Rust consente di scrivere sia codice “safe” che “unsafe”. Il codice sicuro deve rispettare le regole in termini di tipizzazione e ownership di Rust, mentre il codice insicuro può essere utilizzato in modo controllato quando necessario. Un approccio intelligente che bilancia la sicurezza e la flessibilità in base alle esigenze di progetto.
  • Scoped Ownership: Il sistema di ownership di Rust è basato su blocchi di codice (si dice che è scope-based): questo significa che il ciclo di vita di un valore è limitato al blocco di codice in cui esso è dichiarato. Ciò semplifica la gestione della memoria, poiché la deallocazione avviene automaticamente alla fine della gestione di ogni singolo blocco.
  • Zero-Cost Abstractions: Rust offre astrazioni ad alto livello senza alcun costo aggiuntivo a livello di esecuzione. È quindi possibile scrivere codice sicuro e pulito senza sacrificare le prestazioni.

In conclusione

Prodotti software diversi, in ogni caso, necessitano di strategie differenti per attenuare o azzerare gli effetti del codice non sicuro. L’unica cosa che i produttori di software non possono fare, tuttavia, è ignorare il problema. L’industria del software non deve insomma restare più alla finestra e agire proattivamente per gestire un problema che è sempre più attuale: è insomma necessario promuovere sempre più il concetto di security-by-design.

Credit immagine in apertura: iStock.com/da-kuk

Ti consigliamo anche

Link copiato negli appunti