# Runbook Crytox

## Choix de la cible

Groupe actif depuis 2020, toujours actif en 2025 (rapport Talos IR Q1 2025,
rapport Halcyon mi/fin 2025 sur un nouvel encrypteur maison qui n'utilise
plus HRSword pour désactiver l'EDR). Deux rapports publics documentent
l'implémentation crypto :

- Zscaler ThreatLabz, 2022-09-21, "Technical Analysis of Crytox Ransomware"
  (Romain Dumont) : affirme que la clé AES par fichier provient d'un
  générateur pseudo-aléatoire maison, seedé par `GetTickCount()` (32 bits)
  combine à une constante `config_seed` figée dans la configuration du
  malware. Citation directe du rapport : "Using a 32-bit integer as the
  seed is not sufficient with today's computational power." Neuf hashs
  fournis, un seul présent sur MalwareBazaar (voir Acquisition).
- Halcyon, 2025, "Crytox Consistently Evades Endpoint Security via
  PowerShell" : confirme l'activité récente (mi-2025, dizaines de
  victimes), décrit un nouvel encrypteur maison (RSA-2048 + AES-256 par
  fichier, extension `.wait`, note de rançon `Help.hta`, contact via
  uTox). Un hash cité (`d3d0bbaf2108ef7cee7ae795e31450135acff39fd95b3ec3af5da9a198d90f97`)
  mais absent de MalwareBazaar (échantillon d'engagement IR, non
  soumis publiquement) : impossible à récupérer par le canal habituel.
  Le rapport ne confirme ni n'infirme si le générateur seedé par
  `GetTickCount` de la version 2022 est toujours en place dans ce nouvel
  encrypteur.

**Question ouverte à trancher (même forme que Gunra/Rhysida déjà traités
dans ce dépôt)** : le défaut de seed 32 bits documenté par Zscaler en 2022
est-il toujours présent dans les échantillons qu'on peut effectivement
obtenir, et jusqu'à quelle date ? On ne peut pas répondre pour la version
2025 précise du rapport Halcyon faute d'échantillon public, mais on peut
confirmer/détailler le mécanisme exact sur les échantillons 2020-2021
disponibles, ce qui n'a pas été fait au niveau code (Zscaler ne montre pas
le détail du générateur, juste sa conclusion).

## Acquisition

Recherche par tag MalwareBazaar/ThreatFox infructueuse (`Crytox` n'existe
pas comme tag exploitable, `illegal_tag` sur les variantes avec
underscore). Les 9 hashs cités par le rapport Zscaler et le hash cité par
Halcyon vérifiés un par un via `get_info` : un seul présent sur
MalwareBazaar.

```bash
# Hash directement cite par Zscaler/ThreatLabz comme echantillon analyse
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
  -H "Auth-Key: $(cat ../.mb_api_key)" \
  -d "query=get_file&sha256_hash=32eef267a1192a9a739ccaaae0266bc66707bb64768a764541ecb039a50cba67" \
  -o binaries/32eef267.zip
7z x -pinfected -y -obinaries binaries/32eef267.zip

# Pivot par imphash (365b1d12b684a96b167a74679ec9e4e3, meme builder) :
# get_imphash renvoie exactement 2 resultats, tous les deux 2020-2021
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
  -H "Auth-Key: $(cat ../.mb_api_key)" \
  -d "query=get_file&sha256_hash=d60dc6965f6d68a3e7c82d42e90bfda7ad3c5874d2c59a66df6212aef027b455" \
  -o binaries/d60dc696.zip
7z x -pinfected -y -obinaries binaries/d60dc696.zip
```

Deux échantillons obtenus, même taille (1 287 168 octets), même imphash :

- `32eef267a1192a9a739ccaaae0266bc66707bb64768a764541ecb039a50cba67.exe`
  (first_seen MalwareBazaar 2021-04-08, échantillon directement analysé
  par Zscaler)
- `d60dc6965f6d68a3e7c82d42e90bfda7ad3c5874d2c59a66df6212aef027b455.exe`
  (nom original `worker 5 -t.exe`, first_seen 2020-04-18, même famille)

## Depackaging

```bash
file binaries/*.exe
# PE32+ ... 3 sections -> sections nommees UPX0/UPX1/.rsrc : UPX confirme
# (coherent avec la note Zscaler "packed with UPX")

upx -d -o binaries_unpacked/32eef267....unpacked.exe binaries/32eef267....exe
upx -d -o binaries_unpacked/d60dc696....unpacked.exe binaries/d60dc696....exe
# 1 287 168 -> 3 070 464 octets (ratio 41.92%), UPX standard, depackaging propre
```

**Point d'attention** : le point d'entrée de l'exécutable depackage
(`0x2e8000`, très proche de la fin de l'image) et une table d'imports
statique réduite à 10 entrées (`GlobalAlloc`, `GlobalFree`, `CreateFileA`,
`WriteFile`, `CloseHandle`, `ExitProcess`, `GetModuleFileNameA`,
`GetLastError`, `RegCloseKey`, `MessageBoxA`) suggèrent que le payload
résout lui-même le reste de ses API (dont très probablement
`GetTickCount` et les fonctions `Crypt*`) dynamiquement à l'exécution
(`GetProcAddress` sur des noms construits/lus en mémoire), plutôt que par
table d'imports statique -- motif d'évasion courant, cohérent avec
Halcyon qui décrit explicitement des versions récentes conçues pour
échapper aux outils de sécurité. Confirme par `strings` :
`GetTickCount` apparaît comme **chaîne littérale** dans le binaire
depackage, mais **pas** dans la table d'imports résolue par `pefile` :

```bash
strings -n6 binaries_unpacked/32eef267*.exe | grep -iE "GetTickCount|Crypt"
# -> GetTickCount (chaine seule, pas d'autre match Crypt*)
```

## Chaîne complète de désassemblage : dropper -> PE embarqué -> shellcode injecteur -> payload final

Plutôt que de suivre la piste 1 telle quelle (xrefs Ghidra sur la chaîne
`"GetTickCount"`), la chaîne complète a été demontée couche par couche,
combinant lecture statique et émulation Unicorn ciblée (`tools/
emulate_shellcode.py`, `tools/emulate_stage3_keygen.py`,
`tools/crack_api_hash.py`). Quatre niveaux d'indirection en tout :

1. **Dropper depackage UPX** (`entry()` à `0x1402e8000`) : écrit un PE
   embarque sur disque (`WriteFile`), gère une clé de registre
   (`RegOpenKeyExA`/`RegQueryValueExA`, tailles attendues `0x114`/`0x500`),
   puis énumère les processus en cours (`Process32First`/`Next`) pour
   injecter un shellcode (`FUN_1402e8547`) dans un processus cible via
   `VirtualAllocEx`/`WriteProcessMemory`/`CreateRemoteThread`.
2. **PE embarqué** (extrait à l'offset RVA `0x1000` du dropper, `3 043 328`
   octets, PE32 i386, sha256
   `0a563967a7d48e702adaa6f21f22a80b7db6aae7b7149ede51b0fde583e37283`) :
   confirme être le client **uTox** complet (chaînes "uTox", "utox.io",
   version "0.14.0"), cohérent avec le protocole de contact par messagerie
   uTox documenté publiquement (Halcyon, K7). Utilise ici uniquement comme
   "processus légitime" pour l'injection, pas comme charge utile active.
3. **Shellcode injecte** (`0x1142` = 4418 octets, extrait de
   `0x1402e889e` dans le dropper) : résout ses appels d'API par hash
   (`ROR32` 7 bits sur le nom de module UTF-16 majuscule+NUL, `ROR32` 11
   bits sur le nom de fonction ASCII+NUL, somme, cf `tools/
   crack_api_hash.py` -- algorithme rétro-concu empiriquement, confirmé
   sur plus de 70 hash cassés avec succès au total sur ce dépôt).
   Rouvre le FICHIER DROPPER ORIGINAL sur disque (chemin reçu en
   paramètre), mappe le fichier (`CreateFileA`/`CreateFileMappingA`/
   `MapViewOfFile`), extrait un blob de `0x4400` (17408) octets à l'offset
   fixe `0x135ca4`, **zeroise puis supprime le fichier original**
   (autodestruction anti-forensique confirmée dynamiquement : `rep stos`
   sur toute la vue mappée avant `DeleteFileA`). Déchiffré un petit blob
   de configuration embarquée (`0xc0` octets, AES-NI, clé dérivée des
   tables S-box) révélant des noms de fichiers (`pghdn.txt`, `rwjfk.bat`),
   écrit et **exécuté réellement via `ShellExecuteA`** un script de
   sabotage anti-récupération :
   ```
   for /F "tokens=*" %1 in ('wevtutil.exe el') DO wevtutil.exe cl "%1"
   vssadmin.exe Delete Shadows /All /Quiet
   diskshadow.exe /s ../pghdn.txt
   ```
   (purge des journaux d'événements Windows + suppression de tous les
   clichés instantanés, capture intégrale dynamique, cf
   `tools/emulate_shellcode.py`). Énumère ensuite les processus à la
   recherche de `EXPLORER.EXE` (hash `0xdcf164cd`) ou, à défaut,
   `SVCHOST.EXE` (`0x561f1820`), pour y injecter (mêmes primitives
   `VirtualAllocEx`/`WriteProcessMemory`/`CreateRemoteThread`) le blob de
   `0x4400` octets **déchiffré** (même routine AES-NI, clé dérivée des
   tables S-box à `[r12+0x428]`) : sortie `20 480` octets, capturée
   intégralement via le hook `WriteProcessMemory`.

   Deux bugs réels du HARNAIS D'ÉMULATION (pas du malware) trouvés et
   corrigés pendant cette étape, tous deux des inversions de paramètres
   dans les stubs d'API simulées en Python (à chaque fois confirmés par
   comparaison avec la convention d'appel x64 réelle observée au
   désassemblage) :
   - `memcpy(dest,src,n)` simulé avec `n`/`src`/`dest` sur les mauvais
     registres (`RCX` pris pour la longueur au lieu de la destination) :
     la copie du blob de `0x4400` octets vers le paramètre du thread
     suivant échouait silencieusement (vérification de bornes toujours
     fausse), laissant le blob a zero.
   - `CryptAcquireContextA` simulé avec le pointeur de sortie sur `R9`
     (en réalité `dwProvType`, un entier) au lieu de `RCX` (le vrai
     `phProv`) : ecriture mémoire a l'adresse littérale `0x1`, plantage
     immédiat au tout premier appel API du payload final.

4. **Payload final** (les `20 480` octets déchiffrés et injectés,
   position-indépendant, même mécanisme de résolution d'API par hash) :
   surface d'API cassée via `crack_api_hash.py` (44 constantes de hash
   extraites du binaire, la grande majorité résolues) confirmant sans
   ambiguïté qu'il s'agit du **vrai module de chiffrement de fichiers** :
   `CryptAcquireContextA`/`CryptGenKey`/`CryptExportKey`/`CryptImportKey`/
   `CryptEncrypt`/`CryptDestroyKey` (CryptoAPI, génération et export de la
   paire de clés RSA de session -- cohérent avec la description Halcyon
   "génère une paire de clés RSA-2048 de session... chiffre la clé privée
   de session cinq fois avec la clé publique de l'attaquant"),
   `GetLogicalDrives`+boucle sur les 26 lettres (spawn d'un thread
   `CreateThread` par lecteur local détecté), `WNetOpenEnumW`/
   `WNetEnumResourceW`/`WNetCloseEnum` (énumération des lecteurs réseau,
   confirme la portée "lecteurs locaux ET réseau" documentée
   publiquement), `FindFirstFileW`/`FindNextFileW`/`FindClose` (parcours
   récursif), `CreateFileW`/`ReadFile`/`WriteFile`/`SetEndOfFile`/
   `SetFilePointerEx`/`SetFileAttributesW`/`MoveFileW` (E/S de chiffrement
   par fichier + renommage), `AdjustTokenPrivileges`/
   `LookupPrivilegeValueA`/`OpenProcessToken` (SeDebugPrivilege ou
   similaire), `RegOpenKeyExA`/`RegQueryValueExA`/`RegSetValueExA`/
   `RegCloseKey` (persistance d'un identifiant victime).

## Le générateur pseudo-aléatoire : reconstruction complète et exacte

Localise par lecture directe dans le payload final (offset `0x1396` du
blob de 20 480 octets, immédiatement precede d'un appel `GetTickCount`
dont le hash `0xe2b00ace` est résolu). Code source
reconstitué (variables nommées d'après leur rôle, traduction directe du
désassemblage x86-64, aucune conjecture) :

```c
// r12 : structure de configuration partagee (dechiffree au demarrage du
//       payload par AES-NI depuis un blob de 0x2f30 octets embarque).
// [r12+0x10] : etat 64 bits PERSISTANT du generateur, partage entre TOUS
//              les appels de cette fonction sur tout le run (jamais remis
//              a zero -- confirme par le site d'appel unique de cette
//              fonction dans tout le payload, place juste apres
//              SetFilePointerEx/ReadFile sur le fichier courant : appelee
//              UNE FOIS PAR FICHIER, coherent avec Zscaler "each file is
//              encrypted with a unique AES-256 key generated at runtime").
// [r12+0x58] : diviseur/modulus (valeur issue de la configuration
//              dechiffree, pas une constante figee dans le CODE lui-meme
//              -- seuls le multiplicateur et l'incrementateur ci-dessous
//              sont des constantes de code en dur).
//
// rax porte le "fil" de calcul d'une iteration a l'autre : au tout debut
// du tout premier appel de cette fonction dans tout le run, rax vaut
// GetTickCount() (appele a l'entree de CHAQUE appel, mais seule sa toute
// premiere valeur compte vraiment : ensuite rax est entierement regi par
// la recurrence ci-dessous). A l'interieur d'un tour, rax porte le
// QUOTIENT de la division precedente vers l'iteration suivante ; entre
// deux tours, rax porte le MOT DE SORTIE du tour precedent (pas remis a
// GetTickCount()).
void generer_cle_32_octets(uint64_t *sortie /* rdi, 4 x 8 octets */) {
    uint64_t rax = GetTickCount();             // SEULE injection d'entropie fraiche
    for (int tour = 0; tour < 4; tour++) {     // 4 tours -> 32 octets
        uint64_t acc = 0;
        for (int i = 0; i < 64; i++) {         // 64 iterations par tour
            rax = rax + r12->accumulateur;             // 0x13b7
            rax = rax * 0x7D6C3ULL + 0x159EC7ULL;      // 0x13bc-0x13c3, LCG
            r12->accumulateur = rax;                    // 0x13c9, PERSISTE
            uint64_t haut = rax >> 16;                  // 0x13ce
            uint64_t cube = haut * haut * haut;         // 0x13d8/0x13dc, deux imul
            uint64_t quotient = cube / r12->modulus;    // 0x13e0
            uint64_t reste    = cube % r12->modulus;
            rax = quotient;               // reinjecte dans l'iteration SUIVANTE
            acc = (acc >> 1) | ((reste & 1) << 63);   // 0x13e5, shrd : 1 bit/iteration
        }
        *sortie++ = acc;   // 8 octets de sortie par tour, 4 tours = 32 octets
        rax = acc;         // reinjecte au tour SUIVANT (pas remis a GetTickCount())
    }
}
```

Réimplémentation Python complète et testée : `tools/crytox_lcg_poc.py` (fonction
`derive_key`/`derive_key_chain`, cf docstring pour le détail).

**Constat** : l'intégralité de l'entropie de la clé
AES-256 (32 octets = 256 bits) dérivé d'un **unique appel `GetTickCount`**
(nombre de millisecondes depuis le démarrage de Windows, 32 bits,
résolution typique de 10 à 16 ms sur la plupart des systèmes). Le reste du
mécanisme (multiplicateur/incrémentateur LCG fixés dans le code,
élévation au cube, division modulaire, extraction bit a bit via `shrd`)
est de la mise en forme déterministe et publique : **aucune information
supplémentaire n'est nécessaire pour reproduire intégralement la clé**
au-delà de la valeur de `GetTickCount` au moment de l'infection (bornée
par la durée de fonctionnement du système en millisecondes, un espace de
recherche dérisoire face à un keyspace AES-256 réel). Confirmé et détaillé
au niveau assembleur ce que Zscaler avait seulement pu affirmer par sa
conclusion ("32-bit integer... not sufficient") sans montrer le
mécanisme. `[r12+0x58]` (le "modulus") n'a pas été pin été à une valeur
numérique précise (dépend du contenu de la configuration déchiffrée,
elle-même chiffrée par une clé non extraite ici) mais cela ne change rien
au constat : quel que soit ce modulus, la chaîne entière est une fonction
PUREMENT DÉTERMINISTE d'un seul entier 32 bits public/devinable
(`GetTickCount`).

**Vérification dynamique partielle** : l'émulation Unicorn du payload
final (`tools/emulate_stage3_keygen.py`) confirmé dynamiquement
l'intégralité de la chaîne jusqu'au spawn du thread de chiffrement par
lecteur (bug de paramètre `CryptAcquireContextA` trouvé et corrige au
passage, cf ci-dessus), et jusqu'au tout début de ce thread (un appel
d'API supplémentaire non résolu, hash `0xada80e4c`, non identifié faute de
temps). L'appel réel à `generer_cle_32_octets` n'a pas été atteint
dynamiquement (nécessiterait de résoudre ce dernier hash et de poursuivre
la simulation du parcours de fichiers), mais la lecture statique du code
ci-dessus est sans aucune ambiguïté (structure de boucles, constantes,
séquence d'instructions parfaitement idiomatique d'un LCG) : le niveau de
confiance sur ce point ne dépend pas de cette dernière étape d'émulation.

## Exploitabilité : preuve de concept de récupération de clé

Confirmation directe du site d'appel : `generer_cle_32_octets` (offset
`0x1396`) n'est appelée qu'**une seule fois dans tout le binaire**, juste
après `SetFilePointerEx`/`ReadFile` sur le descripteur du fichier en cours
de traitement (offset `0xc0b`, vérifié par recherche de tous les `call
rel32` ciblant `0x1396`) : confirme un appel **par
fichier**, cohérent avec Zscaler ("each file is encrypted with a unique
AES-256 key generated at runtime"). L'accumulateur `[r12+0x10]` n'est en
revanche jamais réinitialisé entre deux fichiers : la clé du fichier N
dépend de l'état laissé par le fichier N-1, pas seulement d'un nouvel
appel `GetTickCount` isole.

**Conséquence directe pour la récupération** : retrouver la valeur
`GetTickCount()` utilisée pour le **premier fichier** chiffre d'un run
suffit, en rejouant exactement la même récurrence, à reconstruire les clés
de **tous les fichiers suivants du même run** (l'accumulateur est
entièrement déterministe une fois amorce). Le coût de la récupération
complète d'une victime se ramène donc, au pire, au coût d'un seul
brute-force sur un espace de 2^32 valeurs (`GetTickCount` est un compteur
32 bits, résolution ~10-16 ms, boucle tous les ~49,7 jours), pas un
brute-forcé par fichier.

## Extraction réelle du modulus, de la configuration, et validation bit-à-bit

Suite directe de la section précédente : les trois manques identifiés
("modulus non extrait", "format IV/footer inconnu", "aucun fichier de
test") ont été attaques un par un par émulation Unicorn poussée
(`tools/emulate_stage3_keygen.py`, plusieurs passes successives
réutilisant la même instance mémoire pour rejouer les threads que
Unicorn, mono-thread, ne peut pas exécuter nativement).

### Le modulus, obtenu en exécutant réellement le déchiffrement de la config

Hook placé juste après le retour de la routine AES-NI de déchiffrement de
configuration (offset `0x1292`, l'epilogue commun aux deux branches de
`FUN_0047xxxx`-équivalent) : dump direct de la mémoire à `[r12+0x58]`
après déchiffrement réel (pas de réimplémentation manuelle de l'AES
nécessaire, Unicorn/QEMU émulé nativement `aesenc`/`aesenclast`).

**Modulus réel obtenu : `0xa77efce7`** (2 810 117 351), pour l'échantillon
`32eef267a1192a9a739ccaaae0266bc66707bb64768a764541ecb039a50cba67`.

Bonus inattendu : la configuration déchiffrée (12 080 octets) contient
bien plus que le modulus. Structure identifiée par inspection directe des
octets décryptés :

- **Octets 0-3** : le modulus lui-même (`e7 fc 7e a7`, LE) -- confirme
  que `[r12+0x58]` pointe simplement vers le tout début du buffer de
  sortie du déchiffrement, pas un champ spécial.
- **Octets 4 à ~11460** : la **note de rançon complète au format HTA**
  (`<html><head>...<HTA:APPLICATION...`), interprétable directement
  (police, styles CSS, image en base64, script VBScript lançant
  `%windir%\utox.exe`). Contenu de la note : "YOUR FILES ARE ENCRYPTED",
  délai de 5 jours avant suppression de la clé privée, contact prioritaire
  via uTox (ID d'opérateur `204E91D375BADE81DC528EFCC105A5D046DB92FCC4B75F08E151053DCD8D5025DA55D689165D`),
  email de secours **`Colin_Kaan@aol.com`**, limite de 5 Mo pour les
  fichiers de test envoyés par email.
- **Chaînes suivantes** (extraites proprement, mélange de chaînes larges
  UTF-16 et étroites ASCII selon le champ) :
  - `windows`, `ReadMe.hta` : nom du fichier de note déployé
  - `\\?\C:\*.*` : motif d'énumération récursive (chemin long, tous
    fichiers)
  - `.waiting` : **extension de fichier chiffré**, confirme le format
    documente publiquement par Zscaler
  - `SOFTWARE\Microsoft\Windows\CurrentVersion\Run\` : clé de persistance
  - `SeBackupPrivilege`, `SeRestorePrivilege` : privilèges ajustés
    (cohérent avec `AdjustTokenPrivileges`/`LookupPrivilegeValueA` déjà
    relevés dans la surface d'API)
  - `C:\Windows\System32\mshta.exe "C:\ReadMe.hta"` : commande de
    persistance/affichage de la note au démarrage
  - `.waiting\shell\open\command\`, `open` : association d'extension
    (double-clic sur un fichier chiffre relance la note)
  - `MM/dd/yyyy` : format de date (probablement pour le compte à rebours
    de la note, dont le template contient un `01/01/2000` factice non
    substitue dans cet échantillon)
- **Clé publique RSA-2048 de l'attaquant**, format Microsoft CryptoAPI
  `PUBLICKEYBLOB` standard (magic `RSA1` à l'offset `0x2de0` de la config,
  `bType=0x06`, `bVersion=0x02`, `aiKeyAlg=CALG_RSA_KEYX`, `bitlen=2048`,
  exposant `65537`, module extrait et vérifié valide sur 2048 bits). Clé
  **spécifique à ce build/cette victime** (comme documenté par le
  mécanisme de personnalisation par build déjà observé sur d'autres
  familles de ce dépôt, ex. Akira), pas une constante Crytox universelle.

**Toutes ces valeurs sont propres à CET échantillon/build précis** : un
autre échantillon Crytox aurait très probablement un modulus, une clé RSA
et un contact différents (même mécanisme de personnalisation par
configuration chiffrée que les autres familles de ce dépôt), à
re-extraire de la même façon sur un autre sample.

### Validation bit-à-bit de l'algorithme LCG reconstruit

En poussant l'émulation en trois passes successives (bootstrap ->
thread par lecteur `0x7c7` -> fonction renommer+chiffrer `0xa73`,
capturée et rejouée directement une fois son adresse et son paramètre
identifiés via `CreateThread`), l'appel réel à `generer_cle_32_octets`
(`0x1396`) a été atteint et son exécution RÉELLE tracée pas a pas
(hooks sur `0x13c9` et `0x13e0` dans `tools/emulate_stage3_keygen.py`),
avec `GetTickCount` figé à une valeur connue.

**Point à ne pas rater**, visible en comparant l'état interne itération
par itération entre la sortie réelle et `tools/crytox_lcg_poc.py` :
l'instruction réelle est `div DWORD PTR [r12+0x58]`, une **division 32
BITS** (`EDX:EAX` avec `EDX=0` confirmé par traçage, donc en pratique
`EAX / diviseur`), et non une division 64 bits sur le cube entier. Seuls les 32 bits bas du cube
comptent, et le quotient qui en ressort (réinjecte à l'itération
suivante) tient sur 32 bits (`RAX` zéro-étendu par l'instruction `DIV`
elle-même). `tools/crytox_lcg_poc.py` suit cette convention.

**Validation stricte** : `derive_key(tick, 0xa77efce7)`
en Python reproduit **exactement, octet pour octet**, la sortie de la
fonction réelle exécutée dans Unicorn, vérifie sur deux valeurs
`GetTickCount` de test indépendantes (`0x123456` et `0x7777777`). Le
brute-forceur par plaintext connu (`bruteforce_known_plaintext`) a
également été revalidé avec ce modulus réel sur un cas synthétique :
`tick=12345` retrouvé en 0,9 seconde.

### Ce qui reste ouvert

1. **Le format exact de l'IV et du footer du fichier chiffré** n'a
   toujours pas été tracé jusqu'au bout : la fonction `0xa73` (renommer
   + chiffrer un fichier) a été localisée et son début confirmé
   dynamiquement (renommage via `MoveFileW` avant chiffrement, extension
   `.waiting` ajoutée, `CreateFileW` sur le fichier renommé), mais la
   boucle de lecture/chiffrement/écriture elle-même (au-delà de l'appel
   au générateur de clé) n'a pas été rejouée jusqu'à la construction de
   l'IV ni jusqu'au format exact du footer contenant la clé enveloppée en
   RSA.
2. **Aucun fichier réellement chiffré par Crytox** n'est disponible pour
   valider l'oracle contre du ciphertext authentique plutôt qu'un test
   synthétique -- aucun trouve publiquement (voir §Acquisition), et une
   exécution réelle du malware (même en VM isolée jetable, protocole déjà
   utilise sur Akira dans ce dépôt, cf `annexes/preparation-vm-dynamique.md`)
   n'a pas été tentée ici : décision à prendre à tête reposée
   avant de l'entreprendre, vu la nature du risque (exécution réelle d'un
   ransomware), même isolé.
3. Question ouverte non tranchable avec les échantillons actuels (2020/
   2021) : comparer avec la description Halcyon 2025 (nouvel encrypteur
   maison) pour savoir si ce même défaut persiste -- nécessiterait un
   échantillon 2024/2025 réel, non trouvé sur MalwareBazaar/ThreatFox à ce
   jour (2026-07-31).

## Environnement

Identique aux autres analyses de ce dépôt (Ghidra headless via PyGhidra,
venv `~/.venvs/ctf-reverse` sur disque local, PAS sur la clé USB exFAT --
voir la mémoire dédiée à ce piège). Outils génériques copiés depuis
`gunra-time-seed-rng/tools/` (`list_functions.py`, `decompile_by_name.py`,
`find_api_callers.py`, `decompile_addr.py`, `find_string_refs.py`,
`find_constant_refs.py`).
