# Runbook : reproduction pas à pas (KONNI, lot MalwareBazaar 2024-06-05)

## A. Recherche de l'échantillon

```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=KONNI&limit=1000" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for e in sorted(d.get('data', []), key=lambda x: x.get('first_seen',''), reverse=True)[:20]:
    print(e.get('first_seen'), e.get('sha256_hash'), e.get('file_type'), e.get('file_size'))
"
```

53 échantillons taggés `Konni`. Le plus récent (`aa51573f9a...`, 2026-03-17)
s'avère protégé par VMProtect (`.vmp0`/`.vmp1` RWX, entropie 7,88, table
d'import quasi vide) : écarté après vérification directe avec `pefile`,
piste conservée pour un travail futur mais hors de portée immédiate (même
type de blocage que RagnarLocker/VMProtect dans ce dépôt). Recherche de
recherche d'un échantillon `Kimsuky`/`AppleSeed` comme piste alternative
initialement envisagée : AppleSeed déjà bien documenté publiquement
(RC4 + clé dérivée MD5, Malwarebytes/Telsy), KONNI retenu car moins
documenté au niveau algorithme précis.

Lot retenu, non protégé : `2024-06-05`, plusieurs fichiers sous un même
horodatage MalwareBazaar (`.exe` x3, `.dll`, `.cab`, `.msi` x2, `.bat`,
`.unknown`). Récupération des trois `.exe` :

```bash
h1=174f1e0c65001b227383b46568b084cdf4fc450485c7363a7ff77bf1ee218652  # dropper
h2=b60dc12833110098f5eec9a51749d227db7a12d4e91a100a4fd8815695f1093f  # composant reseau
h3=aa51573f9abcd4a1ec4a61ee7e5811c0279e015ea22bdb787780d67ce7153a57  # build recent, VMProtect
for h in $h1 $h2 $h3; do
  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 ..
done
```

## B. Triage

```python
import pefile
for h in (h1, h2, h3):
    pe = pefile.PE(f"binaries/{h}.exe", fast_load=True)
    pe.parse_data_directories()
    print(h, pe.get_warnings())
    for s in pe.sections:
        print(" ", s.Name.decode(errors="ignore").strip("\x00"), round(s.get_entropy(), 2))
```

`h1` (82 432 octets) : sections normales, imports CRT + `WinExec`/
`MoveFileW`/`DeleteFileW` (profil dropper). `h2` (1 156 608 octets, PE32+
x86-64) : imports `WININET.dll` complets, section non standard `.piuy0`
(code+données, entropie ~6,8 uniforme, pas de packing façon VMProtect).
`h3` : `.vmp0`/`.vmp1` RWX, table d'import quasi vide, écarté (voir §A).

## C. Décompilation Ghidra headless (PyGhidra), vue d'ensemble

```bash
source ~/.venvs/ctf-reverse/bin/activate
python3 tools/entry_and_overview.py binaries/$h2.exe tools/overview_b60dc128.txt
```

Deux points d'entrée : `entry` (DllMain classique) et `ServiceMain`
(export nommé, confirme un service Windows). 456 fonctions.

## D. Localisation de la logique réseau

```bash
python3 tools/find_api_callers.py binaries/$h2.exe InternetOpenW tools/out_internetopen.txt
python3 tools/find_api_callers.py binaries/$h2.exe HttpSendRequestW tools/out_httpsend.txt
```

Deux fonctions distinctes : `FUN_180004640` (`GET .../dn.php?name=...&prefix=...`,
canal descendant, écrit la réponse brute sur disque) et `FUN_1800049b0`
(`POST .../up.php?name=...`, canal montant, chiffre le fichier avant
upload multipart). C'est dans `FUN_1800049b0` que la routine de chiffrement
(`FUN_180002e90`) est appelée.

## E. Identification des primitives cryptographiques

```bash
python3 tools/decompile_addr.py binaries/$h2.exe tools/decomp_crypto.txt \
  0x180002e90 0x180003a70 0x180005ef4 0x1800050f0
python3 tools/decompile_addr.py binaries/$h2.exe tools/decomp_crypto2.txt \
  0x180003b90 0x180003db0 0x180003510 0x1800038e0 0x180005f98
```

Constantes IV SHA-256 (`0x6a09e667`, `0xbb67ae85`, etc.) reconnues dans
`FUN_180002e90` et `FUN_180003a70` : confirme `FUN_180003510` (compression)
et `FUN_1800038e0` (finalisation) comme une implémentation SHA-256 standard.
`FUN_180003b90` (boucle jusqu'à l'indice `0x3c` = 60 mots de sous-clés) et
`FUN_180003db0` (14 tours, incrément de compteur big-endian en fin de
boucle) confirmés comme AES-256 en mode CTR (60 mots/14 tours = signature
AES-256, contre 44 mots/10 tours pour AES-128).

## F. Reconstruction du schéma de dérivation de clé

Dans `FUN_180002e90` : `QueryPerformanceCounter` puis `SHA256` tronqué à 16
octets pour le compteur CTR initial (IV, sain) ; `FUN_180003a70` (SHA-256)
appliqué à une chaîne construite juste avant par `vswprintf`
(`FUN_180005f98` -> `_vswprintf_s_l`), pour produire la clé AES-256.

Le pseudocode Ghidra simplifiait les arguments variadiques de l'appel
`vswprintf` ; désassemblage brut nécessaire pour retrouver l'ordre exact
des paramètres :

```bash
python3 -c "
import pyghidra
pyghidra.start(install_dir='/opt/ghidra')
with pyghidra.open_program('binaries/$h2.exe', analyze=True) as flat_api:
    program = flat_api.getCurrentProgram()
    listing = program.getListing()
    fm = program.getFunctionManager()
    af = program.getAddressFactory()
    f = fm.getFunctionContaining(af.getAddress('180004a8c'))
    it = listing.getInstructions(f.getBody(), True)
    started = False
    for instr in it:
        if str(instr.getAddress()) >= '1800049f0': started = True
        if started: print(instr.getAddress(), instr)
        if str(instr.getAddress()) >= '180004a95': break
"
```

Résultat : `R8=&DAT_180013ea8` (format `L"%s%02d-%02d %02d-%02d-%02d.txt"`),
`R9=&DAT_180013ee8` (1er vararg, `%s`), puis sur la pile dans l'ordre
`wMonth`, `wDay`, `wHour`, `wMinute`, `wSecond` (chargés depuis un
`SYSTEMTIME` rempli par `GetLocalTime()` juste avant, offsets `+2` cohérents
avec la structure `SYSTEMTIME` standard). Vérification que `DAT_180013ee8`
est bien une constante présente dans le fichier (`.rdata`, pas la zone
`.data` mise à zéro au chargement) :

```python
import pefile
pe = pefile.PE(f"binaries/{h2}.exe", fast_load=True)
rva = 0x180013ee8 - pe.OPTIONAL_HEADER.ImageBase
print(pe.get_data(rva, 8).decode("utf-16le"))  # "rr "
```

## G. Recherche d'une source publique corroborant ou contredisant ce constat

Recherche web : un rapport Malwarebytes (janvier 2022, variante "stealthier"
de KONNI) confirme indépendamment que l'IV des fichiers envoyés au C2 est
dérivé d'un appel `QueryPerformanceCounter`, et mentionne une méthode
"timestamp-based" pour les URLs d'exfiltration, sans détailler la
construction de la clé AES elle-même ni confirmer que l'horodatage sert
directement de matériau de clé. Traité comme une corroboration partielle,
pas une contradiction : le mécanisme d'IV est cohérent avec cette analyse,
le point nouveau (l'horodatage comme clé, pas seulement comme IV ou nom de
fichier) n'est pas couvert par la source publique trouvée.

## H. Vérification de la source du canal descendant (config/commandes)

```bash
python3 tools/find_addr_writes.py binaries/$h2.exe tools/writes_18001a4d0_all.txt 0x18001a4d0
python3 tools/decompile_addr.py binaries/$h2.exe tools/decomp_keysrc.txt 0x180001440
```

`DAT_18001a4d0` (matériau de clé du canal descendant, `FUN_180001890`/
`FUN_180002ab0`) est rempli par `wcscpy_s(&DAT_18001a4d0, 0x104, param_1 + 0x104)`
dans `FUN_180001440` (boucle de travail principale), où `param_1` est le
paramètre du thread de travail, pas une constante du binaire. Confirmé par
inspection de section (`.data`, offset au-delà de `SizeOfRawData`, donc zone
mise à zéro au chargement, pas de contenu réel dans le fichier). Ce canal
n'est donc pas cassable par cette seule analyse statique : voir konni-native-rat.md
§6. Vérification du dropper (`h1`) pour la source de ce paramètre :
`FUN_00401000` lance `wapsvc.bat` sans qu'aucune écriture de registre
contenant un mot de passe ne soit visible dans ce binaire précis ; probable
construction de la ligne de commande dans `wapsvc.bat` lui-même, non
récupéré dans cette analyse.

## I. Preuve de concept et validation

```bash
python3 tools/konni_upload_crypto.py          # auto-test chiffrement/dechiffrement
python3 -c "
from tools.konni_upload_crypto import encrypt_upload
blob = encrypt_upload(b'PK\x03\x04' + b'A'*500, month=3, day=22, hour=14, minute=37, second=52)
open('/tmp/konni_test_blob.bin','wb').write(blob)
"
python3 tools/konni_upload_key_bruteforce.py /tmp/konni_test_blob.bin \
  2024-03-22_14:00:00 2024-03-22_15:00:00
```

Résultat : clé retrouvée en 2273 essais (fenêtre d'une heure), moins d'une
seconde. Test complémentaire sur une fenêtre de 24 heures complètes
(86 400 essais) : moins d'une seconde également. Voir `konni-native-rat.md` §5.

## J. Le canal descendant : récupération du script d'installation manquant

Retour sur le lot MalwareBazaar complet (§A) : un fichier `.bat`
(1162 octets) et un fichier `unknown` (611 octets) n'avaient pas été
récupérés initialement.

```bash
h4=b792867d9275c0ed64cbb501431fe0368c08ee3fa284a72590e46881241b2076  # .bat
h5=a56a8217bad8523c3f2f163a69a50fba79142ab0a71b4a1443fb4143847e7146  # unknown, 611 o
for h in $h4 $h5; do
  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 ..
done
```

`$h4.bat` (lu directement, script en clair) : installe le composant réseau
comme service Windows générique sous le nom `wiasvc` (masquerade "Windows
Image Acquisition Service"), via
`sc create wiasvc binpath= "%windir%\System32\svchost.exe -k wiasvc" ...`
et une clé de registre `ServiceDll`. Copie `wiasvc.dll` et `wiasvc.ini` dans
`System32`, supprime les originaux après. **Aucun secret ni argument de
ligne de commande contenant une clé** n'apparaît dans ce script : confirme
que le matériau de clé du canal descendant ne vient ni du registre ni de la
ligne de commande du service.

`$h5.unknown` (611 octets, entropie elevée) : taille et profil compatibles
avec une instance capturée de `wiasvc.ini` (le fichier de config chiffré
référencé et supprimé par le `.bat`), sans confirmation directe par nom de
fichier (voir §K pour la validation par déchiffrement réussi).

## K. Trace de la construction du paramètre secret par émulation CPU (Unicorn)

Recherche de la source de `DAT_18001a4d0` (le matériau de clé du canal
descendant) : confirmé écrit depuis `param_1 + 0x104` dans `FUN_180001440`
(voir §H), mais le seul appelant retrouvé par recherche de références
statiques était une entrée de table de métadonnées de gestion d'exceptions
générée par le compilateur (`18011e4e8`), pas un site d'appel réel.

Décompilation de `ServiceMain` en détail : appelle
`RegisterServiceCtrlHandlerW`/`SetServiceStatus` via des pointeurs de
fonction (`DAT_1800196b0`/`DAT_1800196a8`) résolus dynamiquement par
`GetProcAddress`, pas via la table d'import statique (explique l'absence de
ces symboles dans `pefile.DIRECTORY_ENTRY_IMPORT`). Juste avant le
lancement du worker, `ServiceMain` appelle `FUN_180001250`, qui appelle
`FUN_18011e3b2` :

```bash
python3 tools/decompile_addr.py binaries/$h2.exe tools/decomp_hidden.txt 0x18011e3b2
```

Échec de décompilation : `Control flow encountered bad instruction data`,
`halt_baddata()`. Lecture du pseudocode partiel : la fonction patche un
octet en mémoire à une adresse calculée (`*(char*)(ptr+0x1f) -= 0x68`) avant
de continuer l'exécution dans ce code désormais modifié : code
auto-modifiant, anti-désassemblage délibéré.

Construction d'un harnais d'émulation CPU pure (`tools/emulate_service_init.py`,
Unicorn Engine, x86-64) : chargement du PE en mémoire émulée aux adresses
d'origine (pas de relocation, image base conservée), TEB/PEB minimal via le
registre `UC_X86_REG_GS_BASE` (offset PEB->ProcessHeap à TEB+0x60+0x30,
cohérent avec la structure PEB réelle x64), table de stubs pour toute la
table d'import (chaque stub = une instruction `RET`, hook `UC_HOOK_CODE`
déclenché juste avant son exécution pour simuler un comportement plausible
et logger l'appel), stub spécial pour `GetProcAddress` qui alloue
dynamiquement un nouveau stub pour toute fonction demandée au runtime (gère
la résolution dynamique observée dans `ServiceMain`), et un hook
`UC_HOOK_MEM_WRITE` sur toute écriture en mémoire exécutable qui invalide le
cache de traduction Unicorn (`ctl_remove_cache`) pour que le code
auto-modifié soit re-décodé correctement au prochain passage.

```bash
python3 tools/emulate_service_init.py binaries/$h2.exe --entry 180001020 --max-instr 500000
```

Démarrage à `ServiceMain`, interception du premier `CreateThread` :
`lpStartAddress=0x180001440` (le worker déjà identifié en §F/§H),
`lpParameter` pointe vers un buffer contenant deux chaînes wide :

```
[param_1+0x000] = "C:\Windows\System32\wiasvc.ini"
[param_1+0x104] = "wiasvc"
```

Entre l'appel `GetModuleFileNameW` (dont la valeur de retour est fournie
par le harnais) et ce `CreateThread`, l'émulation traverse environ 57 000
instructions de bourrage (rotations, `xchg`/`bswap` sans effet utile, sauts
en cascade) : confirme visuellement qu'il s'agit d'une vraie couche
d'obfuscation/virtualisation maison, pas un artefact de l'émulation.

**Test de contrôle** : relancer l'émulation avec une valeur de chemin de
module différente (variable d'environnement `KONNI_FAKE_MODULE_PATH`) :

```bash
KONNI_FAKE_MODULE_PATH='C:\Windows\System32\ZZZZZZZZ.dll' \
  python3 tools/emulate_service_init.py binaries/$h2.exe --entry 180001020 --max-instr 500000
```

Résultat : `param_1+0x000` change en conséquence (`ZZZZZZZZ.ini`), mais
`param_1+0x104` reste exactement `"wiasvc"`. Confirme que ce n'est pas une
valeur dérivée du nom de fichier réel du module, mais une constante figée
indépendante de l'environnement, simplement cachée par l'obfuscation.

## L. Validation : déchiffrement du fichier de config capturé

```bash
python3 tools/konni_config_crypto.py binaries/$h5.unknown
```

Clé `SHA-256(UTF16LE("wiasvc"))` appliquée au fichier de 611 octets
récupéré en §J : déchiffrement réussi, sortie ASCII parfaitement lisible,
liste de 21 domaines C2 séparés par `\r\n` (voir `konni-native-rat.md` §6 et §8 pour
la liste complète). Confirme d'un coup la clé et le schéma
de chiffrement du canal descendant, sur un artefact réel et non sur une
donnée auto-générée par une réimplémentation Python.

## Environnement

Identique aux autres analyses de ce dépôt : Ghidra headless via PyGhidra,
`pefile`, venv `~/.venvs/ctf-reverse`, clé API MalwareBazaar dans
`.mb_api_key` à la racine. `pycryptodome` (AES-256-CTR) et `unicorn`+`capstone`
(émulation CPU x86-64, déjà présents dans le venv depuis les travaux sur
RagnarLocker/VMProtect) pour les preuves de concept et le harnais
d'émulation.
