# KONNI : deux canaux C2, deux clés faciles à retrouver

**Catégorie :** RAT

**Analyste :** Gordon PEIRS

**Famille :** KONNI, outil d'accès distant utilisé par des acteurs nord-coréens liés à l'ombrelle Kimsuky, documenté depuis 2014.

**Méthode :** reverse statique (Ghidra headless via PyGhidra), complété par de l'émulation CPU (Unicorn Engine) là où l'obfuscation empêche la décompilation. Les échantillons ne sont jamais exécutés et il n'y a aucun accès réseau, à aucun moment.

---

## Résumé

Le composant réseau de KONNI parle au C2 par deux canaux HTTP. Chacun a sa propre faille de dérivation de clé AES-256, et elles sont de nature différente.

Le canal **montant** (exfiltration de fichiers) chiffre chaque envoi en AES-256-CTR. La clé vaut `SHA-256(préfixe_statique + horodatage_local)`, où l'horodatage vient de `GetLocalTime()`, à la seconde près. Un défenseur qui connaît le moment de l'upload, ou qui peut le borner, recalcule la clé exacte en testant au plus quelques millions de SHA-256 au lieu des 2²⁵⁶ possibilités théoriques. Une incertitude de 24 heures entières se résout en moins d'une seconde en Python pur.

Le canal **descendant** (configuration et commandes reçues du C2, stockées en local dans un fichier `wiasvc.ini`) utilise la même primitive AES-256-CTR, mais sa clé est une **constante statique** : `SHA-256(UTF16LE("wiasvc"))`, le nom du service sous lequel le malware s'installe. Cette constante est cachée derrière une obfuscation maison (près de 57 000 instructions de bourrage entre l'appel à `GetModuleFileNameW` et le lancement du thread de travail) qui rend la décompilation illisible. Une émulation CPU (Unicorn Engine, sans exécution réelle) suffit pourtant à la faire apparaître en clair dans un registre au démarrage du thread. J'ai validé cette clé en déchiffrant un vrai fichier de configuration récupéré sur MalwareBazaar : on obtient une liste lisible de 21 domaines C2.

Un rapport public de Malwarebytes (janvier 2022, sur une variante plus furtive de KONNI) confirme de son côté une partie du premier mécanisme : l'IV des fichiers envoyés au C2 est bien dérivé d'un appel à `QueryPerformanceCounter`. Ce rapport ne détaille ni la construction de la clé du canal montant, ni le second défaut (canal descendant, clé constante cachée par obfuscation). Ces deux points sont ce que cette analyse apporte.

> Tout est reproductible depuis [`runbook.md`](writeups/konni-native-rat/runbook.md). Les scripts sont dans [`tools/`](tools.html?slug=konni-native-rat).

## 1. Échantillons

| SHA256 | Rôle | Détails |
|---|---|---|
| `b60dc12833110098f5eec9a51749d227db7a12d4e91a100a4fd8815695f1093f` | Composant réseau (DLL de service, `wapsvc.dll`) | PE32+ x86-64, non protégé, exporte `ServiceMain`, imports `WININET.dll` complets |
| `174f1e0c65001b227383b46568b084cdf4fc450485c7363a7ff77bf1ee218652` | Dropper/déployeur | PE32 x86-64, 82 432 octets, dépose et lance le composant réseau via `cmd.exe /c wapsvc.bat` |
| `aa51573f9abcd4a1ec4a61ee7e5811c0279e015ea22bdb787780d67ce7153a57` | Build le plus récent connu (2026-03-17) | Protégé par VMProtect (`.vmp0`/`.vmp1`, RWX, entropie 7,88), écarté de cette analyse, voir §6 |

Les trois échantillons viennent d'un même lot MalwareBazaar tagué `Konni` (2024-06-05 pour les deux premiers). Il existe un build plus récent (mars 2026), mais il est protégé par VMProtect. J'ai choisi de travailler sur les builds non protégés pour aller au bout du mécanisme de chiffrement, au lieu de rester bloqué derrière une couche de virtualisation.

## 2. Le composant réseau : dropper, service, canaux montant et descendant

Le dropper (`174f1e0c...`) détecte l'architecture (`GetNativeSystemInfo`), renomme la bonne variante 32/64-bit du composant réseau en `wapsvc.dll`, puis lance `cmd.exe /c wapsvc.bat` pour l'enregistrer et le démarrer comme service Windows (la DLL exporte bien `ServiceMain`).

Le composant réseau (`b60dc128...`) implémente deux canaux HTTP vers le C2, tous deux lisibles en clair dans le code décompilé :

- **Descendant** (`FUN_180004640`) : `GET <host>/dn.php?name=<...>&prefix=<...>`, la réponse brute est écrite sur disque. Il sert à récupérer la configuration et les commandes, et éventuellement des payloads supplémentaires.
- **Montant** (`FUN_1800049b0`) : `POST <host>/up.php?name=<...>`, upload multipart d'un fichier préalablement chiffré. Il sert à exfiltrer des documents (avec une compression en plus pour les archives `.7z`/`.zip`/`.rar`/`.cab`/`.docx`/`.xlsx`), des captures d'écran, ou la sortie de commandes shell (`systeminfo`, `tasklist`) lancées par des chaînes de commande visibles en clair (`PTR_u_cmd__c_systeminfo_>...`, `PTR_u_cmd__c_tasklist_>...`).

## 3. SHA-256 et AES-256-CTR authentiques, confirmés par décompilation

Il y a deux jeux de fonctions cryptographiques, réutilisés à plusieurs endroits :

- `FUN_180003510` (compression SHA-256, avec les constantes standard `K[0..63]`) et `FUN_1800038e0` (finalisation : padding, longueur en bits, sortie big-endian). C'est un SHA-256 conforme, qui sert à dériver des clés et, ailleurs dans le binaire, à hacher un nom de fichier.
- `FUN_180003b90` (expansion de clé Rijndael, table S-box `DAT_180013af0` et constantes de tour `UNK_180013cf0`, boucle jusqu'à l'indice `0x3c` = 60 mots) et `FUN_180003db0` (14 tours de chiffrement, incrément de compteur big-endian avec propagation de retenue en fin de bloc, XOR du keystream et du clair). C'est un AES-256 en mode CTR conforme : 60 mots de sous-clés et 14 tours sont la signature d'AES-256, contre 44 mots et 10 tours pour AES-128.

`FUN_180002e90`, le point d'entrée de l'exfiltration appelé depuis `FUN_1800049b0`, orchestre le tout :

```c
QueryPerformanceCounter(&compteur);              // 8 octets
SHA256(compteur, 8, &iv16);                       // IV/compteur CTR, tronque a 16 octets : SAIN
FUN_180003a70(&nom_fichier, longueur, &cle32);    // SHA-256(nom_fichier) -> cle AES-256 : PROBLEME, voir §4
FUN_180003b90(&contexte, &cle32);                 // expansion de cle Rijndael
FUN_180003db0(&contexte, donnees, taille);        // AES-256-CTR en place
// sortie = iv16 || donnees_chiffrees
```

## 4. Le défaut : la clé AES-256 est un horodatage haché, pas un secret

Le « nom de fichier » haché pour faire la clé n'est pas un identifiant quelconque. C'est une chaîne construite juste avant l'appel par `vswprintf`. J'ai lu le désassemblage brut, et pas seulement le pseudocode où les arguments variadiques sont simplifiés, pour avoir l'ordre exact des paramètres passés sur la pile :

```
format = L"%s%02d-%02d %02d-%02d-%02d.txt"
args   = (DAT_180013ee8, wMonth, wDay, wHour, wMinute, wSecond)
```

`DAT_180013ee8` est une constante wide-string en `.rdata` (présente dans le fichier, pas dans une zone mise à zéro au chargement) : `"rr "`. Les cinq champs numériques viennent d'un `SYSTEMTIME` rempli juste avant par `GetLocalTime()`, à la seconde près. La chaîne finale a donc la forme `"rr MM-JJ HH-MI-SS.txt"`, par exemple `"rr 03-22 14-05-33.txt"`, et :

```
clé_AES256 = SHA-256("rr " + "MM-JJ HH-MI-SS.txt")
```

Le seul élément imprévisible de cette chaîne est l'heure locale de la machine victime au moment de l'upload, à la seconde près. Ce n'est ni un secret partagé avec le C2 ni une valeur issue d'un aléa cryptographique. C'est l'horloge système, et dans presque tous les cas réels de réponse à incident on peut la borner précisément : journaux de proxy ou de pare-feu horodatés, en-tête HTTP `Date` de la réponse du C2 (si l'infrastructure de l'attaquant la journalise ou si elle est observée en transit), fuseau horaire et plage d'activité connue de la victime.

## 5. Preuve de concept : réduction de 2²⁵⁶ à quelques millions de calculs SHA-256

`tools/konni_upload_crypto.py` réimplémente tout le schéma en Python, validé par auto-test (un chiffrement puis un déchiffrement cohérents avec la même reconstruction de clé). Le script de récupération, `tools/konni_upload_key_bruteforce.py`, prend un blob capturé (compteur CTR de 16 octets + chiffré) et une fenêtre de temps plausible. Il teste chaque seconde de la fenêtre et reconnaît le bon déchiffrement à la signature du fichier (`PK\x03\x04` pour zip/docx/xlsx, `Rar!`, `7z\xbc\xaf`, `MSCF` pour cab, ou du texte imprimable).

Test de bout en bout : je chiffre un texte de test avec un horodatage que l'outil de récupération ne connaît pas (`2024-03-22 14:37:52`), puis je le retrouve par brute-force sur une fenêtre d'une heure :

```
$ python3 konni_upload_key_bruteforce.py blob.bin 2024-03-22_14:00:00 2024-03-22_15:00:00
[*] fenetre de recherche : 3601 seconde(s)
[+] cle retrouvee, horodatage local victime : 2024-03-22 14:37:52 (2273 candidat(s) teste(s))
[+] 548 octet(s) recuperes, apercu : ...
```

Sur une incertitude complète de 24 heures (86 400 secondes), retrouver la clé et déchiffrer le contenu prend moins d'une seconde en Python pur sur mon poste d'analyse.

## 6. Le canal descendant : une constante cachée sous obfuscation, retrouvée par émulation CPU

Le canal descendant (config et commandes reçues du C2 via `dn.php`, stockées en local dans un fichier `wiasvc.ini`) utilise la même primitive AES-256-CTR. Sa clé est dérivée d'un matériau (`DAT_18001a4d0`) qui n'est pas lisible en clair dans le désassemblage statique : il est copié depuis un paramètre reçu au démarrage du thread de travail (`wcscpy_s(&DAT_18001a4d0, 0x104, param_1 + 0x104)`).

Le script d'installation, `wiasvc.bat`, est dans le même lot MalwareBazaar. Il installe le composant réseau comme un service Windows générique (`sc create wiasvc binpath= "%windir%\System32\svchost.exe -k wiasvc" ...`) et ne passe aucun secret, ni en ligne de commande ni dans le registre. Il référence un fichier `wiasvc.ini` et le supprime après l'installation, ce qui confirme que c'est bien le fichier de config chiffré local.

Le paramètre passé au thread de travail (`param_1`) est construit par `ServiceMain`, dans un code qui enchaîne `GetModuleFileNameW` puis un appel à une fonction (`FUN_18011e3b2`) que Ghidra n'arrive pas à désassembler proprement (`Control flow encountered bad instruction data`, `halt_baddata()`). Cette fonction patche un octet en mémoire à une adresse calculée, puis continue dans le code qu'elle vient de modifier. C'est de l'anti-désassemblage classique par code auto-modifiant.

**Émulation CPU (Unicorn Engine, x86-64, `tools/emulate_service_init.py`)**, sans exécution réelle ni accès réseau. Je charge le PE en mémoire émulée avec un TEB/PEB minimal (registre `GS_BASE`). La résolution dynamique des API est interceptée, y compris `GetProcAddress`, appelé à l'exécution pour obtenir `RegisterServiceCtrlHandlerW`/`CreateThread` qui ne sont pas dans la table d'import statique. Un hook sur les écritures en mémoire exécutable suit le code auto-modifiant (avec `ctl_remove_cache` d'Unicorn pour invalider le cache de traduction après chaque patch). L'émulation démarre à `ServiceMain` et j'intercepte le premier `CreateThread`. Le paramètre passé au thread de travail contient deux chaînes : le chemin du fichier `.ini` local et, à l'offset `+0x104`, la chaîne `"wiasvc"`.

**Test de contrôle** : j'ai remplacé le chemin de module fourni à l'émulation (`GetModuleFileNameW`) par une valeur arbitraire (`ZZZZZZZZ.dll` au lieu de `wiasvc.dll`). La chaîne récupérée au `CreateThread` ne change pas, c'est toujours `"wiasvc"`. Elle n'est donc pas dérivée du nom du fichier sur disque. C'est une **constante figée**, indépendante de l'environnement d'exécution, cachée par les ~57 000 instructions de bourrage que l'émulation traverse entre les deux appels.

```
clé_config = SHA-256(UTF16LE("wiasvc"))
```

**La validation qui tranche** : le même lot MalwareBazaar contient un fichier de 611 octets (hash `a56a8217bad8523c3f2f163a69a50fba79142ab0a71b4a1443fb4143847e7146`, tagué `unknown`) dont la taille et l'entropie collent avec une instance capturée de `wiasvc.ini`. Avec cette clé (`tools/konni_config_crypto.py`), il se déchiffre en une liste lisible de 21 domaines C2 (voir §8). La clé et le schéma de chiffrement du canal descendant sont donc confirmés d'un coup.

`"wiasvc"` est le nom de service choisi pour cette campagne (il se fait passer pour « Windows Image Acquisition Service », ce que confirme `wiasvc.bat`). Un autre build ou une autre campagne peut utiliser un autre nom de service, et la clé changerait avec. Ce qui se généralise à ces variantes, c'est la méthode (émuler jusqu'au `CreateThread` et lire le paramètre), pas le résultat `"wiasvc"`.

## 7. Ce qui n'a pas été résolu

- Je n'ai pas examiné le build de mars 2026 (`aa51573f9a...`), protégé par VMProtect. Sans lever cette protection, impossible de dire si les deux défauts de dérivation de clé existent encore sur les builds récents.
- Je n'ai utilisé aucune capture de trafic réel, puisque je m'interdis d'exécuter l'échantillon ou de contacter une infrastructure C2. Les PoC sont validés soit par auto-test de la réimplémentation Python (canal montant), soit sur un vrai fichier de configuration récupéré sur MalwareBazaar, jamais sur une capture réseau en direct (canal descendant).
- Je n'ai pas regardé les autres fichiers du lot MalwareBazaar (une `.dll` de 4,5 Mio, un `.cab`, deux `.msi`).
- L'obfuscation de `FUN_18011e3b2` (code auto-modifiant, ~57 000 instructions de bourrage) n'a été traversée que par émulation, pour cette clé. Je n'ai pas étudié son fonctionnement interne (quel obfuscateur, outil tiers ou développement maison) au-delà de ce qu'il fallait pour extraire la constante.

## 8. Indicateurs de compromission

| Type | Valeur |
|---|---|
| SHA256 (composant réseau, `wapsvc.dll`/`wiasvc.dll`) | `b60dc12833110098f5eec9a51749d227db7a12d4e91a100a4fd8815695f1093f` |
| SHA256 (dropper) | `174f1e0c65001b227383b46568b084cdf4fc450485c7363a7ff77bf1ee218652` |
| SHA256 (installeur, `wiasvc.bat`) | `b792867d9275c0ed64cbb501431fe0368c08ee3fa284a72590e46881241b2076` |
| SHA256 (config C2 capturée, `wiasvc.ini` chiffré) | `a56a8217bad8523c3f2f163a69a50fba79142ab0a71b4a1443fb4143847e7146` |
| SHA256 (build récent, VMProtect, non analysé) | `aa51573f9abcd4a1ec4a61ee7e5811c0279e015ea22bdb787780d67ce7153a57` |
| Noms de fichiers/service déposés | `wapsvc.dll`/`wiasvc.dll` (variantes selon campagne), `wiasvc.ini`, `wiasvc.bat`, service Windows `wiasvc` ("Windows Image Acquisition Service") |
| Chemins C2 | `/dn.php?name=...&prefix=...` (GET, descendant), `/up.php?name=...` (POST, montant) |
| Préfixe statique de dérivation de clé (canal montant) | `"rr "` |
| Format de l'horodatage utilisé comme matériau de clé (canal montant) | `MM-JJ HH-MI-SS` (heure locale victime, `GetLocalTime`, granularité seconde) |
| Matériau de clé du canal descendant | `"wiasvc"` (nom de service, propre à cette campagne) |
| Domaines C2 (extraits du fichier de config déchiffré) | `victory-2024.mywebcommunity.org`, `3cym4ims.medianewsonline.com`, `j1p75639.medianewsonline.com`, `99695njd.myartsonline.com`, `mhhnv7s9.myartsonline.com`, `g66nzt8q.mygamesonline.org`, `p593d8g9.mygamesonline.org`, `mbfasq54.mypressonline.com`, `tl2j38w9.mypressonline.com`, `t8nptw2h.mywebcommunity.org`, `w9uzs9la.mywebcommunity.org`, `zcvbm1zv.onlinewebshop.net`, `zomfaa9a.onlinewebshop.net`, `694qf6w8.scienceontheweb.net`, `24ev0apa.scienceontheweb.net`, `c6cdg4su.sportsontheweb.net`, `5s6bqbea.sportsontheweb.net`, `jbkza9h7.atwebpages.com`, `88zr7cua.atwebpages.com`, `p8tebfel.getenjoyment.net`, `cor8xcib.getenjoyment.net` |

## 9. Divulgation et usage

Les échantillons étudiés sont publics (MalwareBazaar) et l'outil est documenté depuis 2014. Un rapport public de Malwarebytes (2022) confirme l'usage de `QueryPerformanceCounter` pour l'IV des uploads, ce qui recoupe une partie de ce travail, mais il ne détaille ni la construction de la clé AES-256 du canal montant ni la constante cachée qui protège le canal descendant. Ce que j'apporte : la description précise de ces deux dérivations de clé, la démonstration par des réimplémentations qui fonctionnent que la confidentialité des deux canaux C2 de KONNI ne tient pas (l'un repose sur l'horloge locale de la victime, l'autre sur une constante qu'on extrait par émulation malgré l'obfuscation), et une liste vérifiée de domaines C2 liés à cette campagne. Une équipe de réponse à incident qui dispose d'un trafic capturé, d'un fichier de configuration local ou simplement de l'échantillon a ainsi un moyen concret de récupérer du contenu chiffré sans accès à l'infrastructure de l'attaquant.

## 10. Reproduire l'analyse

Le journal détaillé est dans [`runbook.md`](writeups/konni-native-rat/runbook.md). Le PoC et l'outil de récupération sont dans [`tools/`](tools.html?slug=konni-native-rat) (`konni_upload_crypto.py`, `konni_upload_key_bruteforce.py`, `konni_config_crypto.py`, `emulate_service_init.py`). Le guide de détection et de remédiation : [`remediation.md`](writeups/konni-native-rat/remediation.md).
