Attacchi tramite dati esterni e mezzi per affrontarli

Attacchi tramite dati esterni e mezzi per affrontarli

Per cominciare, vale la pena ricordare cos'è la vulnerabilità e perché non ci si dovrebbe fidare dei dati ricevuti dall'esterno. Le vulnerabilità sono semplici bug che ti renderanno famoso su Internet. Più formalmente:è un difetto di sistema, che consente di interromperne intenzionalmente l'integrità, causare malfunzionamenti, rimuovere o modificare dati privati. Ovviamente, dovresti proteggere il tuo software da vulnerabilità di questo tipo con tutti i mezzi.

Dati pericolosi

Una delle scappatoie attraverso le quali un intruso può utilizzare negativamente il tuo sistema sono i dati, che provengono dall'esterno, più precisamente:un'eccessiva fiducia per questi dati. Ciò potrebbe esprimersi, ad esempio, in assenza di controllo per soddisfare determinati requisiti che garantiscono il corretto funzionamento del programma.

Come possiamo commettere un errore del genere per utilizzare dati esterni senza un controllo? Tutti conoscono la semplice verità - "prima prova - poi usa".

Ci sono alcune spiegazioni ovvie:

  • Il fattore umano in azione:si è dimenticato di eseguire un controllo. Gli sviluppatori si sono stancati alla fine della giornata, hanno pensato a una tazza di espresso aromatizzato mentre scrivevano il codice, si sono presi un momento per guardare una foto divertente inviata da un collega e 1001 ragioni.
  • Eccesso di fiducia. "No, beh, qui arriveranno sempre solo dati corretti" - aveva pensato uno sviluppatore, e due mesi dopo il prossimo rilascio vide la descrizione online di CVE con un codice sospettosamente familiare...
  • Controllo insufficiente. È importante controllare non solo il fatto che i dati siano stati ricevuti, ma anche prestare attenzione a cosa esattamente è stato ricevuto.
  • Test insufficienti. Qui non c'è nemmeno niente da descrivere - uno sviluppatore ha fatto intervenire potboiler / fattore umano (ascolta, è una bella scusa!), come risultato - una parte del codice è rimasta non testata. Un problema più globale potrebbe essere:livello insufficiente di test del software in generale, torneremo più avanti su questo problema.
  • Mancanza di competenza. Un programmatore può semplicemente non essere a conoscenza di alcune cose che rendono il codice vulnerabile. Ad esempio, non tutti sono consapevoli del fatto che un compilatore a volte ha il diritto di rimuovere la chiamata al memset funzione, in esecuzione per cancellare i dati privati ​​(una chiamata del genere potrebbe assomigliare a questa - memset(privateBuf, 0, bufSize) ). Questa situazione è considerata in dettaglio nell'articolo "Cancellazione sicura dei dati privati".
  • Introduzione deliberata di codice dannoso. Un commit che apparentemente contiene correzioni utili/nuove funzionalità, ma aggiunge proprio quella scappatoia, che può quindi essere utilizzata in seguito. Cosa è pericoloso:è il caso in cui un intruso non solo conosce l'esistenza della vulnerabilità, ma conosce anche le modalità del suo sfruttamento.
  • E così via.

Per quanto riguarda le fonti dei dati errati, tutto dovrebbe essere chiaro. Questi sono i dati ricevuti dal server, input dell'utente, file esterni, variabili di ambiente, ecc.

Per capire meglio come appaiono le situazioni problematiche, è meglio mettere gli occhi su esempi di vulnerabilità reali.

Prima prova, poi fidati

Iniziamo con una vulnerabilità abbastanza nota trovata in OpenSSL CVE-2014-0160, nota anche come Heartbleed. Questo è un fatto interessante che la vulnerabilità è stata aggiunta al repository OpenSSL nel dicembre 2011 e chiusa solo nell'aprile 2014. Al momento della segnalazione della vulnerabilità, il numero di siti Web vulnerabili attaccabili era terrificante e costituiva mezzo milione, pari a circa il 17% di siti Web protetti.

L'errore contenuto nell'estensione per TSL - Heartbeat. Senza entrare nei dettagli, notiamo che durante il lavoro un client e un server si scambiavano costantemente pacchetti di lunghezza casuale e mantenevano una connessione nello stato attivo. La query consisteva in payload e nella sua lunghezza.

Il problema era che formando una richiesta errata in cui la lunghezza del carico utile specificata supera la sua lunghezza effettiva, era possibile ottenere informazioni private durante la risposta, poiché durante la generazione della risposta non veniva verificata la corrispondenza delle lunghezze effettive e specificate. Pertanto, è stato possibile leggere dati da RAM di dimensioni fino a 64 Kb per richiesta. Molti dati dalla memoria potrebbero essere letti utilizzando la ripetizione multipla dello sfruttamento degli errori.

Il codice confuso sembrava il seguente:

int tls1_process_heartbeat(SSL *s)
{
  unsigned char *p = &s->s3->rrec.data[0], *pl;
  unsigned short hbtype;
  unsigned int payload;
  unsigned int padding = 16; /* Use minimum padding */
  /* Read type and payload length first */
  hbtype = *p++;
  n2s(p, payload);
  pl = p;
  ....
}

Come accennato in precedenza, il numero di byte per la richiesta di restituzione è stato copiato in base al valore del payload , anziché la lunghezza effettiva del carico utile.

memcpy(bp, pl, payload);

Il problema è stato risolto aggiungendo due controlli.

Il primo è stato il controllo che la lunghezza del carico utile non fosse nulla. Il messaggio è stato semplicemente ignorato, se la lunghezza del carico utile era zero.

if (1 + 2 + 16 > s->s3->rrec.length)
  return 0;

Il secondo controllo consisteva nel verificare se il valore della lunghezza specificata corrispondeva alla lunghezza effettiva del carico utile dei dati. In caso contrario, la richiesta viene ignorata.

if (1 + 2 + payload + 16 > s->s3->rrec.length)
  return 0;

Alla fine, dopo aver aggiunto gli opportuni controlli, il codice di lettura dei dati è diventato il seguente:

/* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
  return 0;
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
  return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;

Un'altra vulnerabilità ha un identificatore CVE 2017-17066, noto anche come GarlicRust. È stato trovato nei progetti Kovri e i2pd - implementazioni I2P in C++ e ha portato a una perdita di dati dalla RAM durante l'invio di messaggi appositamente predisposti (non ti ricorda qualcosa?). L'ironia è che in questo caso il controllo necessario era in codice, ma è stato eseguito solo dopo aver inviato la risposta.

In i2pd la vulnerabilità è stata chiusa diverse ore dopo aver ricevuto informazioni al riguardo e la correzione è stata inclusa nella versione 2.17. Nel caso di Kovri, la correzione è stata eseguita nel ramo principale su GitHub.

Il codice del problema (ridotto) è riportato di seguito:

void GarlicDestination::HandleGarlicPayload(
    std::uint8_t* buf,
    std::size_t len,
    std::shared_ptr<kovri::core::InboundTunnel> from)
{
  ....
  // Message is generated and sent until performing
  // the necessary check
  if (tunnel) {
    auto msg = CreateI2NPMessage(buf, 
      kovri::core::GetI2NPMessageLength(buf), from);
    tunnel->SendTunnelDataMsg(gateway_hash, gateway_tunnel, msg);
  } else {
    LOG(debug)
      << "GarlicDestination: 
          no outbound tunnels available for garlic clove";
  }
  ....
  // Check in progress. After the message has been
  // sent
  if (buf - buf1  > static_cast<int>(len)) {
    LOG(error) << "GarlicDestination: clove is too long";
    break;
  }
  ....
}

Non sarà difficile trovare altre vulnerabilità che derivano dall'eccessiva fiducia nei confronti dei dati esterni e dalla mancanza di controlli. Prendi almeno alcune vulnerabilità da OpenVPN. Ma non ci soffermeremo più qui:vediamo quanto ti costerà la correzione di tali errori e come affrontarli.

$$$

È un dato di fatto che più a lungo un errore rimane nel codice, maggiore è la complessità e il costo della sua correzione. Per quanto riguarda i difetti di sicurezza, qui tutto è più critico. Sulla base dei dati del National Institute of Standards and Technology of the United States (NIST), il team di PVS-Studio ha creato un'immagine che mostra il costo delle correzioni di sicurezza nelle diverse fasi del ciclo di vita del software.

Gli unicorni laboriosi e il sole che ride sono molto belli, finché non presti attenzione alle figure. L'immagine illustra perfettamente l'affermazione all'inizio della sezione:prima viene trovato l'errore, meglio è (e meno costoso).

Si noti che le cifre elencate sono nella media. Alcuni difetti di sicurezza non producono effetti evidenti e vengono eliminati solo gradualmente. Altri diventano noti a tutta Internet e subiscono perdite per milioni di dollari. È una fortuna del sorteggio... O meglio una sfortuna.

Modi per proteggersi da dati dannosi

A poco a poco, dopo aver accertato le cause e le fonti dei dati pericolosi, oltre ad aver esaminato l'aspetto pratico del codice non sicuro/non sicuro, arriviamo alla domanda di base:come proteggere l'applicazione?

La risposta è ovvia:controllare i dati esterni. Tuttavia, come abbiamo considerato sopra, la semplice conoscenza di questo fatto non è sufficiente. Quindi, sarebbe bello adottare misure aggiuntive per identificare tali luoghi.

Si dovrebbe capire che c'è una linea sottile tra errori semplici e vulnerabilità - ricordate se non altro la vulnerabilità critica CVE-2014-1266 di iOS (anche se sembra molto innocua - solo due operatori goto uno per uno). Ecco perché è così importante concentrarsi sul miglioramento della qualità del software in generale. In questo articolo, ci concentreremo su due tecniche automatizzate di controllo del codice:analisi statica e fuzzing.

Sfocato

Il fuzzing è una tecnica di test, consistente nel passare all'applicazione dati errati/imprevisti/casuali e nel tracciare il comportamento del sistema. Se durante il test fuzzing il sistema ha riattaccato / bloccato / si è comportato in modo errato, questa è un'indicazione di un errore.

A differenza dell'analisi statica, il fuzzing identifica i problemi che si verificano esattamente durante il lavoro dell'applicazione. In altre parole, tale approccio è privo di falsi allarmi. E questo è il grande vantaggio.

Ma, naturalmente, un tale approccio presenta diversi svantaggi:vengono analizzate solo le interfacce disponibili (eseguibili), sono necessarie prestazioni di più programmi con diversi insiemi di dati. È anche importante ricordare di predisporre un ambiente speciale per la sfocatura, per non danneggiare accidentalmente quello principale/di lavoro.

Analisi statica

La ricerca di vulnerabilità / errori nel codice utilizzando l'analisi statica passa dalla ricerca del codice del programma senza eseguire programmi. Il lato negativo dell'analisi statica è la presenza di falsi allarmi (vale la pena notare che il numero di essi può essere ridotto da una corretta configurazione dell'analizzatore). I vantaggi:la copertura dell'intera base di codice, non è necessario eseguire l'applicazione, generare dati in input.

Pertanto, l'analisi statica è un buon candidato per la ricerca di dati pericolosi, dal punto di vista che è possibile rilevare il problema prima (quindi più economico da risolvere) e non richiede set di dati di input. Hai scritto il codice del problema, eseguito la build del progetto, quindi l'analizzatore statico si è avviato automaticamente e hai detto:"Amico, prendi i dati dall'esterno e li usi qui. E chi controllerà?"

Sebbene l'analisi statica sia in genere utilizzata per diagnosticare gli errori in generale, il team dell'analizzatore statico di PVS-Studio si è recentemente interessato all'argomento della ricerca delle vulnerabilità e sta attualmente lavorando a una soluzione per rilevare l'uso di dati contaminati senza un controllo preventivo.

Analisi statica o dinamica?

Molto probabilmente, hai una domanda - cosa è meglio usare - analisi statica o fuzzing? La risposta è semplice:entrambi. Non si escludono a vicenda, ma sono mezzi complementari, ognuno con i propri vantaggi e svantaggi. Gli analizzatori dinamici funzionano a lungo, ma colpiscono nel segno, statici:lo fanno significativamente più velocemente, ma a volte sbagliano un colpo. Gli analizzatori dinamici sono in grado di identificare quegli errori che non sono così facili da rilevare dall'analizzatore statico. Ma è altrettanto vero il contrario!

Se osservi il ciclo di vita dello sviluppo della sicurezza Microsoft, puoi vedere che include sia l'analisi statica (fase di implementazione) sia il fuzzing (fase di verifica).

La morale è semplice:entrambe le tecniche rispondono alla domanda "Cos'altro posso fare per migliorare la qualità del software?" e per un effetto migliore usali insieme.

Conclusione

Non fidarti il ​​più possibile dei dati provenienti dall'esterno. Controlla non solo il fatto di ricevere dati, ma guarda anche cosa hai ricevuto esattamente. Utilizza strumenti automatizzati per cercare luoghi, lavorando con dati esterni senza verificarli. E poi, forse, la tua applicazione potrà diventare famosa in un modo migliore rispetto alla menzione nell'elenco di CVE.