Estrazione di chiavi AES da App Android

Nei giorni scorsi ho dovuto analizzare i sistemi di sicurezza implementati in un’App iOS/Android. 

L’App – di cui ho tolto i riferimenti per rispettare NDA – permette a utenti registrati di interagire tra loro inviandosi messaggi privati, file, videochiamate, e il mio scopo era capire il livello di sicurezza delle informazioni degli utenti:

  • Può un utente accedere alle informazioni (dati personali, contenuti, media) di altri?
  • È possibile leggere o eseguire dump dei dati presenti nel backend?
  • È possibile eseguire azioni per conto di altri utenti, scavalcando il sistema di permessi?

In questo articolo vi voglio raccontare della procedura utilizzata, che per forza di cose non sarà identica per tutte le App che analizzerete, ma può:

  • essere un punto di partenza per capire come muovervi 🙂
  • fare da vademecum sugli strumenti da utilizzare
  • darvi qualche “insight” su cosa cercare e dove

Tornando a noi, mi sono quindi focalizzato sull’analisi dei protocolli usati dall’app, e il primo step è stato quello di un Man In The Middle attack per sniffare il traffico tra app e backend.

Per fare questo, ho utilizzato:

  • Uno smartphone Android * (vedi note)
  • Il mio Mac, utilizzato in questo caso come router abilitando la Condivisione internet. La scheda wifi utilizzata come “Access Point” a cui collegare lo smartphone Android, e la rete cablata come WAN.
Condivisione Internet
Burp Suite Community Edition

Alcune note sugli strumenti scelti * : 

Sono principalmente un Mac e iOS user, infatti i primi test sull’app li ho fatti tramite iOS.
Tuttavia, su iOS è molto complicato decompilare un’app: tutte le app disponibili su Apple Store sono cifrate, e le chiavi di crittografia sono conservate nei secure enclave di iPhone/iPad. L’unico modo per decompilare un’app iOS è quindi avere il sorgente decrittato – recuperabile tramite un dump diretto dalla memoria di iOS, procedura possibile solo su iPhone Jailbroken.

BURP SUITE

Imposteremo Burp Suite per un man in the middle attack, se sai già come impostarlo puoi saltare questo paragrafo

Burp Suite permette di attivare un proxy solo su porte specifiche, e non su un range-wildcard. 

Va configurato come da seguente immagine, quindi port 8080, bind to all interfaces, “invisible” e “per-host” certificate

Burp Suite Proxy

Per ovviare al discorso delle porte e non creare decine di regole, ho modificato il file /etc/pf.conf del mio Mac come segue

scrub-anchor "com.apple/*"
nat-anchor "com.apple/*"
rdr-anchor "com.apple/*"
rdr on bridge100 inet proto tcp to any port 1:65535 -> 127.0.0.1 port 8080
dummynet-anchor "com.apple/*"
anchor "com.apple/*"
load anchor "com.apple" from "/etc/pf.anchors/com.apple"

La riga incriminata è quella in grassetto, e praticamente dice: su bridge100 (l’interfaccia di rete che viene creata attivando la Condivisione Internet), tutti i pacchetti tcp che vengono ricevuti su tutte le porte – range 1:65535 – devono essere ruotati su localhost alla porta 8080 (quella che usiamo su Burp Suite come proxy)

A questo punto possiamo dare un:

sudo pfctl -f /etc/pf.conf
sudo pfctl -e

per ricaricare le regole di packet filtering e rendere attivo il file appena modificato.

Rimane solo una cosa da fare: Burp Suite può fare Man In The Middle su SSL, a patto che:

  • l’app da analizzare non usi Certificate Pinning ( https://security.stackexchange.com/questions/29988/what-is-certificate-pinning )
  • ci sia pieno controllo del device target, quindi sia possibile installare un certificato Self Signed e impostarlo come trusted. In questo modo Burp Suite inietterà nelle request un suo certificato, decripterà il payload per presentarvelo in chiaro, e poi lo reencrypterà prima di inviarlo al backend, e viceversa.

Una volta avviato Burp Suite, sarà sufficiente collegarsi dallo Smartphone Android a http://burp e seguire la procedura indicata per l’installazione e trust del certificato SSL ( https://support.portswigger.net/customer/portal/articles/1783087-installing-burp-s-ca-certificate-in-firefox )

Se abbiamo fatto tutto correttamente, vedremo riempirsi la history HTTP (e nel caso dell’app che ho analizzato, anche websocket)

Burp Calls
Burp Websocket

ANALISI

Dopo un po’ di di analisi dei payload transitanti su Burp Suite, ho notato che all’invio di un messaggio chat, seguivano alcuni messaggi websocket, quindi mi sono focalizzato su quelli.

Peccato che l’output fosse incomprensibile

Output

Per questo motivo, dopo un po’ di tentativi di “deoffuscamento”, ho cambiato strategia e mi sono focalizzato sul decompilare l’APK.

Ho deciso di scaricare l’APK tramite questo sito: https://m.apkpure.com/e l’ho dato in pasto a Jadx ( https://github.com/skylot/jadx ), un decompilatore Java.

Dopo un po’ di ricerche nel codice parzialmente offuscato, sono approdato su una classe chiamata Crypto

Decompiled

e in particolare i metodi m36022a() e m36020a() che rispettivamente si occupano di Encrypt e Decrypt di un array di bytes. 

Seguendo il flusso del codice, ho capito che il messaggio Websocket in uscita segue il seguente path:

  • viene generato a partire da oggetti (Model e servizi) che vengono assemblati insieme per creare il Payload del messaggio, una cosa del genere:
{
"event" : {
"dbName" : "xxx",
"action" : 1111,
"v" : "1.1.1",
"name" : "database"
},
"objects" : [
{
"userID" : "123123123123"
}
]
}
  • questi JSON vengono a inviati passati alla funzione di encrypt che restituisce un array di bytes
  • vengono inviati all’Application Server tramite Websocket binario

E, in ingresso, il flusso è al contrario.

A questo punto, chiarito il flusso, ho cercato di capire:

  • – il tipo di encryption usata
  • – le chiavi di cifratura

Problema: le variabili di istanza sono parzialmente offuscate, per colpa di Kotlin ( https://kotlinlang.org ).

Non conosco a fondo l’ecosistema Android, ma ho capito che si tratta di un linguaggio nativo che sta diventando lo standard de facto su Android, e che compila binari Java-compatibili.

Per questo motivo riusciamo a decompilare qualcosa che somiglia a Java, ma che ha delle “stranezze”

Metadata

Per esempio, l’annotation @Metadata contiene un sacco di dati encodati che poi vengono utilizzati a runtime per popolare le variabili di istanza statiche che ho cerchiato.

In Java f25721B = f25721B non significa nulla, ma il vero valore della static String f25721B è in quei metadata, di cui ho trovato pochissima documentazione.

Purtroppo, però, non sono riuscito a trovare un decompilatore Kotlin, quindi ho proseguito con le mie indagini seguendo altre strade.

Ho quindi analizzato altri 2 metodi della classe Crypto, che vengono richiamati durante l’init della classe Cypher ( https://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html )

instance.init(1, new SecretKeySpec(f25746a.m36021a(), C3944ae.m18613e() ? f25743X : f25744Y), new IvParameterSpec(f25746a.m36023b()));
AES
IV

Analizzando la signature del metodo init di Cypher e del constructor di IvParameterSpec(), ho capito che si tratta dei metodi che generano le chiavi di crittografia.

In particolare, recuperano rispettivamente 32 e  16 variabili di istanza e le accodano una a una fino a formare una chiave.

Altro problema, le variabili referenziate sono quelle viste poco sopra:

Variables

e quindi siamo nuovamente a un punto morto, non abbiamo accesso ai byte che compongono le chiavi di crittografia.

Decido quindi di indagare ulteriormente su Kotlin, e scopro che il processo di decompilazione fatto da Jadx, prevede vari passaggi intermedi, tra cui l’estrazione del bytecode dell’applicazione prima di decompilarla.

Mi chiedo quindi…che succede se frugo nel bytecode?

Per estrarlo, uso apktool:

apktool d nome_app.apk

che spacchetta l’apk in una directory contenente diversi file con varie estensioni, tra cui dei file con estensione *.smali che sono quelli contenenti il bytecode dell’app.

Smali

Decido quindi di cercare in tutti i file la stringa Crypto, il nome della classe su cui sto lavorando:

find ./ -iname *.smali -exec grep -H Crypto {} \;
Smali2

e trovo un file a.smali che contiene alcune info interessanti:

  • Una serie di stringhe statiche, di 1 singolo carattere ciascuna. E sono 48 (32 + 16), quelle che cercavo!
Static Fields
  • Il bytecode dei metodi che ho mostrato poco più sopra, quelli che generano la key.
Smali3

In questo modo ho:

  • il contenuto di 48 variabili di istanza  (32 + 16)
  • i dettagli delle assegnazioni, mi spiego meglio:
    • alla riga 115 vediamo che viene generato l’array vuoto
    • alla riga 117, viene assegnata la prima variabile, che è ->C
    • alla riga 118, viene assegnata la seconda, che è ->u
    • e così via

Se torniamo al primo elenco, vedremo che alla lettera C (maiuscola) corrisponde la seguente static var:

che contiene la lettera “q”.

Nel restante bytecode sono anche riuscito a trovare i dettagli dell’encryption usata:

AES

Mi è bastato quindi ricostruire la chiave con il metodo precedente, e tramite quest’ottimo tool online leggere finalmente il payload del messaggio websocket: https://gchq.github.io/CyberChef

Questa specifica app analizzata non utilizza altri metodi di autenticazione e autorizzazione “per user”, quindi la decodifica dei payload permette cose interessanti. I payload possono essere quindi intercettati, modificati e reinviati al backend, eseguendo azioni per conto di altri utenti.

Non è lo scopo di questo post consigliare sugli adeguati metodi di protezione da implementare nelle vostre app, questi metodi dipendono fortemente dalla tipologia di app trattata, ma provo a riassumere alcuni punti fermi:

  • Se la vostra app lavora con dati di utenti (quindi esiste una procedura di login), non reinventate la ruota e usate degli standard. Dopo il login, rilasciate all’utente un token (ad esempio un bel token JWT https://jwt.io ) e utilizzatelo per autorizzare ogni singola chiamata. Una cosa del genere:
{
"event" : {
"dbName" : "xxx",
"action" : 1111,
"v" : "1.1.1",
"name" : "database"
},
"objects" : [
{
"userID" : "123123123123",
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
]
}

Il vostro backend dovrà accettare solo request effettuate con token validi e firmati, inoltre dovrà verificare che le azioni richieste dal payload siano permesse all’utente “proprietario” del token.

Il token può essere comunque letto tramite un attacco MITM come quello che abbiamo appena fatto, ma sarà “per user”, quindi un eventuale attaccante potrà solo fare chiamate con token a lui assegnati (quindi sarà identificabile), e solo azioni che il backend gli permette.

Inoltre, è vero che il token JWT è un normale base64, quindi il suo contenuto leggibile, ma non può essere alterato senza conoscere la sua chiave (in caso di alterazione la sua firma non sarebbe più valida)

Nota importante: il token JWT va generato (“staccato”) direttamente dal backend, e non va generato dall’app, altrimenti siamo punto e a capo, perchè significa che in qualche modo la chiave JWT sarà nota all’app.

  • Non affidatevi esclusivamente all’approccio “Security through obscurity”. Se mettete tutta la logica applicativa, inclusa la validazione e l’autorizzazione, direttamente nell’app client, praticamente state lasciando a un attaccante le chiavi di casa vostra, sono solo nascoste. Magari per l’attaccante sarà difficile scoprire dove nascondete le chiavi, ma con sufficiente pazienza riuscirà a trovarle. (e tipicamente, a meno che non siate un’azienda che investe davvero *tanto* in sicurezza, avrà più tempo e pazienza di voi). Spostate le logiche su backend e trattate le app, anche se fatte al 100% da voi, come oggetti “not trusted”. Possono essere manomesse.

È tutto, fatemi sapere cosa ne pensate e se avete bisogno di altre informazioni nei commenti.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *