# 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](https://zimperium.com/blog/android-bankers-4-campaigns-in-a-row) 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 :
```bash
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()`) :

```bash
unzip -p nested_app.apk res/raw/servers.json
```
```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`) :

```java
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](https://zimperium.com/blog/android-bankers-4-campaigns-in-a-row) |

## 8. Reproduire l'analyse

Le journal détaillé est dans [`runbook.md`](writeups/saferrat-android/runbook.md).
