# Runbook : RagnarLocker (build non protégé, réutilisation de flux Salsa20)

## Corpus

| SHA256 | Première soumission | Taille | Format |
|---|---|---|---|
| `9706a97ffa43a0258571def8912dc2b8bf1ee207676052ad1b9c16ca9953fc2c` | 2020-08-03 | 38 400 octets | PE32 i386, non packé |
| `dd79b2abc21e766fe3076038482ded43e5069a1af9e0ad29e06dce387bfae900` | 2020-10-01 | 55 808 octets | PE32 i386, non packé |

Outils : PyGhidra (analyse headless), `pefile`, scripts maison dans `tools/`
(`find_api_callers.py`, `find_func_callers.py`, `decompile_addr.py`,
`ragnarlocker_keystream_reuse_poc.py`, `ragnarlocker_recover_file.py`). Aucune exécution réelle de
l'échantillon.

## Acquisition (MalwareBazaar)

```bash
KEY=$(cat ../.mb_api_key)
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $KEY" \
  -d "query=get_taginfo&tag=RagnarLocker&limit=1000" -o /tmp/rl_tag.json
```

25 échantillons taggés `RagnarLocker`. Triés par taille : deux entrées à
5-6 Mo (protégées VMProtect, hors sujet ici) contre une dizaine entre 38 Ko
et 120 Ko (2020-07/08, tag `RagnarLocker` seul ou avec un nom de victime).
Choisi le plus petit et le plus ancien.

```bash
h=9706a97ffa43a0258571def8912dc2b8bf1ee207676052ad1b9c16ca9953fc2c
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $KEY" \
  -d "query=get_file&sha256_hash=$h" -o "binaries/$h.zip"
cd binaries && 7z x -pinfected -y "$h.zip" && cd ..
```

## Vérification directe (ne pas se fier au tag seul)

```python
import pefile
pe = pefile.PE("binaries/9706a97f....exe", fast_load=True)
pe.parse_data_directories()
# entropie max 6.52 sur toutes les sections (contre 7.95+ pour un build VMProtect)
# table d'import complete et normale : 53 KERNEL32, 20 ADVAPI32, 4 CRYPT32, etc.
```

Confirme un binaire natif standard, aucune trace de virtualisation/packer.
Table d'import ADVAPI32/CRYPT32 correspond exactement au schéma documenté
publiquement pour RagnarLocker : `CryptAcquireContextW`, `CryptGenRandom`,
`CryptEncrypt` (génération de clé de session) + `CryptDecodeObjectEx`,
`CryptImportPublicKeyInfo` (import de la clé publique RSA embarquée).

## Localisation de la routine de chiffrement (Ghidra headless)

```bash
python3 tools/find_api_callers.py binaries/9706a97f....exe CryptEncrypt out.txt
python3 tools/find_api_callers.py binaries/9706a97f....exe CryptGenRandom out.txt
```

`CryptEncrypt` n'est appelé qu'à un seul endroit (`FUN_004018c0`), avec un
buffer de sortie `taille_entree * 10` et `Final=TRUE` systématique : sert à
chiffrer via RSA de petits blobs (une clé de session), pas le contenu des
fichiers (Salsa20 n'est pas un algorithme CryptoAPI natif, donc le
chiffrement de masse est forcément une implémentation maison ailleurs dans
le binaire).

`CryptGenRandom` est appelé dans `FUN_004031d0`, qui elle-même est appelée
deux fois dans `entry()` :

```
FUN_004031d0(&DAT_0040a208, 0x28);  // 40 octets aleatoires -> derives
FUN_004031d0(&DAT_0040a230, 0x20);  // 32 octets aleatoires -> derives
```

`FUN_004031d0` ne renvoie pas les octets bruts de `CryptGenRandom` : ils
passent par une construction hash dont l'état initial correspond exactement
aux 8 mots IV de BLAKE2b (`0x6a09e667f3bcc908`, etc., vérifié octet par
octet). Construction saine en elle-même (hash d'un aléa CSPRNG légitime),
pas la source du problème.

Décompilation ciblée des fonctions internes (`decompile_addr.py`, variante
de `find_api_callers.py` qui prend une adresse plutôt qu'un nom de symbole
importé) :

```bash
python3 tools/decompile_addr.py binaries/9706a97f....exe out.txt \
    00407240 004072f0 00406f30 00407430   # construction BLAKE2b
python3 tools/find_func_callers.py binaries/9706a97f....exe 004031d0 out.txt
    # -> decompile entry(), revele l'appel double a FUN_004031d0
python3 tools/decompile_addr.py binaries/9706a97f....exe out.txt \
    004026b0 00402310 004022b0   # coeur Salsa20 + init d'etat
```

## Constat central

`FUN_004026b0` (appelée une fois par fichier, au tout début de
`FUN_00401490`, la routine de chiffrement par fichier) initialise l'état
Salsa20 à partir des DEUX MÊMES globales (`DAT_0040a208`, `DAT_0040a230`)
générées une seule fois par `entry()`, jamais régénérées entre deux
fichiers. Le nonce (positions 6/7 de l'état) provient des 8 derniers octets
de la globale de 40 octets ; le compteur de bloc (positions 8/9) est remis à
zéro à chaque appel. Voir `ragnarlocker-keystream-reuse.md` pour l'analyse complète et la preuve
de concept.

Vérification croisée du premier pas de quart-de-tour décompilé contre la
formule Salsa20 standard (`b ^= rotl(a+d, 7)`) : correspondance exacte,
opérandes et rotation. Confirme que la structure extraite de la
décompilation est fiable, pas une lecture erronée.

## Recherche d'une source publique contredisant ce constat

Recherche web : Kaspersky/Securelist affirme que RagnarLocker génère une
clé ET un nonce Salsa20 uniques par fichier, contredisant directement le
constat ci-dessus. Plutôt que de laisser la contradiction non résolue,
recherche d'un second échantillon plus tardif pour vérifier par
décompilation directe si le défaut a été corrigé entre-temps.

```bash
# dans la liste RagnarLocker deja recuperee : candidats > 2020-08
# 2022-06-07 f4d742d8... (58880 octets) : ECARTE apres verification directe
#   (section .teslaX RWX simultane + table d'import illisible = packe,
#   pas comparable sans travail de deballage)
# 2020-10-01 dd79b2ab... (55808 octets) : retenu, meme profil d'imports
#   ADVAPI32/CRYPT32 que l'echantillon initial, entropie normale, non packe

h=dd79b2abc21e766fe3076038482ded43e5069a1af9e0ad29e06dce387bfae900
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $KEY" \
  -d "query=get_file&sha256_hash=$h" -o "binaries/$h.zip"
cd binaries && 7z x -pinfected -y "$h.zip" && cd ..
```

## Comparaison directe (Ghidra headless)

```bash
python3 tools/find_api_callers.py binaries/dd79b2ab....exe CryptGenRandom out.txt
```

`CryptGenRandom` est toujours dans une fonction équivalente
(`FUN_00404e90`, même structure `CryptAcquireContextW` + `CryptGenRandom` +
mélange 64 octets, mais le mélange utilise maintenant un second appel
`CryptGenRandom` frais au lieu d'une constante fixe). Différence
structurelle immédiatement visible : cette fonction a **deux appelants
distincts** (`FUN_00401a40`, `FUN_00402040`), chacun l'appelant 4 fois, pas
juste `entry()` deux fois.

```bash
python3 tools/find_func_callers.py binaries/dd79b2ab....exe 00404e90 out.txt
```

Décompilation complète des deux appelants : ce sont les routines de
traitement par lots de 64 fichiers (avec un thread par fichier, ajouté
entre les deux versions). `FUN_00404e90` y est appelée **à l'intérieur de
la boucle sur les fichiers du lot**, une fois par fichier :

```c
puVar14 = (uint *)(contexte_fichier + 0xcf24);
FUN_00404e90(puVar14, 0x28);   // secret de 40 octets FRAIS par fichier
puVar10 = (uint *)(contexte_fichier + 0xcf4c);
FUN_00404e90(puVar10, 0x20);   // secret de 32 octets FRAIS par fichier
```

Confirme directement, par lecture de code sur un second échantillon réel,
que le défaut du build du 2020-08-03 est corrigé sur celui du 2020-10-01 :
chaque fichier reçoit ses propres secrets. Voir `ragnarlocker-keystream-reuse.md` §5 pour
l'analyse complète.
