Runbook : reproduction pas à pas (scorched-earth-ausfc)
A. Acquisition et triage initial#
unzip -l Archive.zip
# -> un seul fichier : fc.exe (pas de mot de passe sur l'archive)
unzip -o Archive.zip -d /tmp/inspect
sha256sum fc.exe
file fc.exe # PE32+ executable for MS Windows 6.01 (console), x86-64, 16 sections
import pefile
pe = pefile.PE("fc.exe", fast_load=True)
pe.parse_data_directories()
for s in pe.sections:
print(s.Name.decode(errors="ignore").strip("\x00"), round(s.get_entropy(), 2))
for e in pe.DIRECTORY_ENTRY_IMPORT:
print(e.dll.decode())
Retour : sections nommées /4, /19, /32... (noms longs COFF via table de chaînes, typique MinGW/Go), section .symtab présente, seul kernel32.dll importé directement. Ces indices pointent vers un binaire Go (confirmé ensuite).
strings -n 8 fc.exe | grep -iE "go1\.|golang|Go build ID"
Retour : go1.26.2, golang.org/x/crypto/argon2, golang.org/x/crypto/chacha20poly1305, golang.org/x/crypto/hkdf, golang.org/x/crypto/blake2b.
strings -n 6 fc.exe | grep -iE "C:/Users|scorched|main\.go"
Retour : C:/Users/User/source/scorched-earth-ausfc/cmd/encrypter/main.go, mod scorched-earth-ausfc (devel). Recherche web du nom de projet : infructueuse, pas de dépôt public trouvé.
B. Extraction des symboles Go (binaire non strippé)#
objdump -t fc.exe | grep -i "crypto\.\|chv1\.\|fswalk\."
Retour : table de symboles COFF complète avec noms de packages/fonctions Go en clair (scorched-earth-ausfc/internal/crypto.DeriveKeyArgon2id, .deriveFinalKey, .EncryptFileTo, .DecryptTo, .VerifyEncryptedMatchesFile, internal/chv1.EncodeHeaderV1/DecodeHeaderV1/ReadHeaderV1/WriteHeaderV1, internal/fswalk.IterFiles/isEncrypted/isTmp/shouldExclude).
C. Décompilation par nom de symbole (Ghidra headless, PyGhidra)#
Outil créé pour cette investigation, généralise decompile_addr.py d'incransom/pay2key en résolvant l'adresse par nom de symbole Ghidra plutôt que par adresse fournie à la main (le binaire Go non strippé rend ça possible et bien plus rapide) :
python3 tools/decompile_by_name.py fc.exe keycore.txt \
"scorched-earth-ausfc/internal/crypto.DeriveKeyArgon2id" \
"scorched-earth-ausfc/internal/crypto.deriveFinalKey" \
"scorched-earth-ausfc/internal/crypto.DeriveKeyMaterial"
Retour : chaîne de dérivation de clé complète : Argon2id -> SHA-256 -> HKDF-SHA256, sel de 16 octets. Détail dans scorched-earth-ausfc.md §6.
python3 tools/decompile_by_name.py fc.exe encrypt_main.txt \
"scorched-earth-ausfc/internal/crypto.EncryptFileTo"
Retour : confirme crypto/rand.Read pour le sel (16 octets) et le préfixe de nonce (4 octets), chacha20poly1305.New, boucle de chiffrement par chunks avec nonce = préfixe aléatoire || compteur de chunk (12 octets), hash SHA-256 du texte clair en parallèle (probablement pour VerifyEncryptedMatchesFile).
python3 tools/decompile_by_name.py fc.exe nonce_header.txt \
"scorched-earth-ausfc/internal/crypto.nonceForChunk" \
"scorched-earth-ausfc/internal/chv1.EncodeHeaderV1"
Retour : nonceForChunk introuvable comme symbole distinct (confirmé par objdump -t | grep -i nonce : aucun résultat), probablement inlinée par le compilateur Go dans EncryptFileTo (cohérent avec ce qui a été vu directement dans cette dernière). EncodeHeaderV1 : DECOMPILE FAILED sur la première tentative, retentée avec succès ensuite.
D. Décodage du format chv1 sur un fichier chiffré réel#
Contexte : un fichier réellement chiffré a été fourni en complément du binaire, HelloWorld.txt.prinzeugen (86 octets).
python3 tools/decompile_by_name.py fc.exe header_v1.txt \
"scorched-earth-ausfc/internal/chv1.WriteHeaderV1" \
"scorched-earth-ausfc/internal/chv1.DecodeHeaderV1" \
"scorched-earth-ausfc/internal/chv1.ReadHeaderV1"
Retour clé : ReadHeaderV1 lit exactement 0x35 = 53 octets fixes (taille du header). DecodeHeaderV1 valide successivement : magic 0x31564843="CHV1" (LE), byte[4]==1 (version), byte[5]==1 (kdf, sinon erreur "unsupported kdf"), byte[0x12]=byte[18]==0x10=16 (longueur du sel, copie depuis l'offset 0x13=19, soit exactement le sel), byte[0x23]=byte[35]==2 (cipher id, sinon erreur de 22 caractères), byte[9*4]=byte[36]==4 (longueur du nonce de base, sinon erreur "unsupported base nonce length").
Décodage complet et vérification arithmétique sur le fichier réel :
import struct
data = open("HelloWorld.txt.prinzeugen","rb").read()
print("magic:", data[0:4], "version:", data[4], "kdf_id:", data[5])
print("argon2 memory KiB:", struct.unpack("<I", data[6:10])[0])
print("argon2 time:", struct.unpack("<I", data[10:14])[0])
print("argon2 parallelism:", struct.unpack("<I", data[14:18])[0])
print("salt:", data[19:35].hex())
print("cipher_id:", data[35], "base_nonce_len:", data[36])
print("base_nonce:", data[37:41].hex())
print("chunk_size:", struct.unpack("<I", data[41:45])[0])
print("orig_size:", struct.unpack("<Q", data[45:53])[0])
chunk_len = struct.unpack("<I", data[53:57])[0]
print("chunk_ct_len:", chunk_len)
print("total accounted:", 57+chunk_len, "== filelen", len(data))
Retour : chaque champ vérifié, 86/86 octets du fichier expliqués sans reste. Argon2id memory=262144 KiB (256 Mo), time=3, parallelism=2, chunk_size=1048576 (1 Mo), taille originale=13 octets, un seul chunk (13+16 tag=29 octets). Détail dans scorched-earth-ausfc.md §5.
E. Recherche du mot de passe : capacités réseau, env, CLI, puis constante codée en dur#
grep -iE "net/http|net\.Dial|crypto/tls|smtp|webhook" /tmp/all_syms.txt # vide : pas de capacite reseau
grep -iE "flag\.String|term\.ReadPassword|os\.Getenv|Password" /tmp/all_syms.txt
python3 tools/decompile_by_name.py fc.exe getenv_callers.txt "os.Getenv"
# -> tous les appelants sont des internes Go/stdlib (os/exec, golang.org/x/sys/cpu), jamais le code du binaire
python3 tools/decompile_by_name.py fc.exe main_flow.txt "main.main" "main.encryptOne" "main.selfDelete"
Retour : main.main construit un buffer de 32 octets une seule fois avant la boucle de fichiers (variable nommée de façon confuse par Ghidra, ex. s_timeBegin_EndPeriod_not_foundtri_14014b199._3387_8_), réutilisé pour chaque appel à encryptOne. main.selfDelete confirme l'auto-suppression après usage (os.Executable() + os/exec.Command).
Localisation des octets bruts réels (contourner le nommage confus de Ghidra, lire directement l'adresse virtuelle référencée via pefile) :
import pefile
pe = pefile.PE("fc.exe", fast_load=True)
imagebase = pe.OPTIONAL_HEADER.ImageBase
addr = 0x14014b199 + 3387 # adresse de base du symbole + offset relatif lu dans le decompile
rva = addr - imagebase
print(pe.get_data(rva, 32))
Retour : b'SUPERCLEDEMORTQUITUE' (valeur réelle remplacée pour cette publication), 32 caractères ASCII dans le binaire réel, mot de passe codé en dur.
Même technique pour retrouver la chaîne d'info HKDF (DAT_140148620-ish dans DeriveKeyMaterial, ajustée de quelques octets pour tomber sur le bon début de chaîne) : b'scorched-earth key v1' (21 octets).
F. Déchiffrement réel confirmé#
import hashlib, struct
from argon2.low_level import hash_secret_raw, Type
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
data = open("HelloWorld.txt.prinzeugen","rb").read()
salt, base_nonce, ct = data[19:35], data[37:41], data[57:57+29]
password = b"SUPERCLEDEMORTQUITUE" # valeur reelle remplacee pour cette publication
argon2_out = hash_secret_raw(password, salt, time_cost=3, memory_cost=262144, parallelism=2, hash_len=32, type=Type.ID)
material = hashlib.sha256(argon2_out).digest()
key = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=b"scorched-earth key v1").derive(material)
nonce = base_nonce + struct.pack("<Q", 0)
print(ChaCha20Poly1305(key).decrypt(nonce, ct, None))
# -> b'Hello World !'
Déchiffrement réussi, tag Poly1305 validé. Outil générique créé : tools/decrypt_chv1.py FICHIER.prinzeugen [SORTIE], gère les fichiers multi-chunks. Détail dans scorched-earth-ausfc.md §8.
Conclusion (premier passage)#
Aucune faille évidente trouvée sur le sel et le nonce, qui viennent de crypto/rand (CSPRNG réel), pas de seed temporelle ni de PRNG faible : la faille identifiée par la suite se situe dans le mot de passe codé en dur (scorched-earth-ausfc.md §7), pas dans cette partie du schéma.