Analyse de Malwares Paquetés avec UPX Unpacker
Quand l'utiliser
- L'analyse statique révèle des sections à haute entropie et des importations minimales, indiquant que le binaire est compressé
- PEiD, Detect It Easy ou PEStudio identifient UPX ou un autre packer connu
- La table d'importation ne contient que LoadLibrary et GetProcAddress (résolution d'importation dynamique typique des binaires compressés)
- Vous devez récupérer le binaire original pour un désassemblage et une décompilation corrects dans Ghidra ou IDA
- La décompression automatique UPX échoue car l'auteur du malware a modifié les octets magiques ou les en-têtes UPX
Ne pas utiliser avec des packers personnalisés, des protecteurs basés sur des machines virtuelles (Themida, VMProtect), ou des échantillons où le désempaquetage dynamique via débogage est plus approprié.
Prérequis
- UPX (Ultimate Packer for eXecutables) installé (
apt install upx-uclou téléchargement depuis https://upx.github.io/) - Detect It Easy (DIE) pour l'identification du packer
- Python 3.8+ avec la bibliothèque
pefilepour la réparation manuelle des en-têtes - x64dbg ou x32dbg pour le désempaquetage manuel lorsque les outils automatisés échouent
- PE-bear ou CFF Explorer pour l'inspection et la réparation des en-têtes PE
- Machine virtuelle d'analyse isolée sans connectivité réseau
Flux de travail
Étape 1 : Identifier le packer
Déterminez si l'échantillon est compressé et identifiez le packer :
# Vérification avec Detect It Easy
diec suspect.exe
# Vérification avec UPX (test sans désempaquetage)
upx -t suspect.exe
# Analyse d'entropie et détection de packer en Python
python3 << 'PYEOF'
import pefile
import math
pe = pefile.PE("suspect.exe")
print("Analyse des sections :")
for section in pe.sections:
name = section.Name.decode().rstrip('\x00')
entropy = section.get_entropy()
raw = section.SizeOfRawData
virtual = section.Misc_VirtualSize
print(f" {name:8s} Entropie : {entropy:.2f} Brut : {raw:>8} Virtuel : {virtual:>8}")
# Recherche des noms de sections UPX
section_names = [s.Name.decode().rstrip('\x00') for s in pe.sections]
if 'UPX0' in section_names or 'UPX1' in section_names:
print("\n[!] Noms de sections UPX détectés")
elif '.upx' in [s.lower() for s in section_names]:
print("\n[!] Noms de sections variantes UPX détectés")
# Vérification du nombre d'importations (les binaires compressés en ont très peu)
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
total_imports = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
print(f"\nNombre total d'importations : {total_imports}")
if total_imports < 10:
print("[!] Très peu d'importations - probablement compressé")
else:
print("\n[!] Aucune table d'importation - fortement compressé")
PYEOF
Étape 2 : Tenter la décompression UPX standard
Essayez la décompression UPX intégrée :
# Décompression UPX standard
upx -d suspect.exe -o unpacked.exe
# Si UPX échoue avec l'erreur "not packed by UPX", les en-têtes peuvent avoir été modifiés
# Sortie verbeuse pour le débogage
upx -d suspect.exe -o unpacked.exe -v 2>&1
# Vérification du fichier désempaqueté
file unpacked.exe
diec unpacked.exe
Étape 3 : Réparer les en-têtes UPX modifiés
Si la décompression standard échoue, réparez les octets magiques falsifiés :
# Réparation des en-têtes UPX modifiés
import struct
with open("suspect.exe", "rb") as f:
data = bytearray(f.read())
# Octets magiques UPX : "UPX!" (0x55505821)
# Les auteurs de malware modifient souvent ces octets pour empêcher le désempaquetage automatique
# Recherche des signatures UPX modifiées
upx_magic = b"UPX!"
modified_patterns = [b"UPX0", b"UPX\x00", b"\x00PX!", b"UPx!"]
# Recherche et restauration des noms de sections
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
num_sections = struct.unpack_from("<H", data, pe_offset + 6)[0]
section_table_offset = pe_offset + 0x18 + struct.unpack_from("<H", data, pe_offset + 0x14)[0]
print(f"Offset PE : 0x{pe_offset:X}")
print(f"Nombre de sections : {num_sections}")
print(f"Offset de la table des sections : 0x{section_table_offset:X}")
for i in range(num_sections):
offset = section_table_offset + (i * 40)
name = data[offset:offset+8]
print(f"Section {i} : {name}")
# Restauration des octets magiques UPX dans le binaire
# Recherche de l'emplacement de la signature d'en-tête UPX (généralement près de la fin des données compressées)
for i in range(len(data) - 4):
if data[i:i+3] == b"UPX" and data[i+3] != ord("!"):
print(f"Octets magiques UPX modifiés trouvés à l'offset 0x{i:X} : {data[i:i+4]}")
data[i:i+4] = b"UPX!"
print(f"Restauration en : UPX!")
# Restauration également des noms de sections si modifiés
for i in range(num_sections):
offset = section_table_offset + (i * 40)
name = data[offset:offset+8].rstrip(b'\x00')
if name in [b"UPX0", b"UPX1", b"UPX2"]:
continue # Déjà correct
# Vérification des modifications courantes
if name.startswith(b"UP") or name.startswith(b"ux"):
original = f"UPX{i}".encode().ljust(8, b'\x00')
data[offset:offset+8] = original
print(f"Nom de section restauré à 0x{offset:X} en {original}")
with open("suspect_fixed.exe", "wb") as f:
f.write(data)
print("\nFichier réparé écrit. Réessayez : upx -d suspect_fixed.exe -o unpacked.exe")
Étape 4 : Désempaquetage manuel avec un débogueur
Lorsque le désempaquetage automatisé échoue complètement, utilisez le désempaquetage dynamique :
Désempaquetage manuel UPX avec x64dbg :
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Charger l'échantillon compressé dans x64dbg
2. Exécuter jusqu'au point d'entrée (breakpoint système puis F9)
3. Modèle de la routine de désempaquetage UPX :
a. PUSHAD (sauvegarde tous les registres)
b. Boucle de décompression (traite les sections compressées)
c. Résout les importations (appels LoadLibrary/GetProcAddress)
d. POPAD (restaure les registres)
e. JMP vers OEP (point d'entrée original)
4. Placer un breakpoint matériel sur ESP après PUSHAD :
- Après PUSHAD, clic droit sur ESP dans les registres -> Suivre dans le dump
- Placer un breakpoint matériel sur l'accès à l'adresse [ESP]
- Exécuter (F9) - s'arrête à POPAD avant le JMP vers OEP
5. Avancer pas à pas (F7/F8) jusqu'à atteindre le JMP vers OEP
6. À l'OEP : Utiliser le plugin Scylla pour dumper et corriger les importations :
- Plugins -> Scylla -> OEP = EIP actuel
- Cliquer sur "IAT Autosearch" -> "Get Imports"
- Cliquer sur "Dump" pour sauvegarder le binaire désempaqueté
- Cliquer sur "Fix Dump" pour réparer la table d'importation
Étape 5 : Valider le binaire désempaqueté
Vérifiez que l'échantillon désempaqueté est valide et complet :
# Vérification que le PE désempaqueté est valide
python3 << 'PYEOF'
import pefile
pe = pefile.PE("unpacked.exe")
# Vérification des sections normales
print("Analyse des sections désempaquetées :")
for section in pe.sections:
name = section.Name.decode().rstrip('\x00')
entropy = section.get_entropy()
print(f" {name:8s} Entropie : {entropy:.2f}")
# Vérification que les importations sont résolues
print(f"\nNombre d'importations :")
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
count = len(entry.imports)
print(f" {dll} : {count} fonctions")
total = sum(len(e.imports) for e in pe.DIRECTORY_ENTRY_IMPORT)
print(f" Total : {total} importations")
# Comparaison des tailles de fichiers
import os
packed_size = os.path.getsize("suspect.exe")
unpacked_size = os.path.getsize("unpacked.exe")
print(f"\nCompressé : {packed_size:>10} octets")
print(f"Désempaqueté : {unpacked_size:>10} octets")
print(f"Rapport : {unpacked_size/packed_size:.1f}x")
PYEOF
Concepts clés
| Terme | Définition |
|---|---|
| Compression (packing) | Compression ou chiffrement du code exécutable pour réduire la taille du fichier et entraver l'analyse statique ; le binaire contient une routine de désempaquetage qui restaure le code à l'exécution |
| UPX | Ultimate Packer for eXecutables ; compresseur d'exécutables open-source couramment utilisé par les auteurs de malwares car il est gratuit et efficace |
| Point d'Entrée Original (OEP) | La véritable adresse de début du code malveillant avant compression ; la routine de désempaquetage décompresse le code puis saute vers l'OEP |
| Reconstruction des Importations | Processus de reconstruction de la table d'adresses d'importation après avoir dumpé un processus désempaqueté de la mémoire à l'aide d'outils comme Scylla ou ImpRec |
| PUSHAD/POPAD | Instructions x86 qui sauvegardent/restaurent tous les registres généraux ; UPX utilise ce motif pour préserver l'état des registres pendant le désempaquetage |
| Entropie de Section | Mesure de l'aléatoire des données d'une section PE ; les sections compressées présentent une entropie > 7.0 tandis que les sections de code normales se situent en moyenne entre 5.0 et 6.5 |
| Octets Magiques | Octets de signature dans un fichier identifiant son format ; UPX utilise "UPX!" que les auteurs de malwares modifient pour empêcher la décompression automatisée |
Outils et systèmes
- UPX : Compresseur d'exécutables open-source avec capacité de décompression intégrée pour les fichiers correctement compressés
- Detect It Easy (DIE) : Outil de détection de packers, compilateurs et éditeurs de liens qui identifie les protections sur les fichiers PE, ELF et Mach-O
- x64dbg/x32dbg : Débogueur Windows open-source utilisé pour le désempaquetage manuel via l'exécution dynamique et la recherche d'OEP par points d'arrêt
- Scylla : Outil de reconstruction d'importations intégré à x64dbg pour reconstruire l'IAT après le dump mémoire
- PE-bear : Visualiseur et éditeur de fichiers PE pour inspecter et réparer les en-têtes PE après désempaquetage
Scénarios courants
Scénario : Désempaquetage d'un malware avec en-têtes UPX modifiés
Contexte : Un échantillon de malware est identifié comme compressé UPX par les noms de sections (UPX0, UPX1) mais upx -d échoue avec l'erreur "CantUnpackException: header corrupted". L'auteur du malware a modifié les octets magiques UPX pour empêcher la décompression automatisée.
Approche :
- Ouvrir le binaire dans un éditeur hexadécimal et rechercher la zone d'en-tête UPX (généralement à la fin des données compressées)
- Identifier les octets magiques modifiés (par exemple, "UPX!" changé en "UPX\x00" ou complètement mis à zéro)
- Utiliser le script de réparation Python pour restaurer "UPX!" et corriger les noms de sections
- Réessayer
upx -dsur le binaire réparé - Si la réparation échoue, passer au désempaquetage manuel avec x64dbg (PUSHAD -> BP matériel sur ESP -> POPAD -> JMP OEP)
- Valider que le binaire désempaqueté a des importations correctes et des valeurs d'entropie raisonnables
- Importer dans Ghidra ou IDA pour une analyse statique complète
Pièges :
- Supposer qu'UPX est le seul packer ; le binaire peut être doublement compressé (UPX + couche personnalisée)
- Modifier l'échantillon compressé original au lieu de travailler sur une copie
- Ne pas reconstruire les importations après un dump mémoire manuel (le binaire dumpé plantera sans correction de l'IAT)
- Oublier de vérifier les données de superposition ajoutées après les sections PE compressées par UPX
Format de sortie
RAPPORT D'ANALYSE DE DÉSEMPAQUETAGE
======================================
Échantillon : suspect.exe
SHA-256 : e3b0c44298fc1c149afbf4c8996fb924...
Packer : UPX 3.96 (en-têtes modifiés)
BINAIRE COMPRESSÉ
Sections : UPX0 (entropie : 0.00) UPX1 (entropie : 7.89) .rsrc (entropie : 3.45)
Importations : 2 (kernel32.dll: LoadLibraryA, GetProcAddress)
Taille fichier : 98 304 octets
MÉTHODE DE DÉSEMPAQUETAGE
Méthode : Réparation d'en-tête + UPX -d
Correction : Octets magiques UPX! restaurés à l'offset 0x1F000
Commande : upx -d suspect_fixed.exe -o unpacked.exe
Résultat : SUCCÈS
BINAIRE DÉSEMPAQUETÉ
Sections : .text (entropie : 6.21) .rdata (entropie : 4.56) .data (entropie : 3.12) .rsrc (entropie : 3.45)
Importations : 147 (kernel32, user32, advapi32, wininet, ws2_32)
Taille fichier : 245 760 octets (expansion 2.5x)
OEP : 0x00401000
VALIDATION
PE valide : Oui
Importations résolues : Oui (147 fonctions sur 8 DLL)
Exécutable : Oui (s'exécute sans plantage dans le bac à sable)
PROCHAINES ÉTAPES
- Importer unpacked.exe dans Ghidra pour un désassemblage complet
- Exécuter des règles YARA contre le binaire désempaqueté
- Soumettre le binaire désempaqueté à VirusTotal pour une détection améliorée