# Runbook : reproduction pas à pas (Qilin Rust)

## A. Acquisition et extraction
```bash
h=61f7aa918b55238278a1666cb723df9e3639d229d1027611e02ae3808ede33ed
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat ~/ctf-reverse/.mb_api_key)" \
  -d "query=get_file&sha256_hash=$h" -o q.zip
7z x -pinfected q.zip; chmod -x $h.exe; sha256sum $h.exe; file $h.exe
```
Retour : `PE32 console i386 stripped`, hash conforme.

## B. Rust, toolchain, crates, crypto
```bash
rabin2 -qz $h.exe > s.txt
grep -ao '/rustc/[a-f0-9]*' s.txt | head -1
grep -aoE '(chacha20|aes|rsa|rand|serde_json|zeroize)-[0-9.]+' s.txt | sort -u
grep -aqF 'expand 32-byte k' $h.exe && echo chacha20
python3 -c "print('aesenc', open('$h.exe','rb').read().count(b'\x66\x0f\x38\xdc'))"
```
Retour : rustc `d5c2e9c3...` ; crates `aes-0.8.3`, `rsa-0.7.2`, `rand-0.8.5`, `serde_json-1.0.105`, `zeroize-1.6.0` ; ChaCha20 présent ; `aesenc = 130` (AES-NI).

## C. Extraction de la config (en clair)
```bash
python3 -c "d=open('$h.exe','rb').read(); import json; \
 i=d.rfind(b'{',0,d.find(b'company_id')); \
 c,_=json.JSONDecoder().raw_decode(d[i:i+20000].decode('utf-8','replace')); \
 print(list(c.keys())); print('company_id',c['company_id'],'n',c['n'],'p',c['p']); \
 print('public',len(c['public_rsa_pem']),'private',len(c['private_rsa_pem']),'pwhash',bool(c['password_hash']))"
```
Retour : config JSON en clair (offset `0x1a7b8c`), schéma avec `password_hash` en plus. `company_id=yd9Ca9y5RN`, `n=0`, `p=1`, public **0**, private **3243**, password_hash présent. Config déplacée dans `samples/` (gitignoré).

## D. Vérification de la clé privée (faille de récupération)
```bash
python3 -c "import json; from cryptography.hazmat.primitives.serialization import load_pem_private_key; \
 c=json.load(open('config_qilin_2025.json')); pem=c['private_rsa_pem']; \
 print('ENCRYPTED' in pem); k=load_pem_private_key(pem.encode(),password=None); print('RSA',k.key_size,'bits')"
```
Retour : PEM **non chiffré**, clé RSA-**4096** valide chargée sans mot de passe. Voir qilin-rust.md §6 (cadrage : exception au niveau du build, pas un cassage universel de Qilin).

## E. YARA
```bash
yara -w qilin.yar $h.exe
```
Voir `qilin.yar`.

## F. Détecteur de fuite de clé + survey d'un lot
```bash
python3 tools/qilin_config_scan.py sample.exe          # verdict sur un fichier
python3 tools/qilin_config_scan.py batch/*             # survey d'un lot
```
Retour : sur 11 échantillons Qilin 2025, 2 exposent leur config en clair (variante Windows PE non packée), et les **2 fuient une clé RSA-4096 privée** (affiliés `yd9Ca9y5RN`, `MFnPJ_k7BE`). Les PE ~5,25 Mo sont packés (config non visible), les ELF portent la note "Qilin" mais pas de config JSON en clair. Voir qilin-rust.md §6bis.

## G. Recherche de clé PEM brute et balayage exhaustif MalwareBazaar
```bash
# scanner ameliore : fallback recherche directe de cle PEM (hors config JSON)
python3 tools/qilin_config_scan.py sample.exe
# balayage de toute la signature/tag Qilin (limite API 2000/j, backoff 429, resumable)
python3 tools/mb_sweep.py --signature Qilin --limit 1000 --out sweep_qilin.csv --keep leaking
python3 tools/mb_sweep.py --tag Agenda --out sweep_agenda.csv
```
Retour : 67 samples Qilin/Agenda checkes (config + PEM brut). **4 fuient** (petite variante EXE 2025, 4 affilies). 0 cle en clair hors config. Voir qilin-rust.md §6bis.

## H. Mécanisme du flip public/privé
```bash
# comparer public_rsa_pem vs private_rsa_pem sur builds qui fuient vs sains
python3 -c "import json; c=json.load(open('config.json')); print('pub',len(c['public_rsa_pem']),'priv',len(c['private_rsa_pem']))"
```
Retour : builds sains = public rempli / private vide ; builds qui fuient = public vide / private RSA-4096 rempli. Deux configs de build distinctes.

## I. Preuve du principe de récupération (sans exécuter le malware)
```bash
python3 tools/recovery_poc.py config_qilin.json   # chiffre modele + dechiffre avec la cle fuitee
```
Retour : roundtrip OK, fichier test identique a l'original. Principe prouve (pas la compat bit-a-bit).

## J. Reference-build (identifier les fonctions crypto du binaire strippe)
```bash
cargo new qilin_ref && cd qilin_ref
cargo add aes@0.8.3 chacha20@0.8.1 ctr@0.8.0 rsa@0.7.2 rand@0.8.5 serde_json@1.0.105 zeroize@1.6.0 sha2@0.10
# remplacer src/main.rs par un main qui appelle chaque primitive (voir tools/reference_build/)
cargo build --release   # -> target/release/qilin_ref AVEC symboles (AES-NI, ChaCha20, RSA OAEP/PKCS1)
nm target/release/qilin_ref | grep -iE 'encrypt_block|chacha|oaep|pkcs1'
```

## K. Reversing du format via Ghidra headless (100% CLI, sans GUI)
Import + analyse complete (reutilisable ensuite via -process) :
```bash
/opt/ghidra/support/analyzeHeadless PROJ_DIR smallvar -import 61f7aa91....exe
```
Script Java post-analyse (les scripts Jython ne marchent pas sans PyGhidra ; Java oui) :
```bash
/opt/ghidra/support/analyzeHeadless PROJ_DIR smallvar \
  -process '61f7aa91....exe' -noanalysis \
  -scriptPath SCRIPTS -postScript monscript.java
```
Localisation de l'orchestrateur de chiffrement (l'ancrage par references IAT echoue, appels
indirects Rust) -> **force brute scriptee** : decompiler toutes les fonctions de taille
moyenne et chercher l'appel dans le C decompile (le decompilateur resout les appels indirects) :
```java
// decgrep.java : pour chaque fonction (120<size<9000), decompiler et tester
//   c.contains("MapViewOfFile") -> imprimer la fonction
```
Retour : sur 1285 fonctions, une seule reference `MapViewOfFile` = `FUN_00548540` (helper
mapping). Son unique appelant = **l'orchestrateur `FUN_00533bc0` (6633 octets)**.

Puis remonter les sous-fonctions crypto/footer :
```java
// getReferencesTo(toAddr(0x548540)) -> appelant ; decompiler FUN_00533bc0
// lister ses appels : FUN_00425270 (x3), FUN_00548c50, FUN_005357a0, ... + memcpy (x9)
```
Reste (itératif, CLI) : matcher ces sous-fonctions au reference-build (cipher/RSA/keygen),
et tracer l'ecriture du pied de fichier. Voir qilin-rust.md §6ter.

Note profil d'entropie (pour la grande variante 5,25 Mo) :
```bash
# .rdata global ~7,4 (donnees Rust) mais region 0x716000-0x777000 a ~7,95 = blob chiffre
# blob non compresse standard (zlib/gzip/lzma/bz2 echouent) -> config chiffree custom
```

---

Fin de l'analyse en l'etat. Points ouverts : voir qilin-rust.md §8.

## L. Confirmation du format de fichier (recoupement public + marqueurs binaires)
```bash
# marqueur de pied de fichier + OAEP présents dans le binaire ?
grep -aoiE 'END CIPHERTEXT BLOCK|begin ciphertext|oaep|label' 61f7aa91....exe | sort -u
```
Retour : `-----END CIPHERTEXT BLOCK-----`, `begin ciphertext`, `oaep`, `label` présents.
Recoupé avec l'analyse publique (AhnLab/ASEC) : **pied de fichier = 550 octets** =
clé AES-256 wrappée RSA-4096-**OAEP** (512 o) + longueur (8 o) + marqueur (30 o).
Cipher = **AES-256-CTR** (AES-NI) sinon ChaCha20. Recoupe exactement le reversing ASM
(FUN_0042f2d0 AES-NI, clés 32 o). Voir qilin-rust.md §6ter.

## M. Décrypteur (variante à clé fuitée)
```bash
# self-test roundtrip (chiffre au format Qilin documente avec la cle fuitee, dechiffre)
python3 tools/qilin_decryptor.py --selftest config_qilin.json
# sur un vrai fichier chiffre :
python3 tools/qilin_decryptor.py --config config_qilin.json --in fichier.chiffre --out fichier.clair
```
Format implemente (recoupe AhnLab/ASEC + derp.ca + ASM local) : pied 550 o =
[512 o cle wrappee RSA-4096-OAEP][8 o longueur][30 o "-----END CIPHERTEXT BLOCK-----"],
corps en AES-256-CTR. Self-test roundtrip OK. Disposition interne du bloc RSA (cle/nonce)
et compteur CTR a confirmer contre un vrai fichier. Voir qilin-rust.md §6ter.
