Come trovare $ 10 milioni leggendo la Blockchain

Due settimane fa, un appassionato di Golem e un titolare di GNT hanno segnalato uno strano bug relativo alla transazione di trasferimento GNT. Dopo aver esaminato i dati allegati alla transazione, ho scoperto che doveva esserci un problema nel modo in cui lo scambio stava preparando i dati per la transazione. "Oh no", ho pensato, "questo bug potrebbe essere utilizzato per svuotare l'intero account GNT sullo scambio!" E un numero piuttosto elevato di token è stato memorizzato lì!

Il bug era davvero colpa dello scambio, ma era anche correlato al modo in cui i contratti Ethereum vedono i dati di input della transazione e l'ABI di Solidity (ad esempio il modo in cui i metodi dei contratti di Solidity codificano e decodificano gli argomenti). Quindi, ovviamente, non era specifico per GNT, ma per tutti i token ERC20, così come per altri contratti che hanno metodi simili al trasferimento. Sì, hai letto bene: questo potrebbe potenzialmente funzionare per qualsiasi token basato su Ethereum elencato in detto scambio, se solo i prelievi fossero gestiti allo stesso modo di GNT. Non sappiamo che sia così, ma supponiamo che sia molto probabile.

Ethereum Contract ABI

I contratti Raw Ethereum non hanno né metodi né funzioni. I metodi sono caratteristiche di linguaggi di alto livello come Solidity e usano l'ABI del contratto Ethereum per specificare come il bytecode di un contratto è diviso in metodi, nonché come diversi tipi di argomenti sono codificati nei dati di input della transazione. (Vedi https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI per un riferimento.)

Per invocare il metodo di trasferimento (indirizzo a, uint v) del contratto GNT per trasferire 1 GNT a indirizzo 0xabcabcabcabcabcabcabcabcabcabcabcabcabca è necessario includere 3 dati:

  • 4 byte, essendo l'id del metodo: a9059cbb
  • 32 byte, con l'indirizzo di destinazione (20 byte) riempito con zeri iniziali: 000000000000000000000000ababababababababcabcabcabcabcabcabcabcabcabca
  • 32 byte, essendo il valore da trasferire, 1 * 10¹⁸ GNT: 000000000000000000000000000000000000000000000000000de0b6b3a7640000

La transazione completa sarebbe quindi simile a: a9059cbb00000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca00000000000000000000000000000000000000000000000000000de0b6b3a7640000.

I dati di input della transazione sono infiniti

Questo è uno degli aspetti più disordinati della macchina virtuale Ethereum, ma è fondamentale per comprendere appieno il problema. L'EVM può leggere i byte di ogni dato offset dei dati di input usando CALLDATALOADopcode. Se i dati in questo offset non vengono forniti nella transazione dal creatore della transazione, EVM riceverà zero come risposta. Allo stesso tempo, il contratto è in grado di verificare la lunghezza reale dei dati di input della transazione forniti con il codice operativo CALLDATASIZE.

Il bug

Il servizio di preparazione dei dati per i trasferimenti di token presupponeva che gli utenti immettessero indirizzi lunghi a 20 byte, ma la lunghezza degli indirizzi non è stata effettivamente verificata. Nella transazione di cui sopra, l'utente ha inserito un indirizzo non valido di lunghezza inferiore: 79735. I dati risultanti sono stati malformati perché l'argomento address ha richiesto 14,5 byte (12 byte per zero iniziali + 4,5 byte dall'input dell'utente). Per essere precisi, i dati delle transazioni andavano bene per la piattaforma Ethereum in quanto non si preoccupava dei dati inclusi nelle transazioni tranne l'applicazione della commissione per ogni byte. L'unico motivo per cui il trasferimento di token non è stato eseguito dal contratto GNT era che l'importo nella transazione era ridicolmente elevato (superiore alla fornitura totale e ovviamente superiore al saldo dell'indirizzo in questione). Il proprietario dell'indirizzo è stato davvero abbastanza fortunato in quanto l'utente ha usato una stringa così breve per l'indirizzo: con un po 'di (sfortuna) fortuna, l'utente sarebbe stato in grado di * svuotare casualmente * l'indirizzo di tutti i GNT e inviarli a un numero casuale indirizzo. Questo è quando ci siamo resi conto che il bug poteva anche essere usato per l'attacco ed era molto serio.

Il possibile attacco

Come avrai notato, consentire a un utente di inserire un indirizzo di trasferimento più breve sposta il valore "quantità di token da trasferire" a sinistra, aumentando il valore. È anche molto facile trovare una chiave privata per un indirizzo Ethereum con zeri alla fine dell'indirizzo, ad es. 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000.

Pertanto, il proprietario di questo indirizzo può inserire 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (saltando zeri) nell'interfaccia del servizio. L'aggressore potrebbe quindi ordinare un trasferimento di un certo valore X dal servizio all'indirizzo malformato inserito. Ciò causerebbe effettivamente un trasferimento di un valore spostato di 16 bit, ovvero 65536 volte più grande di X, sul conto Ethereum dell'attaccante!

Cosa abbiamo fatto al riguardo?

Una volta identificato il possibile attacco, abbiamo contattato lo scambio e li abbiamo informati del bug. È stato un processo sorprendentemente difficile e fastidioso; il nostro CEO Julian ha ricevuto una chiamata con una linea di supporto il cui rappresentante non voleva ascoltare, e ha continuato a gridare che i bug non sono affari suoi e si è rifiutato di reindirizzarci ulteriormente nella catena di comando. Alla fine, comunque, dopo un paio d'ore, Alex è riuscito a metterci al livello del CEO e il nostro messaggio è passato. Dopo aver sentito la conferma che il bug è stato corretto, abbiamo contattato altri scambi. Sebbene non avessimo motivo di presumere che fossero vulnerabili, non avevamo nemmeno motivo di assumere il contrario. Mentre dobbiamo ammettere che non abbiamo testato che per altri scambi o altri token, siamo rimasti scioccati e un po 'terrorizzati nel renderci conto delle potenziali conseguenze di qualcuno che sfrutta quel bug per più token su più scambi: l'intera economia token di Ethereum e l'ecosistema di avvio potrebbe essere arretrato di anni.

Cosa può fare Ethereum al riguardo?

Anche se non credo che gli sviluppatori di Ethereum possano fare molto di più che continuare a educare il pubblico su come funziona l'Ethereum, potremmo suggerire ulteriori controlli aggiunti nelle future versioni di Solidity, ad esempio convalidando che la lunghezza dei dati di input della transazione corrisponde al dati previsti per il metodo di contratto indicato.

Cosa dovrebbero assolutamente fare gli scambi al riguardo?

  1. Verifica l'input dell'utente nel modo più rigoroso possibile. Il semplice controllo della lunghezza di un indirizzo fornito da un utente li protegge dall'attacco descritto. Inoltre, convalida il checksum dell'indirizzo Ethereum, se disponibile (vedi EIP55), o accetta indirizzi esclusivamente con checksum. Ciò aumenta sia la sicurezza che la facilità d'uso.
  2. Assicurarsi che i dati della transazione siano codificati correttamente.
  3. I dati di transazione generati potrebbero anche essere analizzati e confrontati con l'input dell'utente.
  4. Controlla se altri parametri come gas, prezzo del gas e l'indirizzo di destinazione della transazione generata corrispondono ai valori previsti.