# Runbook : reproduction pas à pas (chaîne Salat, du script batch à la configuration C2)

Toutes les commandes se lancent depuis `salatstealer-killchain/`. Les scripts Python utilisent `python3 -I` (ou `~/.venvs/ctf-reverse/bin/python -I` quand `capstone` est nécessaire). Rien n'est exécuté : lecture d'octets, désassemblage, calculs hors ligne.

## Choix de la cible

Le tag `SalatStealer` de MalwareBazaar reçoit des centaines d'échantillons par jour. Les rapports publics décrivent déjà les capacités du voleur, l'angle retenu est donc la chaîne de livraison et la protection de la configuration. Le paramètre `limit=1000` de l'API `get_taginfo` renvoie une erreur 502, `limit=500` fonctionne :

```bash
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat ../.mb_api_key)" \
     -d "query=get_taginfo&tag=SalatStealer&limit=500" -o osint/tag_SalatStealer.json
```

## Acquisition et vérification

`tools/mb_pull.py` télécharge par SHA-256 (archive `infected`), vérifie l'empreinte et renomme en `<sha256>.bin` :

```bash
python3 -I tools/mb_pull.py binaries/tag <sha256> [<sha256> ...]
python3 -I tools/mb_pull.py binaries/salat_upx -f osint/salat_upx_hashes.txt   # 204 builds UPX
```

Les 204 hashs UPX sont ceux de l'étiquette `UPX` déposés par un même rapporteur (`osint/salat_upx_hashes.txt`).

## Triage statique

```bash
python3 -I tools/triage.py binaries/tag binaries/extracted
```

Pour chaque fichier : type (PE, UPX, Go, PyInstaller, Rust, .NET), version de Go, chemin de module, nombre de dépendances et drapeaux `-X` des `ldflags`. Constats : deux scripts batch, un chargeur Go aux `ldflags` parlants (`main.pass`, `main.url` en base64), des chargeurs Go amd64 sans `ldflags`, un binaire Go sans symboles, et six conteneurs (.NET, PyInstaller, Rust UPX, NSIS) laissés de côté.

## Décodage du script batch

Le script reconstruit un PE par lignes `echo <base64>>> "%b64%"`. Pour obtenir le PE sans l'exécuter :

```bash
grep '^echo [A-Za-z0-9+/=]*>> "%b64%"$' binaries/tag/e855*.bin | sed 's/^echo //; s/>> "%b64%"$//' | tr -d '\n' | base64 -d > binaries/tag/fcaf1f46...bin
# comparaison de deux scripts : 16 octets différents, tous dans le commentaire final ":: <16 hex>"
cmp -l binaries/tag/e855*.bin binaries/tag/792b*.bin | wc -l
```

Les deux scripts décodés donnent le même PE (`fcaf1f46...`), absent de MalwareBazaar.

## Noms de fonctions dans un binaire Go sans symboles

Les options `-s -w` retirent la table de symboles ELF/PE mais pas `.gopclntab`. `tools/go_pclntab_funcs.py` lit cette table (Go 1.20 et suivants) et affiche adresse, taille et nom de chaque fonction :

```bash
python3 -I tools/go_pclntab_funcs.py binaries/tag/fcaf1f46...bin > osint/fcaf1f46_funcs.txt
grep -E ' (main|5q7z)[./]' osint/fcaf1f46_funcs.txt
```

Résultat sur le chargeur : `main.main` appelle `uac.RunDefaultUAC`, `stealth.RunStealth`, puis déchiffre un blob CBOR, décompresse en zstd, écrit un exécutable `updater_<aléatoire>.exe`, le lance, attend, puis télécharge l'URL décodée.

## Désassemblage annoté

`tools/go_dis.py` désassemble les fonctions dont le nom correspond à une expression régulière et annote chaque appel direct et chaque chaîne Go chargée (motif `lea adresse` puis `mov longueur`) :

```bash
~/.venvs/ctf-reverse/bin/python -I tools/go_dis.py binaries/tag/fcaf1f46...bin 'main\.main' --calls
```

## Extraction de la charge des chargeurs

Les chargeurs `5q7z`/`rogw` (386) et `stub` (amd64) embarquent un blob CBOR `{c: [morceaux], n: [nonces de 12 octets]}`, chaque morceau scellé en ChaCha20-Poly1305, le clair concaténé étant un flux zstd. La clé de 32 octets est une chaîne hexadécimale du binaire (dans les `ldflags` pour `5q7z`/`rogw`). `tools/extract_loader.py` ne présume ni de l'adresse du blob ni de celle de la clé : il localise la map CBOR par ses deux premières clés, essaie chaque chaîne de 64 caractères hexadécimaux et retient celle dont le tag Poly1305 du premier morceau valide :

```bash
python3 -I tools/extract_loader.py binaries/extracted binaries/tag/fcaf1f46*.bin
```

Validation : six chargeurs, six charges (`0a9d56f0...`, `569d2c40...`, `5fcbdcf3...`, `2ab8b874...`, `06aa319f...`, `6321c132...`). Le dépaquetage UPX donne `e575276b...`, `cdb7de4b...`, `498dc093...`.

## Reconstitution du déchiffrement de la configuration de Salat

Lecture de `main.main`, puis `main.dec` (plusieurs modes selon un argument : AES-GCM, XOR cumulatif, XOR par clé, combinaisons des deux, et un mode qui construit l'entier RSA), `main.md5str`, `main.GetHWID`, `main.gb`, `main.getEp` :

```bash
~/.venvs/ctf-reverse/bin/python -I tools/go_dis.py binaries/salat_unpacked/<sha>.upx-dec.bin 'main\.(dec|md5str|gb|getEp|GetHWID)'
```

Points de lecture : `md5str` renvoie les 16 octets bruts (pas l'hexadécimal), donc la clé est de l'AES-128. `GetHWID` pose sur la pile 49 octets (un octet puis douze mots de 32 bits), auxquels s'ajoute un suffixe hexadécimal de 10 caractères lu dans `.rdata` : 54 octets, soit 12 de nonce, 26 de clair et 16 de tag GCM. Pour chaque tableau de 129 mots, le premier mot XOR `0xa5a5a5a5` donne la longueur utile. Le cheminement complet est dans l'en-tête de `tools/extract_salat_config.py`.

Piège rencontré : une version retranscrite à la main de la clé XOR (une lettre cyrillique en minuscule au lieu de la majuscule) fait échouer les tags GCM. La résolution octet par octet (chaque position doit produire des caractères hexadécimaux) l'a révélé. L'extracteur final utilise les octets lus dans le binaire.

## Extraction sur les 204 builds

```bash
tools/batch_salat.sh binaries/salat_upx binaries/salat_unpacked osint/salat_configs.jsonl
python3 -I tools/extract_salat_config.py binaries/salat_unpacked/<sha>.upx-dec.bin --json
```

Les motifs de l'extracteur : tableaux repérés par la séquence `lea esi, [adresse] ; mov ecx, 0x81 ; rep movsd`, module RSA essayé sur chaque candidat jusqu'à ce que les autres tableaux se déchiffrent, identifiant de build lu dans `main.main`. Résultat : 204 configurations, un module RSA, un jeu de dix entrées, 13 valeurs de `bld`.

## Décodage des 75 scripts batch

```bash
python3 -I tools/mb_pull.py binaries/bat -f osint/salat_bat_hashes.txt
python3 -I tools/decode_bat.py binaries/bat --csv osint/salat_bat_decoded.csv --out binaries/bat_pe
```

`decode_bat.py` concatène les lignes `echo <base64>>> "%b64%"`, décode, hache le PE, et calcule un « squelette » (le script sans son commentaire final). Résultat : un seul PE (`fcaf1f46...`, 9 851 392 octets), un seul squelette, 75 commentaires distincts.

## Paquet d'origine du script batch

ThreatFox rattache l'URL GitHub de la seconde charge à un échantillon MalwareBazaar de type ZIP :

```bash
curl -s -X POST https://threatfox-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat ../.mb_api_key)" -H "Content-Type: application/json" \
     -d '{"query":"search_ioc","search_term":"<url de la seconde charge>","exact_match":true}'   # champ "reference"
python3 -I tools/mb_pull.py binaries/tag 5c5548382cec74538a7b81454bfe0868c17d60dc0a74944e92e04b04a12eed0c
7z x -o binaries/extracted/zip_5c554838 binaries/tag/5c554838....bin
( cd binaries/extracted/zip_5c554838 && sha256sum -c SHA256SUMS.txt | grep -v ': OK' )
python3 -I tools/decode_bat.py binaries/extracted/zip_5c554838/zapret-discord.bat
```

Le contrôle des sommes montre `service.bat` listé mais absent, la ligne de `SHA256SUMS.txt` lui-même non conforme, et `zapret-discord.bat` hors liste ; ces deux écarts existent en amont. La comparaison fichier par fichier avec la release légitime donne : 42 fichiers identiques, `service.bat` retiré, `zapret-discord.bat` et `.salt` ajoutés.

```bash
curl -s https://api.github.com/repos/Flowseal/zapret-discord-youtube/releases -o osint/raw/upstream/releases.json   # release dont la date correspond à celle des fichiers de l'archive (1.9.9d)
curl -sL -o osint/raw/upstream/x.zip <browser_download_url>; 7z x -oosint/raw/upstream/x osint/raw/upstream/x.zip
# puis comparaison des SHA-256 fichier par fichier (os.walk + hashlib)
```

Le hash de `bin/winws.exe` est aussi comparé à celui du leurre des binders .NET.

## Binders .NET

`ilspycmd -p` reconstruit le projet et son `.resx` (les ressources en base64). `tools/xbinder_extract.py` lit chaque ressource (4 octets de taille décompressée puis GZip) :

```bash
ilspycmd -p -o /tmp/ilspy_<sha> <binder>.exe
python3 -I tools/xbinder_extract.py /tmp/ilspy_<sha>/<espace>.resx binaries/extracted/xbinder_<sha>
```

La liste `List` du `Program.cs` donne, par fichier : nom, lancement, caché, destination (`%Temp%`) et exclusion Defender (commande PowerShell `Add-MpPreference -ExclusionPath`). Les deux binders déposent le même `Hackmgr.exe` (Salat UPX) avec un leurre différent. L'extracteur de configuration est appliqué au fichier dépacké.

## RAT Python empaqueté avec PyInstaller

```bash
python3 -I tools/pyi_extract.py binaries/tag/3745e309....bin binaries/extracted/pyi_soundcloud
strings -n 5 binaries/extracted/pyi_soundcloud/sc        # entrée 'sc' = script principal (code marshalé, Python 3.14)
```

L'adresse du C2 et son port sont lus dans les constantes du code marshalé (entier `i` suivi de quatre octets) : `77.239.96.159`, port 4444. Les commandes (`STREAM_START`, `WEBCAM_START`, `AUDIO_START`, `GET_PROCS`, `KILL_PROC:`, `UPLOAD_FILE::`, `OPEN_URL::`, `CMD:`) sont des chaînes littérales.

## Build Rust

Les appels directs `call rel32` vers une fonction qu'une analyse automatique ne référence pas se retrouvent par balayage des octets de `.text` ; les débuts de fonction se lisent dans la table `.pdata` (`RUNTIME_FUNCTION`). Les chaînes base64 de la structure de configuration se décodent avec `base64 -d`. `tools/decompile_at.py` accepte une adresse située dans une fonction.

`upx -d` dépack le fichier ; les chemins `/.cargo/registry/src/...` donnent la liste des crates (tungstenite, h3, quinn, rustls, aes-gcm, chacha20poly1305, rusqlite, nokhwa, cpal) et `src/*.rs` la liste des modules. `User-Agent: salat/2.0`, `tonapi.io` et `orbs.network` relient ce build au repli TON de la version Go.

```bash
~/.venvs/ctf-reverse/bin/python -I tools/decompile_refs_range.py <rust.exe> sortie.txt "src/config.rs:0:13" "tonapi.io:30:120"
```

## Exécutables sans symboles ni build info

Le chargeur `b00784d9...` (Go amd64 « garblé ») n'a pas de `ldflags` ni de noms de paquets lisibles, mais l'URL de la seconde charge est en clair dans `.rdata` : `tools/extract_loader.py` la relève et la règle `Salat_Loader_GitHub_Release_URL` la couvre.

## Conteneur à pied de page chiffré (TgWsProxy)

```bash
~/.venvs/ctf-reverse/bin/python -I tools/decompile_importers.py <stub>.exe sortie.txt SetFilePointerEx ReadFile CreateFileW GetModuleFileNameW
~/.venvs/ctf-reverse/bin/python -I tools/decompile_at.py <stub>.exe sortie.txt <adresse> [<adresse> ...]
```

Le stub lit les 16 derniers octets de son propre fichier (offset de l'en-tête, graine, contrôle XOR avec `0x3f8a7d2b`), déchiffre l'en-tête par une clé de 32 bits, puis chaque fichier annexé.

## Recherche de canaux et de contacts embarqués

```bash
python3 -I tools/scan_channels.py binaries/salat_unpacked binaries/extracted binaries/tag binaries/bat_pe | tee osint/channels_scan.txt
```

Liens `t.me`, jetons de bots, webhooks Discord, `.onion`, courriels, comptes GitHub et noms de domaine. Les jetons sont masqués à l'affichage. Résultat sur 446 fichiers distincts : rien d'exploitable (le bruit vient des bibliothèques Go, Python et OpenSSL). Une recherche de chaînes citées par des rapports publics (`nyash`, `Lightshot`, `Procmon`, `RuntimeBroker`, `alive.php`, `sniff_domain_list`, `transferCookie`) donne zéro correspondance ; `webrat` apparaît seulement dans le marqueur de fabrication décrit plus bas, et `Glagol` est le nom d'un alphabet dans la table Unicode de Go (faux positif).

## Marqueur de fabrication et comptes GitHub

```bash
python3 -I tools/extract_pack_marker.py binaries/salat_upx binaries/tag binaries/extracted --csv osint/pack_markers.csv
python3 -I - <<'PY'     # comptes distincts, hors libellé flowseal
import csv; print("\n".join(sorted({r["depot"].split("/")[0] for r in csv.DictReader(open("osint/pack_markers.csv")) if r["etiquette"]=="git-root"})))
PY
python3 -I tools/github_accounts_meta.py osint/github_accounts_from_builds.txt osint/raw/github --repos
python3 -I tools/build_github_table.py osint/pack_markers.csv osint/raw/github osint/github_accounts.csv
```

Le marqueur est dans la zone de remplissage du packer UPX et subsiste après dépaquetage. `github_accounts_meta.py` appelle uniquement `api.github.com/users/<compte>` et `/repos` en anonyme (60 requêtes par heure : l'outil attend la remise à zéro). Aucun fichier de dépôt ni release n'est téléchargé.

## Liste des comptes et canaux

```bash
python3 -I tools/build_accounts_channels.py comptes_et_canaux.md
```

Rassemble les identifiants lus dans les fichiers déjà collectés (GitHub, portefeuilles, domaines, IP, noms cités par des rapports publics) avec leur source et un niveau de confiance. Aucun accès réseau, aucun contact.

## Recoupement avec les rapports publics

```bash
python3 -I tools/osint_pivots.py osint/raw/pivots <domaines et adresses>      # OTX, urlscan, DNS inverse, Internet Archive
curl -s https://otx.alienvault.com/api/v1/pulses/<id>                        # contenu d'une impulsion citée
```

Les rapports lus (CYFIRMA 2025, Solar 4RAYS 2025 et 2026) donnent l'attribution, les tarifs, les domaines de panneau et les portefeuilles. Pour convertir une adresse TON « conviviale » en adresse brute, décoder le base64 URL puis lire l'octet de chaîne et les 32 octets d'adresse, avec la fonction suivante.

```python
import base64
def raw(f):
    b = base64.urlsafe_b64decode(f + "=" * (-len(f) % 4))
    return "%d:%s" % (int.from_bytes(b[1:2], "big", signed=True), b[2:34].hex())
```

## Suivi des portefeuilles de paiement

```bash
python3 -I tools/ton_wallet_flows.py 0:<adresse> osint/raw/ton/<prefixe>
```

Appliqué au portefeuille racine, au portefeuille de juin et au portefeuille « conduit ». Les destinataires des sorties du conduit sont étiquetés par `GET /v2/accounts/<adresse>` (champ `name`). Une bande de prix est définie par le contenu des entrées (9 à 13 TON ou 14 à 17,5 USD₮) et ne sert qu'à compter des payeurs distincts.

## Recherches abuse.ch par indicateur

```bash
curl -s -X POST https://urlhaus-api.abuse.ch/v1/url/ -H "Auth-Key: $(cat ../.mb_api_key)" -d "url=<url>"
curl -s -X POST https://threatfox-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat ../.mb_api_key)" -H "Content-Type: application/json" \
     -d '{"query":"search_ioc","search_term":"wruser.org","exact_match":false}'
```

Le port alternatif `wruser.org:992` est saisi dans ThreatFox depuis le 2026-10-06. Le deuxième compte GitHub (`EmpressRecognize22`) n'y figure pas.

## OSINT passif sur les indicateurs

```bash
python3 -I tools/osint_passive.py osint/raw/passive salator.es wruser.org websalat.top sa1atik.cn salat.cn wrat.in policlinika.top \
    196.196.208.17 196.196.208.18 195.208.21.65 46.173.27.15 168.222.202.171 217.26.28.234 91.142.75.172 62.109.0.189
python3 -I tools/summarize_passive.py osint/raw/passive osint/infrastructure.csv
```

Services tiers uniquement : crt.sh, rdap.org, dns.google, ThreatFox, URLhaus, RIPEstat. Le dossier `osint/raw/` n'est pas publié (réponses RDAP brutes).

## Historique des enregistrements TON

```bash
for d in webratpanel.ton remoteadministratortool.ton webratxaye1.ton shyetootval.ton; do
  curl -s "https://tonapi.io/v2/dns/$d" -o osint/ton/info_$d.json           # adresse de l'élément NFT et propriétaire
  # puis /v2/blockchain/accounts/<élément>/transactions?limit=100 -> osint/ton/tx_$d.json
done
python3 -I tools/ton_dns_history.py osint/ton --csv osint/ton_dns_history.csv
```

Chaque message `change_dns_record` portant la clé SHA-256 de `wallet` donne la valeur écrite. L'adresse est décodée en ASCII après suppression des zéros de remplissage.

## Flux publics du portefeuille racine

```bash
python3 -I tools/ton_wallet_flows.py 0:c441f18d6f9ffe8ba3e4f329af8501fb311fab5f0efb7e96d925f791a89e0451 osint/raw/ton/c441f18d6f
```

Agrégats par mois uniquement dans la sortie ; les adresses de contreparties restent dans `osint/raw/` et ne sont pas publiées. Les micro-virements (moins de 0,01) sont écartés.

## Règles YARA

```bash
yara -w -r salatstealer.yar binaries/            # couverture sur le corpus
yara -w -r salatstealer.yar /usr/bin             # contrôle de faux positifs
```

## Ce qui reste ouvert

- Contenu des `service.exe` des deux comptes GitHub supprimés (ThreatFox classe l'URL du premier compte sous SalatStealer, sans hash de fichier).
- Origine de l'adresse de serveur du Salat réécrit en Rust (chaîne remontée jusqu'au fil d'exécution `0x1400e734f` qui reçoit la configuration ; aucun champ d'adresse dans la structure, les champs vides sont des options d'installation ; deux autres builds Rust ont le même schéma) : la structure de configuration (fonction `0x1400ea1cd`) n'en contient pas, et les appels de la résolution TON (`0x1400dc26a`) n'ont pas été rattachés à une adresse de portefeuille.
- Portefeuille de vente actif depuis juillet 2026 (60 payeurs récents examinés avec `tools/ton_next_payments.py`, aucun destinataire commun ; le portefeuille de juin se vide dans un portefeuille de conservation inactif depuis le 2026-07-22) ; identité des payeurs.
- Aucun canal Telegram, forum ou panneau interrogé (choix, hors périmètre passif).
- Contenu des 158 dépôts thématiques (comparaison avec la release légitime) : seule l'archive référencée par ThreatFox a été comparée.

Les releases des 191 dépôts se lisent avec `python3 -I tools/github_releases_meta.py osint/raw/github_releases osint/github_releases.csv` (métadonnées seulement, 60 appels par heure en anonyme, environ trois heures pour les 191 dépôts).

La page de la note CERT-UA (`cert.gov.ua/article/6287707`) se construit en JavaScript : `google-chrome-stable --headless=new --disable-gpu --no-sandbox --virtual-time-budget=15000 --dump-dom <url>` en sort le texte complet, qu'on réduit ensuite en retirant les balises.

## Archives de release des dépôts jetables

Après accord explicite de téléchargement, les 155 archives `.zip` ont été récupérées et analysées par `python3 -I tools/analyze_release_zips.py osint/github_releases.csv binaries/release_payloads osint/release_zip_analysis.csv` (téléchargement par `curl -L` de `https://github.com/<compte>/<dépôt>/releases/download/<tag>/<fichier>`, SHA-256 comparé au champ `digest`, lecture en mémoire, suppression de l'archive, conservation de la charge seule, aucune exécution). La charge se lit à la fin de `bin/stun.bin` : les 8 derniers octets donnent un décalage, à ce décalage se trouvent `GBZAP1`, la taille sur 8 octets puis la charge. Chaque charge est dépackée avec `upx -d` puis passée à `tools/extract_salat_config.py --json` ; les noms connus de MalwareBazaar se lisent avec `get_info` sur le SHA-256. Les captures d'écran (`screens/`) viennent de `google-chrome-stable --headless=new --screenshot=... --window-size=1400,<hauteur> --virtual-time-budget=20000 <url>` sur des pages publiques.

Les échantillons récents d'une étiquette générique se testent en masse : `get_siginfo` sur `RustyStealer`, filtre sur la date et la taille, `tools/mb_pull.py`, puis recherche des chaînes `salat/2.0`, `src/hwid.rs` et `tonapi.io` (après `upx -d` si besoin).
