SaferRAT : un trojan bancaire Android qui prend la main façon VNC

Catégorie : RAT

Analyste : Gordon PEIRS

Famille : SaferRAT, un trojan bancaire et RAT Android vendu en MaaS. Zimperium zLabs l'a documenté en juin 2026 dans une vague de 4 campagnes (RecruitRat, SaferRat, Astrinox, Massiv).

Type : analyse statique uniquement. Aucun APK n'a été installé ni exécuté, sur appareil ou sur émulateur. Le manifest, la structure du paquet et tout le code source sont décompilés avec androguard.


Résumé

SaferRAT est un trojan bancaire Android à deux étages : Strawberry.apk est le dropper et nested_app.apk le vrai payload (les noms de fichiers sur MalwareBazaar inversent d'ailleurs les rôles). Il permet une prise de contrôle à distance façon VNC, par un service d'accessibilité et un flux WebSocket. La décompilation complète (androguard) montre des dizaines de classes spécialisées (keylogger, exfiltration des SMS, des appels, des contacts et des photos, localisation, micro, capture d'écran), avec une modularité très au-dessus d'un stealer de base. Le C2 est chargé en clair depuis une ressource JSON embarquée, avec rotation entre serveurs de secours. Le domaine identifié (gorila-panel.xyz, hébergé chez le VPS russe VDSINA) est un IOC neuf, absent de ThreatFox et jamais documenté publiquement. Enfin, l'application embarque un assistant de mise en route complet, entièrement en russe, avec des boutons dédiés pour contourner les restrictions de batterie et d'autostart de cinq surcouches Android (Xiaomi, Huawei, Samsung, Oppo, Vivo). On est devant un produit MaaS professionnel, pas un outil artisanal.


1. Contexte et choix du sujet

Je cherchais un sujet plus complexe que les précédents, avec un sample réellement disponible pour ne pas rester bloqué à l'acquisition.

Zimperium zLabs documente SaferRAT parmi 4 campagnes actives qui visent plus de 800 applications bancaires, crypto et de réseaux sociaux. J'ai vérifié la disponibilité sur MalwareBazaar avant de commencer : 2 échantillons tagués SaferRAT, tous deux téléchargeables en entier et gratuitement.

2. Identité des échantillons

Champ Dropper Payload réel
Nom de fichier (MalwareBazaar) Strawberry.apk nested_app.apk
SHA256 d8cd89e8f7eb14c50e25705fea6f34390ab18486f2d1cadd5e195b0e663672c4 c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b
Taille 15 138 871 octets 8 558 466 octets
Package Android com.example.nestedinstaller s3zse.f01pdoi.ohnrb2 (nom obfusqué)
Label affiché (icône générique) "Strawberry"
Premier vu (MalwareBazaar) 27/06/2026 27/06/2026
Reporter BastianHein_ BastianHein_
Signature MalwareBazaar SaferRAT SaferRAT

Attention aux noms : le fichier appelé Strawberry.apk sur MalwareBazaar est en réalité le dropper (package nestedinstaller), et celui appelé nested_app.apk est le vrai payload malveillant, qui s'affiche sous le label « Strawberry » une fois installé. Les noms de soumission ne reflètent pas la structure réelle (vérification au §3).

3. Architecture à deux étages, confirmée octet pour octet

D'après les noms de fichiers, on attend un dropper « nested installer » qui embarque un second APK. Vérification :

unzip -l Strawberry.apk | grep assets
# → assets/nested_app.apk, 8558466 octets
unzip -o Strawberry.apk assets/nested_app.apk
sha256sum assets/nested_app.apk
# → c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b

L'APK assets/nested_app.apk embarqué dans Strawberry.apk a exactement le même hash que le second fichier téléchargé séparément sur MalwareBazaar.

Strawberry.apk (dropper, com.example.nestedinstaller)
  REQUEST_INSTALL_PACKAGES + QUERY_ALL_PACKAGES
  → assets/nested_app.apk (embarqué, identique hash pour hash au payload)
    → installé via NestedInstallerApp / InstallStatusReceiver
      → payload réel : s3zse.f01pdoi.ohnrb2, se présente comme "Strawberry"

La technique est connue. Livrer le vrai malware comme ressource embarquée d'un installeur à l'air anodin, plutôt que comme APK principal, limite ce que détectent les scanners qui n'inspectent que le point d'entrée visible.

4. Permissions et composants du payload réel : confirmation directe du rapport Zimperium

Le manifest du payload (s3zse.f01pdoi.ohnrb2) contient une liste de permissions et de composants qui recoupe point par point les capacités décrites par Zimperium, avec en plus les vrais noms de classes :

Capacité décrite par Zimperium Composant confirmé dans le manifest
Prise de contrôle à distance / streaming en direct com.example.safeservice.vnc.VncAccessibilityService (service d'accessibilité, littéralement nommé "Vnc")
Streaming caméra camera.CameraStreamActivity + camera.CameraStreamService (FOREGROUND_SERVICE_CAMERA, CAPTURE_VIDEO_OUTPUT)
Streaming microphone microphone.MicrophoneStreamService (FOREGROUND_SERVICE_MICROPHONE, RECORD_AUDIO)
Overlay de phishing via WebView distante InjectWebActivity (SYSTEM_ALERT_WINDOW)
Interception SMS (OTP bancaire) SMSReceiver, MmsReceiver (RECEIVE_SMS, READ_SMS, SEND_SMS, BROADCAST_WAP_PUSH), + prise de rôle app SMS par défaut (DefaultSMSAppChooserActivity, HeadlessSmsSendService)
Anti-suppression MyDeviceAdminReceiver (BIND_DEVICE_ADMIN)
C2 Trois canaux confirmés, pas un seul : panel HTTP (gorila-panel.xyz), WebSocket de contrôle temps réel (VncWebSocketClient), et ProxyWebSocketService, un proxy SOCKS inversé complet. Détail au §5.9

Persistance : 5 mécanismes distincts, plus que ce que détaille Zimperium. Il y a KeepAliveJobService (JobScheduler), KeepAliveAlarmReceiver (AlarmManager), AlarmClockKeepAlive$AlarmClockReceiver (un abus d'alarme réveil, technique connue pour survivre au Doze mode) et KeepAliveWidget (un widget d'écran d'accueil qui relance le service). S'y ajoute sync.StubContentProvider + sync.AuthenticatorService, un abus du framework de comptes et de synchronisation d'Android, plus rare. Le cinquième mécanisme apparaît en lisant BotHeartbeatService/GuardService en entier (§5.11) : un watchdog mutuel, où chaque service relance l'autre s'il est tué.

Dernier composant à noter : l'activité com.example.safeservice.DeveloperActivity (exported=false), dont le nom fait penser à un écran de debug. Son contenu est détaillé au §5.2.

5. Décompilation complète : le C2 en clair, et une piste d'attribution

androguard decompile sur le payload donne tout le code Java/Kotlin décompilé : des centaines de classes, bibliothèques tierces comprises (OkHttp, Kotlin coroutines). Les classes propres au malware sont toutes dans com.example.safeservice : BotCallLogExporter, BotHeartbeatService, BotRegister, BotSmsExporter, CallListener, CallLogSender, ContactsFetcher, InjectConfigManager, InstalledAppsSender, KeyLoggerService, LocationSender, NotificationListener, PermissionsManager, PhotosExporter, ServerConfig, SilentAudioPlayer, SmsSender, UssdSender, vnc/VncAccessibilityService, vnc/VncWebSocketClient, vnc/ScreenCastService, vnc/MicrophoneStreamer. C'est beaucoup plus complet et modulaire qu'un stealer de base.

5.1 Le C2 en clair, trouvé dans ServerConfig.java

Le C2 n'est pas codé en dur dans le bytecode. Il est chargé à l'exécution depuis une ressource JSON embarquée (res/raw/servers.json), avec une rotation entre plusieurs serveurs de secours (ServerConfig.next()) :

unzip -p nested_app.apk res/raw/servers.json
{
  "servers": [
    "https://gorila-panel.xyz"
  ]
}

Vérification passive (DNS et whois, aucune connexion au panel) :

Champ Valeur
Domaine gorila-panel.xyz
IP résolue 91.184.247.166
Registrar NameCheap, Inc.
Serveurs de noms NS1-4.VDSINA.RU, VDSINA, hébergeur VPS russe
Créé 13/11/2025, expire 13/11/2026
ThreatFox / Shodan Aucune entrée existante (IOC neuf, jamais documenté publiquement à ce jour)

Ce domaine confirme la piste russophone, et cette fois par de la vraie infrastructure plutôt que par des commentaires de code : il est hébergé chez VDSINA, un fournisseur de VPS russe.

5.2 DeveloperActivity : un assistant de configuration complet

Malgré son nom, ce n'est pas un écran de debug oublié. C'est un assistant de mise en route très abouti, avec des boutons par constructeur pour contourner les restrictions de batterie et d'autostart des surcouches chinoises les plus agressives :

  • Xiaomi/MIUI (AutoStartManagementActivity, HiddenAppsConfigActivity)
  • Huawei/EMUI (ProtectActivity, StartupNormalAppListActivity)
  • Samsung (BatteryActivity)
  • OPPO/OnePlus/Realme/ColorOS (StartupAppListActivity)
  • Vivo (BgStartUpManagerActivity)

S'y ajoutent les demandes de permissions habituelles (overlay, notifications, batterie, SMS par défaut, caméra, localisation, micro, stockage) et un bouton qui ouvre directement les réglages de réinitialisation d'usine (openFactoryResetSettings). Tous les textes affichés sont en russe (« ✅ Разрешение камеры уже выдано », « Перейдите: Система → Сброс настроек »...). Cette fois, c'est une interface complète et fonctionnelle, pas un simple commentaire, qui pointe vers un développement russophone.

Aucune identité ne fuit ici, ce n'est donc pas une erreur d'OPSEC. Mais le niveau d'ingénierie et de finition (5 familles d'OEM Android gérées chacune à part) est celui d'un produit MaaS professionnel, pas d'un outil bricolé.

5.3 La liste des banques ciblées n'est pas statique

InjectConfigManager.java confirme que la table pkg → url_de_phishing n'est remplie que par updateFromJson(), avec des données reçues du C2. Il n'y a donc rien de statique à extraire de l'APK sur ce point, ce qui correspond au mécanisme de « WebView chargée à distance » décrit par Zimperium. C'est une limite de l'architecture, pas un manque de recherche.

5.4 Piste d'attribution : le tag opérateur "Diabloo"

BotRegister.java construit la charge d'enregistrement envoyée au C2 (POST /api/register_bot.php) :

v3_1.put("tag", p13);           // device ID (ANDROID_ID)
v3_1.put("phone", p14);          // numéro de téléphone de la victime
v3_1.put("worker", "Diabloo");   // <-- identifiant codé en dur
v3_1.put("sim_info", this.getSimInfo());  // opérateur, slot SIM, numéro

"Diabloo" est un identifiant codé en dur. C'est très probablement le tag d'un opérateur ou d'un client dans un modèle MaaS : chaque acheteur du builder SaferRAT recevrait un build avec son propre tag « worker », pour que le panel distingue les infections de chaque client. Ça peut aussi être une signature du développeur du build. Une recherche web ne donne aucune occurrence publique de « Diabloo » en lien avec ce malware ou un builder connu. C'est donc soit un tag privé jamais publié, soit un pseudo sans lien avec une campagne documentée. Côté attribution, c'est l'élément le plus concret de cette analyse.

5.5 Protocole complet de contrôle à distance (VncWebSocketClient.handleCommand)

La lecture du dispatcher de commandes montre un système ATS (Automated Transfer System) complet, pas seulement du contrôle manuel façon VNC :

Contrôle tactile temps réel : tap, touch_move, long_press, reset_touch, set_touch_mode (bascule un mode "realTime"), swipe_up/swipe_up_fast/swipe_up_menu, back/home (actions système globales).

Automatisation sans supervision : autopin, l'opérateur envoie un PIN et une séquence de coordonnées (sequence: [{x,y}, ...]), rejouée automatiquement par VncAccessibilityService.autoInputPin(). Vraisemblablement utilisé pour ressaisir automatiquement un code PIN capturé au préalable (overlay de phishing ou keylogger) sans intervention manuelle de l'opérateur. uidump renvoie la hiérarchie d'accessibilité de l'écran (texte/structure), permettant une navigation automatisée sans dépendre du flux vidéo.

Furtivité pendant l'opération : show_overlay_black (overlay noir plein écran, technique connue pour masquer l'activité du bot à la victime pendant qu'il agit en arrière-plan), volume_mute/volume_set/volume_get (couper le son pour ne pas alerter la victime).

Streaming : start_stream/stop_stream (capture d'écran via MediaProjection), start_camera/stop_camera, start_microphone.

Recoupement exact avec le rapport Zimperium : les commandes enable_anti_delete/disable_anti_delete (§4) se retrouvent telles quelles dans le dispatcher, avec le flag VncAccessibilityService.isAntiDeleteEnabled.

Autres : unlock/lock_screen, toggle_injection/update_injection (contrôle à distance de l'overlay de phishing, ce qui colle avec §5.3), download_file, request_projection.

5.6 Modules supplémentaires lus : exfiltration de notifications avec masquage, USSD, et un keylogger dupliqué mais inactif

NotificationListener (NotificationListenerService) envoie le titre et le texte de chaque notification de chaque app à /api/log_notification.php, avec le tag bot_tag (ANDROID_ID). En retour, le C2 fournit une liste de « packages bloqués » (GET /api/not_block_list.php?bot_tag=...). Une notification venant d'une app de cette liste est exfiltrée et supprimée en silence de l'écran de la victime par cancelNotification(). C'est le mécanisme classique pour cacher à la victime les alertes bancaires ou les codes 2FA tout en les récupérant côté attaquant. La liste elle-même reste côté serveur, donc on ne voit pas en statique quels packages sont visés (même constat qu'au §5.3).

UssdSender permet d'envoyer n'importe quel code USSD (par une intention CALL sur une URI tel:*...#), sur ordre à distance. Le code USSD lui-même viendrait du C2, pas de l'APK. Chez les trojans bancaires, cette fonction sert en général à activer un renvoi d'appel vers un numéro de l'attaquant (pour intercepter les OTP envoyés par appel vocal) ou à consulter un solde.

KeyLoggerService, un keylogger en double et inactif : cette classe à part, elle aussi sous-classe d'AccessibilityService, contient un keylogger et un moniteur de presse-papiers fonctionnels, qui envoient à /api/log_keystrokes.php. Mais elle n'est déclarée nulle part dans le manifest : seul VncAccessibilityService a l'intent-filter android.accessibilityservice.AccessibilityService dont Android a besoin pour l'activer. Son code ne tourne donc pas sur ce build. Le keylogging, lui, fonctionne quand même. La lecture complète de VncAccessibilityService.java (§5.7) montre que le service actif embarque son propre keylogger et son propre moniteur de presse-papiers, indépendants de KeyLoggerService. Reste à comprendre pourquoi le code en double de KeyLoggerService est resté là au lieu d'être supprimé ou fusionné. C'est probablement un reste d'une version antérieure du framework MaaS, ou un module partagé entre builds dont celui-ci n'a pas besoin.

J'ai lu KeyLoggerService en entier (39 lignes utiles) et je l'ai comparé ligne à ligne à VncAccessibilityService. Ce n'est pas une implémentation indépendante, c'est un ancêtre direct du même code. La preuve la plus nette : la ligne de log du moniteur de presse-papiers, "📋 Скопировано в буфер: ", est identique dans les deux classes, caractère pour caractère, emoji compris. C'est un copier-coller, pas une réécriture. Mais KeyLoggerService est bien plus rudimentaire. Il n'a ni mise en tampon ni délai de 1 seconde avant l'envoi (chaque TYPE_VIEW_TEXT_CHANGED part tout de suite en HTTP), il ne gère pas TYPE_VIEW_CLICKED, donc ne capture aucun PIN par coordonnées, il n'est pas relié au VncWebSocketClient (seulement un POST HTTP direct vers le même endpoint /api/log_keystrokes.php), et il n'a rien de ce qui entoure un service d'accessibilité complet (pas de setServiceInfo, pas de notification de premier plan, pas de canal WorkManager). Le plus probable : KeyLoggerService est le prototype. Le keylogger et le moniteur de presse-papiers en ont été copiés, puis étendus (mise en tampon, capture de PIN, canal WebSocket) directement dans VncAccessibilityService, quand tout a été regroupé dans un seul service d'accessibilité (Android n'en lie proprement qu'un à la fois). La classe d'origine est restée dans le code, non déclarée. C'est un reste de refactoring, pas un module qu'on active par client.

5.7 VncAccessibilityService lu intégralement : keylogger et moniteur de presse-papier actifs, avec capture et rejeu de PIN

La lecture complète de VncAccessibilityService.java (1039 lignes) et de son Companion le confirme, code à l'appui : le service actif fait tout ce que KeyLoggerService aurait pu faire, avec en plus un pipeline de capture de code PIN bien plus élaboré qu'un keylogging générique.

Keylogger générique : onAccessibilityEvent() capture les événements TYPE_VIEW_TEXT_CHANGED (saisie dans un champ de texte) sur n'importe quelle app au premier plan, bufferise le texte, et l'envoie après 1 seconde d'inactivité (inputDelay) à sendKeystrokeToServer(), qui POST vers /api/log_keystrokes.php avec bot_tag (Android ID), nom de l'app et texte.

Moniteur de presse-papier : câblé directement dans onServiceConnected() via clipboardManager.addPrimaryClipChangedListener(...), envoie le contenu copié au même endpoint, taggé "clipboard".

Capture de code PIN, avec coordonnées exactes et horodatage : onAccessibilityEvent() capture aussi les événements TYPE_VIEW_CLICKED (tap sur un bouton). Si le texte du bouton cliqué est un chiffre unique (regex ^\d{1}$), typiquement une touche de clavier PIN à l'écran, le service route l'événement vers trackPinClickWithCoords() plutôt que vers le keylogger générique : cette méthode calcule le centre exact du bouton tapé (getBoundsInScreen()), horodate le tap, l'accumule dans une séquence (pinTouchSequence), et envoie immédiatement au C2 en WebSocket brut un message {"type":"pin_click","data":{"digit","x","y","timestamp"}}. Après 3 secondes sans nouveau chiffre, trackPinClickWithCoords$lambda$13 vérifie si l'écran est déverrouillé (KeyguardManager.isKeyguardLocked()) : si oui, ce qui signifie que le PIN saisi a bien débloqué l'appareil, il envoie un second message {"type":"pin_verified","bot_tag","pin","timestamp","sequence":[...]} via sendVerifiedPin(), la séquence complète de taps (chiffre + x + y + horodatage) incluse. Le texte des boutons non numériques cliqués (labels, contenu-description) est aussi loggé, tagué "[PIN] ", dans le même flux keylogger générique mais marqué différemment du texte de champ classique.

Ce mécanisme explique la commande autopin vue au §5.5. La séquence de taps capturée, avec ses coordonnées exactes, a précisément le format que autoInputPin() attend en retour pour rejouer la saisie plus tard sur un autre écran de verrouillage, par exemple après un redémarrage forcé de l'appareil.

autoInputPin() (le rejeu) : pour chaque paire de coordonnées reçue du C2, un Handler.postDelayed() est programmé avec un délai de index * 400 millisecondes, soit un tap toutes les 400 ms, dans l'ordre, avant l'appel à performTap(x, y) sur chaque position. Cette lenteur est voulue : elle imite un rythme de frappe humain, là où une saisie instantanée attirerait l'attention ou déclencherait une détection anti-bot côté banque.

dumpUiHierarchy(), implémentation confirmée : parcourt réellement l'arbre d'accessibilité depuis getRootInActiveWindow() (traversée récursive de tous les enfants), construit un tableau JSON avec classe, limites à l'écran et texte de chaque noeud visible, et l'envoie en direct au C2 via WebSocket sous la forme {"type":"uidump","data":[...]}. Confirme que la navigation à distance peut s'appuyer sur la structure réelle de l'écran plutôt que sur un flux vidéo interprété à l'oeil, une capacité d'automatisation plus fiable qu'un simple contrôle façon VNC manuel.

showOverlayWeb(), implémentation confirmée : crée une vraie android.webkit.WebView, avec setJavaScriptEnabled(true), setDomStorageEnabled(true), et charge l'URL reçue du C2 (loadUrl(p12)), affichée en overlay plein écran par-dessus l'app active via WindowManager. Confirme, au niveau du code et pas seulement de la description Zimperium, que l'overlay de phishing est une page web complète et interactive chargée à distance, pas une simple image statique.

5.8 Injection tactile confirmée au niveau de la vraie API Android, pas un stub

performTap(), performInitialLongPress(), processRealtimePoints() et finishRealtimeGesture() passent tous par la vraie API d'accessibilité d'Android pour injecter du tactile. Ils construisent un android.graphics.Path, l'enveloppent dans un GestureDescription.StrokeDescription (avec une vraie durée : 100 ms pour un tap, 300 ms pour un appui long, des segments de 30 ms en mode temps réel), puis appellent AccessibilityService.dispatchGesture(), l'API officielle pour simuler des gestes depuis un service d'accessibilité. Il n'y a donc ni injection d'événements bas niveau ni exploit. C'est un usage documenté de l'accessibilité, légitime au sens de l'API, détourné à des fins malveillantes, comme tout le reste de ce malware (voir aussi le contournement anti-suppression du §4, qui utilise la même API performGlobalAction).

5.9 Quatre services réseau, sur deux hébergeurs

En lisant en entier onServiceConnected(), ProxyWebSocketService.java et BotHeartbeatService.java, on voit que ce payload ne parle pas à un seul C2. Il s'adresse à quatre services réseau séparés, sur deux hébergeurs, chacun avec son rôle :

Service Endpoint Rôle Hébergeur
Panel HTTP gorila-panel.xyz (91.184.247.166) Tous les endpoints /api/*.php : enregistrement, exfiltration keylogger/notifications/PIN/contacts/appels, réception de la liste d'injection (§5.3), et polling de commandes (GET /api/get_bot_commands.php, toutes les 10 secondes, voir §5.11) VDSINA (VPS russe)
WebSocket de contrôle temps réel ws://94.103.89.12:8765, codé en dur dans onServiceConnected() Commandes ATS en direct (tap, autopin, uidump, streaming, voir §5.5), capture PIN en direct (pin_click/pin_verified, §5.7) VDSINA (même IP, port différent du suivant)
Upload photo/vidéo en masse http://94.103.89.12:9000/upload_photo, codé en dur dans PhotosExporter Réception de ZIP multipart contenant l'intégralité de la pellicule photo/vidéo de la victime (§5.11) VDSINA (même IP que le contrôle temps réel, port distinct : service HTTP séparé, pas le même processus WebSocket)
ProxyWebSocketService ws://89.185.80.124:55332/socks/<bot_id>, codé en dur Proxy SOCKS inversé complet, présent dans le code et fonctionnel, mais jamais démarré sur ce build (voir ci-dessous) Global Connectivity Solutions (Royaume-Uni/Danemark), hébergeur distinct des trois autres

Le proxy SOCKS inversé (lu dans handleTextMessage()/handleBinaryMessage()) : le C2 envoie une commande texte connId:host:port, et le téléphone infecté ouvre une vraie java.net.Socket TCP vers cette destination, depuis le réseau et l'adresse IP de la victime. Les données binaires sont ensuite relayées dans les deux sens sur la connexion WebSocket, préfixées par leur longueur et par la valeur de connId pour multiplexer plusieurs connexions. C'est le « proxy résidentiel involontaire » classique des trojans bancaires : la session frauduleuse passe par l'IP et l'empreinte réseau de la victime, ce qui déjoue les contrôles anti-fraude fondés sur la géolocalisation ou sur le lien entre IP et appareil. Un détail de camouflage : la notification de premier plan de ce service affiche « Antivirus » / « System heart », pour rassurer la victime si elle regarde ses notifications persistantes.

Ce proxy n'est pas démarré sur ce build. BotHeartbeatService.onCreate() (§5.11) contient un log explicite, "🛑 Режим прокси отключён, ProxyWebSocketService не будет запущен" (« mode proxy désactivé, ProxyWebSocketService ne sera pas démarré »), et un grep sur tout le code décompilé ne trouve aucun autre endroit où ce service est instancié ou démarré. C'est la même situation que pour KeyLoggerService (§5.6) : du code réel, complet et fonctionnel, mais dormant sur ce build. Le canal reste un IOC valide, puisque l'URL est codée en dur et que le code est prêt à tourner s'il est activé. Ce n'est simplement pas un canal que j'ai vu fonctionner sur cet échantillon.

Aucune des deux nouvelles IP (94.103.89.12, 89.185.80.124) n'a d'entrée sur ThreatFox ou Shodan. Ça fait deux IOC neufs de plus.

5.10 Triage structuré complémentaire (quark-engine)

quark-engine (v26.7.1, règles à jour au 19/07/2026) classe le payload « High Risk » (score cumulé 0.97 au seuil de confiance 60%), avec plus de 130 comportements reconnus à 60% de confiance ou plus. Il n'apporte aucune capacité nouvelle par rapport à la lecture manuelle du code (§5.1-§5.9). Les signatures les mieux pondérées recoupent ce que j'avais déjà confirmé ligne à ligne : invocation dynamique par réflexion (poids 3.01), envoi HTTP POST (1.96), résolution de champ par réflexion (1.79), lecture du presse-papiers, dispatch de gestes, création d'overlay, accès à l'arbre d'accessibilité par texte, ID ou racine de fenêtre, lecture des SMS, du journal d'appels et des contacts, requête de localisation. C'est utile comme validation automatique et indépendante du périmètre de capacités, mais ça ne donne rien de plus sur l'attribution ou le C2. Tous les IOC et les détails de protocole de ce document viennent de la lecture manuelle.

5.11 Les six derniers modules lus : orchestration, streaming d'écran réel, et exfiltration de masse de la pellicule

BotHeartbeatService, le vrai chef d'orchestre, plus central que son nom ne le suggère. Trois apports majeurs :

  • Un canal de commandes supplémentaire, par polling HTTP (GET /api/get_bot_commands.php?tag=<bot_tag>, sur le même panel gorila-panel.xyz, toutes les 10 secondes), avec son propre jeu de commandes, différent de celui du WebSocket temps réel (§5.5) : export_sms/export_call_log (déclenchent BotSmsExporter/BotCallLogExporter, export en masse), send_sms (avec délai et choix de slot SIM configurables), send_ussd, show_notification (affiche une fausse notification système avec titre/texte fournis par le C2, un déclencheur d'ingénierie sociale à la demande), et add_contact : insère un faux contact directement dans le carnet d'adresses de la victime via ContentProviderOperation sur ContactsContract, nom et numéro fournis par le C2. Utilisation la plus probable : faire apparaître un faux numéro de "support bancaire" comme contact légitime pour du vishing (l'appel entrant semble alors venir d'un contact enregistré plutôt que d'un inconnu).
  • Un cinquième mécanisme de persistance, un watchdog mutuel : BotHeartbeatService démarre GuardService et vérifie toutes les 60 secondes qu'il tourne encore ; GuardService, de son côté, relance BotHeartbeatService toutes les 30 secondes (checkMainProcess()). Chaque service ressuscite l'autre s'il est tué, notification de GuardService volontairement invisible (setSilent(true), priorité minimale, pas de badge). onTaskRemoved() sur BotHeartbeatService relance aussi immédiatement le service si l'utilisateur swipe l'app hors des apps récentes.
  • Démarre aussi SilentAudioPlayer (ci-dessous) et confirme, par un log explicite, que ProxyWebSocketService (§5.9) n'est pas démarré sur ce build.

SilentAudioPlayer joue en boucle une piste audio silencieuse embarquée (res/raw/silent), volume à zéro, avec setWakeMode et un redémarrage automatique en cas d'erreur (OnErrorListener). C'est une technique connue pour garder un processus Android en vie : une session de lecture multimédia active échappe à des restrictions de batterie et de Doze que le service seul subirait.

ScreenCastService, le vrai streaming d'écran confirmé au niveau implémentation : capture via MediaProjection + ImageReader + VirtualDisplay, chaque frame convertie en Bitmap, compressée en JPEG (qualité 60), encodée en Base64, envoyée en direct sur le même canal WebSocket que le contrôle temps réel (ws://94.103.89.12:8765, §5.9) sous la forme {"type":"screencast","image":"data:image/jpeg;base64,..."}. Confirme le mécanisme de streaming déjà décrit par Zimperium, avec le protocole exact.

PhotosExporter, exfiltration automatique de toute la pellicule, pas seulement sur commande : parcourt MediaStore (images et vidéos), traite par lots de 100 fichiers, compresse les images (JPEG qualité 75, redimensionnées à 1920x1080 maximum) et copie les vidéos telles quelles, empaquette chaque lot en ZIP, et l'envoie en multipart/form-data vers un quatrième service réseau distinct, http://94.103.89.12:9000/upload_photo (même IP que le WebSocket de contrôle, port et protocole différents, un serveur HTTP séparé). Point important confirmé en cherchant tous les appelants : exportPhotosAndUpload() est déclenché automatiquement depuis MainActivity, à plusieurs endroits liés à l'octroi des permissions de stockage/médias au premier lancement, pas sur commande du C2. Dès que l'app obtient l'accès à la galerie, l'exfiltration complète des photos et vidéos démarre sans action de l'opérateur.

ContactsFetcher est actif, et pas seulement présent. Il dépend du flag ENABLE_CONTACTS_FETCH, une constante true codée en dur dans MainActivity et réaffirmée dans un gestionnaire onActivityResult. Il envoie le nom et le numéro de chaque contact à /api/upload_contacts.php, avec un flag SharedPreferences pour ne le faire qu'une fois par installation.

CallLogSender est un flux distinct de BotCallLogExporter. La commande export_call_log (ci-dessus) déclenche à la demande un export en masse de l'historique d'appels. CallLogSender.sendCallLog(), lui, envoie chaque appel entrant, un par un et en temps réel, à /api/log_call.php dès qu'il arrive (il est déclenché par CallListener, un écouteur d'état téléphonique enregistré par BotHeartbeatService). Ce sont deux mécanismes de surveillance des appels, l'un continu et l'autre à la demande, et ils ne font pas doublon.

6. Limites

  • L'attribution « Diabloo » est une observation brute (une chaîne codée en dur), pas une identité confirmée. Ça peut être un tag de build, un pseudo ou autre chose.
  • Je n'ai consulté aucune confirmation indépendante (sandbox tierce de type Triage ou ANY.RUN).
  • Les IOC ne sont pas encore soumis à ThreatFox : la soumission par API demande un compte sur le portail web d'abuse.ch, la clé de recherche ne suffit pas.
  • ProxyWebSocketService (le proxy SOCKS inversé) est présent et fonctionnel dans le code, mais il n'est pas démarré sur ce build (§5.9). C'est un IOC valide (URL codée en dur), pas un canal observé en fonctionnement sur cet échantillon.

7. IOCs consolidés

Type Valeur
SHA256 dropper (Strawberry.apk) d8cd89e8f7eb14c50e25705fea6f34390ab18486f2d1cadd5e195b0e663672c4
SHA256 payload (nested_app.apk, embarqué et confirmé identique) c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b
Package dropper com.example.nestedinstaller
Package payload s3zse.f01pdoi.ohnrb2
Label affiché du payload Strawberry
C2 panel HTTP gorila-panel.xyz (91.184.247.166)
Hébergement C2 panel (NS) VDSINA (VPS russe)
C2 WebSocket de contrôle temps réel ws://94.103.89.12:8765, codé en dur, VDSINA également
Service d'upload photo/vidéo en masse http://94.103.89.12:9000/upload_photo, codé en dur, même IP que le WebSocket de contrôle, port et protocole distincts
C2 proxy SOCKS inversé (présent, non démarré sur ce build) ws://89.185.80.124:55332/socks/<bot_id>, codé en dur, hébergeur distinct (Global Connectivity Solutions, UK/DK)
Endpoints C2 HTTP confirmés POST /api/register_bot.php, POST /api/log_notification.php, GET /api/not_block_list.php, POST /api/log_keystrokes.php, POST /api/log_call.php, POST /api/upload_contacts.php, GET /api/get_bot_commands.php (polling, 10s)
Commandes reçues via polling HTTP confirmées export_sms, send_sms, export_call_log, show_notification, send_ussd, add_contact (voir §5.11)
Messages WebSocket bot vers C2 confirmés pin_click, pin_verified, uidump, volume_info, screencast (voir §5.7, §5.11)
Tag opérateur/build Diabloo
Service VNC/accessibilité (actif, keylogger + presse-papier + capture PIN intégrés) com.example.safeservice.vnc.VncAccessibilityService
Keylogger dupliqué (code présent, service non déclaré, inactif sur ce build, fonction déjà assurée par VncAccessibilityService) com.example.safeservice.KeyLoggerService
Watchdog mutuel (5e mécanisme de persistance) com.example.safeservice.BotHeartbeatService ↔ com.example.safeservice.GuardService
SHA256 certificat de signature (dropper) a7434275... (keystore debug Android générique)
SHA256 certificat de signature (payload) c3fcf873... (keystore debug Android générique, différent du dropper)
Famille SaferRAT
Rapport source Zimperium zLabs, juin 2026

8. Reproduire l'analyse

Le journal détaillé est dans runbook.md.