Runbook : reproduction pas à pas

A. Vérification de la disponibilité avant de s'engager#

Commande :

MB_KEY=$(cat ~/ctf-reverse/.mb_api_key)
for tag in Rokarolla Crocodilus RecruitRat SaferRat Astrinox Massiv; do
  curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $MB_KEY" -d "query=get_taginfo&tag=$tag&limit=3"
done

Pourquoi : leçon tirée des analyses jscrambler/DeepSeek, vérifier la disponibilité réelle avant de s'engager dans une analyse.

Retour : Crocodilus, RecruitRat, SaferRat disponibles avec samples réels. Rokarolla/Astrinox/Massiv absents (trop récents ou non partagés).

B. Installation de l'outillage Android#

Commande :

pip install androguard

Pourquoi : jadx/apktool nécessitent Java (non installé) ; androguard est pur Python, avec parsing de manifest, décompilation DEX intégrée. Installation complète (pas --no-deps) après avoir appris de la galère Qiling, androguard a des dépendances bien plus légères (pas de pillow).

Retour : installation propre, androguard --help fonctionnel avec les commandes axml, decompile, disassemble, apkid, arsc, sign, cg.

C. Téléchargement des échantillons#

Commande :

MB_KEY=$(cat ~/ctf-reverse/.mb_api_key)
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $MB_KEY" \
  -d "query=get_file&sha256_hash=c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b" -o sample1.zip
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $MB_KEY" \
  -d "query=get_file&sha256_hash=d8cd89e8f7eb14c50e25705fea6f34390ab18486f2d1cadd5e195b0e663672c4" -o sample2.zip

Retour : premier essai → 502 Server Error temporaire sur un des deux, résolu au second essai après quelques secondes.

D. Extraction (zip chiffré AES, mot de passe standard MalwareBazaar)#

Commande :

unzip -P infected -o sample.zip   # échoue : "need PK compat. v5.1 (can do v4.6)"
7z x -p"infected" -o./apks/ sample.zip -y   # fonctionne
find apks -type f -exec chmod -x {} \;

Pourquoi : l'unzip système ne supporte pas le format ZIP AES v5.1 utilisé par MalwareBazaar, 7z (déjà présent sur la machine) le gère.

Retour : 2 APK extraits, jamais rendus exécutables, confirmés Android package (APK) via file.

E. Analyse du manifest : payload réel#

Commande :

androguard axml c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b.apk

Retour : manifest complet, package s3zse.f01pdoi.ohnrb2, permissions SMS/caméra/micro/localisation/device admin, composants VncAccessibilityService, CameraStreamService, MicrophoneStreamService, InjectWebActivity, SMSReceiver, MmsReceiver, MyDeviceAdminReceiver, 4 mécanismes de persistance, voir saferrat-android.md §4.

F. Analyse du manifest : dropper#

Commande :

androguard axml d8cd89e8f7eb14c50e25705fea6f34390ab18486f2d1cadd5e195b0e663672c4.apk

Retour : package com.example.nestedinstaller, REQUEST_INSTALL_PACKAGES+QUERY_ALL_PACKAGES, classe NestedInstallerApp.

G. Confirmation de la structure à deux étages (hash exact)#

Commande :

unzip -l Strawberry.apk | grep assets
# → assets/nested_app.apk, 8558466 octets (taille identique au 2e sample)
unzip -o Strawberry.apk assets/nested_app.apk -d /tmp/extracted_asset
sha256sum /tmp/extracted_asset/assets/nested_app.apk

Retour : c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b, hash identique au payload téléchargé séparément.

Ce qu'on en retient : confirme, par le hash et non par supposition, que le dropper embarque exactement le payload comme ressource. Voir saferrat-android.md §3.


Décompilation complète, C2, attribution#

H. Décompilation complète du payload#

Commande :

androguard decompile -o decompiled_payload c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b.apk

Retour : exécution en arrière-plan (~2 min), centaines de classes Java produites, aucune erreur bloquante (quelques warnings "Multiple exit nodes found" sur des méthodes isolées, sans impact). Structure com.example.safeservice.* complète.

I. Extraction directe de la config C2#

Commande :

unzip -p nested_app.apk res/raw/servers.json

Retour : {"servers": ["https://gorila-panel.xyz"]}, trouvé après lecture de ServerConfig.java qui pointait vers cette ressource plutôt qu'une chaîne en dur dans le bytecode.

J. Vérification passive du C2#

Commande :

dig +short gorila-panel.xyz A
whois gorila-panel.xyz
curl -s "https://internetdb.shodan.io/91.184.247.166"
curl -s -X POST "https://threatfox-api.abuse.ch/api/v1/" -H "Auth-Key: $(cat ~/ctf-reverse/.mb_api_key)" -d '{"query":"search_ioc","search_term":"gorila-panel.xyz"}'

Retour : IP 91.184.247.166, nameservers NS1-4.VDSINA.RU (hébergeur russe), aucune entrée Shodan, aucune entrée ThreatFox (ni pour le domaine ni pour l'IP), IOC neuf.

K. Lecture de DeveloperActivity.java#

Commande : Read direct du fichier décompilé.

Retour : assistant de configuration complet avec contournements OEM (Xiaomi/Huawei/Samsung/OPPO/Vivo), tous les textes UI en russe, voir saferrat-android.md §5.2.

L. Lecture de InjectConfigManager.java#

Retour : confirme que la table cible (package bancaire → URL phishing) est peuplée uniquement via updateFromJson() depuis le C2, rien de statique dans l'APK.

M. Lecture de BotRegister.java : découverte du tag "Diabloo"#

Retour : protocole d'enregistrement POST /api/register_bot.php avec charge JSON {tag, phone, worker: "Diabloo", sim_info}. Recherche web sur "Diabloo" + malware/botnet/telegram → aucun résultat public, tag non documenté ailleurs.


Certificats, recherche multi-samples, protocole complet#

N. Comparaison des certificats de signature#

Commande :

androguard sign d8cd89e8f7eb14c50e25705fea6f34390ab18486f2d1cadd5e195b0e663672c4.apk
androguard sign c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b.apk

Retour : certificats différents (SHA1 ccca3ab1... vs 44bd3913...), tous deux v2-signés uniquement.

Détail complet (androguard API Python, get_certificates_v2()) :

from androguard.core.apk import APK
a = APK(fichier)
for cert in a.get_certificates_v2():
    print(cert.subject.human_friendly, cert.not_valid_before, cert.sha256_fingerprint)

Retour : les deux certificats ont CN=Android Debug, O=Android, C=US (keystore de debug générique Android/Gradle), donc pas de certificat personnalisé attribuable. Dates de validité différentes : dropper depuis 22/06/2026, payload depuis 24/03/2025 (~15 mois d'écart).

Ce qu'on en retient : pas de lien direct via le certificat (debug key générique), mais l'ancienneté du keystore du payload suggère une réutilisation d'environnement de build sur la durée. Voir saferrat-android.md §6 point 2.

O. Recherche d'autres samples SaferRAT#

Commande :

curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $MB_KEY" -d "query=get_taginfo&tag=SaferRAT&limit=50"

Retour : seulement les 2 samples déjà en main. Aucun autre disponible pour tester l'hypothèse multi-opérateurs (tags "worker" différents).

P. Lecture du protocole complet de contrôle à distance#

Commande : Read sur net/VncWebSocketClient.java, méthode handleCommand (dispatcher de commandes basé sur un switch sur le hash de la chaîne action).

Retour : liste complète des commandes C2, voir saferrat-android.md §5.5. Confirmation exacte des commandes enable_anti_delete/disable_anti_delete du rapport Zimperium, plus découverte de autopin (rejeu automatique de PIN + séquence tactile), uidump, show_overlay_black, contrôle audio/vidéo complet.


Modules supplémentaires, lecture intégrale de VncAccessibilityService et ProxyWebSocketService, deux nouveaux C2#

Q. Lecture de UssdSender.java#

Retour : envoi de codes USSD arbitraires via une intention CALL sur tel:*...#, code réel fourni par le C2. Voir saferrat-android.md §5.6.

R. Lecture de NotificationListener.java#

Retour : exfiltre titre/texte de chaque notification (POST /api/log_notification.php), reçoit une liste de packages à bloquer depuis le C2 (GET /api/not_block_list.php?bot_tag=...), et supprime silencieusement (cancelNotification()) les notifications des apps bloquées tout en les exfiltrant quand même. Voir saferrat-android.md §5.6.

S. Lecture de KeyLoggerService.java, puis vérification manifest#

Commande :

androguard axml nested_app.apk | grep -B2 -A3 "KeyLogger\|AccessibilityService"

Retour : KeyLoggerService (vrai keylogger + moniteur presse-papier fonctionnel, POST /api/log_keystrokes.php) n'apparaît pas dans le manifest, seul VncAccessibilityService a l'intent-filter d'accessibilité requis pour être activé par Android.

Ce qu'on en retient : code réel et fonctionnel mais inactif sur ce build précis, cohérent avec un framework modulaire où des fonctionnalités sont activées/désactivées par client sans retirer le code. Voir saferrat-android.md §5.6.

T. Lecture complète de VncAccessibilityService.java et VncAccessibilityService$Companion.java#

Commande : Read direct des deux fichiers décompilés (1039 + 1647 lignes).

Retour : correction majeure par rapport à la lecture précédente (§5.6 dans sa version antérieure) : le service réellement actif (VncAccessibilityService, pas KeyLoggerService) contient son propre keylogger (TYPE_VIEW_TEXT_CHANGED), son propre moniteur de presse-papier (câblé dans onServiceConnected), et un pipeline complet de capture de PIN par coordonnées (trackPinClickWithCoords, sendVerifiedPin), avec deux nouveaux types de message WebSocket bot vers C2 : pin_click et pin_verified. Confirmé aussi : autoInputPin rejoue les taps avec un délai de 400 ms entre chaque via Handler.postDelayed, dumpUiHierarchy fait une vraie traversée récursive de l'arbre d'accessibilité envoyée en uidump, showOverlayWeb instancie une vraie WebView avec JS activé chargeant l'URL du C2, et performTap/performInitialLongPress/processRealtimePoints utilisent tous dispatchGesture/GestureDescription, la vraie API Android d'injection tactile pour service d'accessibilité. Voir saferrat-android.md §5.6 (corrigé), §5.7, §5.8.

Découverte incidente : onServiceConnected() construit VncWebSocketClient(this, "ws://94.103.89.12:8765"), une URL codée en dur, différente du panel HTTP gorila-panel.xyz. Un deuxième canal C2, pour le contrôle temps réel.

U. Vérification passive du deuxième C2 (94.103.89.12)#

Commande :

whois 94.103.89.12
curl -s "https://internetdb.shodan.io/94.103.89.12"
curl -s -X POST "https://threatfox-api.abuse.ch/api/v1/" -H "Auth-Key: $(cat ~/ctf-reverse/.mb_api_key)" -d '{"query":"search_ioc","search_term":"94.103.89.12"}'

Retour : hébergé chez VDSINA (même hébergeur que le panel HTTP, abuse@vdsina.ru), aucune entrée Shodan, aucune entrée ThreatFox, IOC neuf.

V. Lecture complète de ProxyWebSocketService.java#

Retour : troisième canal C2, un proxy SOCKS inversé complet. handleTextMessage reçoit connId:host:port et ouvre une vraie java.net.Socket TCP vers cette destination, handleBinaryMessage relaie les données binaires dans les deux sens sur le WebSocket, multiplexées par connId. URL codée en dur : ws://89.185.80.124:55332/socks/<bot_id>. Notification de premier plan déguisée en "Antivirus" / "System heart". Voir saferrat-android.md §5.9.

W. Vérification passive du troisième C2 (89.185.80.124)#

Commande : mêmes vérifications passives qu'en U.

Retour : hébergeur distinct des deux autres, Global Connectivity Solutions (Royaume-Uni/Danemark, abuse@globconnex.com), aucune entrée Shodan ni ThreatFox, deuxième IOC neuf trouvé ici.

Ce qu'on en retient : ce malware ne parle pas à un C2 unique mais à trois canaux séparés sur deux hébergeurs différents, une segmentation d'infrastructure qui limite l'impact si un seul canal ou un seul hébergeur est démantelé. Voir saferrat-android.md §5.9 pour la table complète et l'explication du mécanisme de proxy résidentiel.

X. Lecture complète de KeyLoggerService.java, comparaison ligne à ligne avec VncAccessibilityService#

Commande : Read direct du fichier décompilé (39 lignes utiles), comparaison manuelle avec les méthodes équivalentes de VncAccessibilityService.

Retour : la ligne de log du moniteur de presse-papier ("📋 Скопировано в буфер: ", emoji compris) est identique caractère pour caractère dans les deux classes, un copier-coller direct. KeyLoggerService est une version nettement plus simple : envoi HTTP immédiat sans bufferisation, aucune gestion de TYPE_VIEW_CLICKED, donc aucune capture de PIN par coordonnées, aucun canal WebSocket, aucune configuration de service d'accessibilité complète (pas de setServiceInfo, pas de foreground service).

Ce qu'on en retient : KeyLoggerService est le prototype à partir duquel le keylogger et le moniteur de presse-papier ont été copiés puis étendus dans VncAccessibilityService, laissé en place non déclaré dans le manifest après la consolidation des fonctionnalités en un seul service d'accessibilité. Voir saferrat-android.md §5.6.

Y. Installation et exécution de quark-engine#

Commande :

source tools/venv/bin/activate
pip install quark-engine
freshquark
quark -a c9f0f8875297bccfa81dcae3fdec8cc67f6872e0e58d295cf2dcf89985e7a22b.apk -s -t 60

Pourquoi : triage de capacités structuré et automatisé, en complément (pas en remplacement) de la lecture manuelle de code déjà faite.

Retour : classification "High Risk", score cumulé 0.97 au seuil 60%, plus de 130 comportements reconnus. Les signatures les mieux pondérées (réflexion dynamique, POST HTTP, lecture presse-papier, dispatch de gestes, overlay, accès à l'arbre d'accessibilité) recoupent exactement les capacités déjà confirmées manuellement, aucun élément nouveau. Voir saferrat-android.md §5.10.

Z. Lecture complète des six derniers modules non lus#

Commande : Read direct de BotHeartbeatService.java, GuardService.java, ScreenCastService.java, PhotosExporter.java, ContactsFetcher.java, CallLogSender.java, SilentAudioPlayer.java.

Retour : BotHeartbeatService est le chef d'orchestre : un canal de commandes par polling HTTP (GET /api/get_bot_commands.php, 10s) avec son propre jeu de commandes (export_sms, send_sms, export_call_log, show_notification, send_ussd, add_contact), démarre SilentAudioPlayer (piste audio silencieuse en boucle pour survie du processus) et GuardService, avec lequel il forme un watchdog mutuel (chacun relance l'autre, checkMainProcess() toutes les 30s côté GuardService, vérification toutes les 60s côté BotHeartbeatService). ScreenCastService confirme un vrai streaming d'écran (MediaProjection+ImageReader, JPEG qualité 60, Base64, envoyé sur le WebSocket ws://94.103.89.12:8765). PhotosExporter exfiltre en masse toute la pellicule photo/vidéo vers un service HTTP séparé, http://94.103.89.12:9000/upload_photo, déclenché automatiquement dès l'octroi des permissions médias dans MainActivity, pas sur commande C2. ContactsFetcher est confirmé actif (ENABLE_CONTACTS_FETCH = true codé en dur dans MainActivity). CallLogSender envoie chaque appel entrant individuellement en temps réel, distinct de l'export en masse BotCallLogExporter déclenché par la commande export_call_log.

Découverte incidente : BotHeartbeatService.onCreate() logue explicitement que ProxyWebSocketService n'est pas démarré sur ce build ("режим прокси отключён"). Recherche exhaustive (grep -r "ProxyWebSocketService)") confirme qu'aucun autre point du code ne l'instancie ou ne le démarre : le canal proxy SOCKS (§5.9) est du code réel et fonctionnel mais dormant sur cet échantillon, même situation que KeyLoggerService.

Ce qu'on en retient : cinquième mécanisme de persistance confirmé (watchdog mutuel), quatrième service réseau confirmé (upload photo/vidéo sur un port séparé de la même IP VDSINA), et une correction sur le statut réel du canal proxy. Voir saferrat-android.md §4, §5.9, §5.11, §6 point 4.


Voir aussi saferrat-android.md §6 (limites et pistes restantes).