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 panelgorila-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éclenchentBotSmsExporter/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), etadd_contact: insère un faux contact directement dans le carnet d'adresses de la victime viaContentProviderOperationsurContactsContract, 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 :
BotHeartbeatServicedémarreGuardServiceet vérifie toutes les 60 secondes qu'il tourne encore ;GuardService, de son côté, relanceBotHeartbeatServicetoutes les 30 secondes (checkMainProcess()). Chaque service ressuscite l'autre s'il est tué, notification deGuardServicevolontairement invisible (setSilent(true), priorité minimale, pas de badge).onTaskRemoved()surBotHeartbeatServicerelance 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, queProxyWebSocketService(§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.