Les packers

Les packers

Analyse d’un packer

Bonjour,

Dans le cadre d’une veille technique orientée red team, je me suis intéressé à un outil de packing présenté dans une vidéo. Certaines simplifications peuvent prêter à confusion dans un contexte défensif réel.

Cet article propose une analyse d’un outil de packing orienté red team, avec un objectif simple : évaluer sa capacité réelle à échapper à des mécanismes de détection modernes, notamment via YARA. Ce que le projet indique clairement…

LoadThatPE vs YARA

Explication de la menace

Ce programme est une sorte de loader/packer. Pour rappel, un loader est un programme qui peut (pas obligatoire) préétablir un environnement adapté et enfin activer une charge (comme un : shellcode, PE programme exécutable, DLL, plus rare, mais aussi un script, etc). L’objectif du loader est l’évasion de solution de sécurité pour les premières étapes de l’exécution d’un programme malveillant. Le packer est un programme qui en contient un autre. Bien entendu, il existe différents types de packer comme UPX dont l’objectif est de réduire la taille du programme en le compressant et en le décompressant lors de l’exécution du programme. Dans ce cas précis, le packer contient en lui-même un programme chiffré qui sera déchiffré en mémoire pendant l’exécution de celui-ci. L’objectif est de ne jamais mettre en clair le programme chiffré sur le disque dur.

Une première question légitime est pourquoi ne pas écrire le programme à exécuter et le supprimer ? Les solutions de sécurité de type host prévention comme les antivirus ou EDR utilisent sur windows un système d’événement noyau qui intervient à chaque opération sur les fichiers (les minifilters sur windows). Cet événement devrait déclencher systématiquement une analyse heuristique ainsi que le scan de signature sur le programme. Il y a d’autres événements comme le chargement d’un programme, etc qui peuvent être des métriques que les EDR/antivirus utilisent (les Event Tracing avec PsNotifyEx).

Il est donc utile pour un redteamer d’avoir ce type d’outil pour charger ses outils sans que les antivirus les embêtent. Comme il est tout à fait normal que le SOC crée une signature pour bloquer cet outil. Et c’est là qu’une sorte de jeu du chat et de la souris commence entre les deux.

Analysons en détail le programme.

Il y a deux dossiers pour deux programmes : les programmes compilés avec de l’IL (qui sera hors scope) et les programmes compilés pour des instructions directement pour le CPU.

Focus sur LoadThatPE/encrypt_pe.py

Ce programme en python prend 3 arguments maximums dont les deux derniers sont optionnels. Le premier étant le programme à packer, le second une éventuelle destination et le troisième la taille.

Le programme va alors déterminer un nombre de chunk du PE à chiffrer entre 10 et 20. Puis pour chaque chunk, il va créer une clé aléatoire de 8 bits soit 256 valeurs, mais qui commencent à 0xAA : la clé est donc entre 0xAA - 0xFF de combinaison possible. C’est une information très intéressante pour créer une signature.

Pour ce qui est du chiffrement en lui-même, l’algorithme split le programme sur le nombre de chunk chiffré et lui donne un nom.

Puis va générer des noms aléatoires pour les fonctions et variables.

800x400

L’étape suivante est la création d’un fichier en C++ destiné à être compilé pour créer le loader/packer.

Tous les “chunks” sont assignés à une variable globale. La taille est aussi assignée à une variable globale.

Nous avons ensuite une fonction qui reconstruit le programme. Il copie un tableau de char[] global dans un tableau de unsigned char* alloué par new. La seule chose à voir est que cette copie est entre fonction GetTickCount dont l’objectif est très certainement de détecter un breakpoint. Cette copie déplacée dans un autre segment de mémoire permet de reconstruire le programme d’origine.

Nous avons une fonction classique de déchiffrement avec des opérations xor.

Puis encore une fonction de mappage du programme packé. Une autre fonction effectue un ré-assignement changement des droits de protection de la mémoire du programme. Sachez que si une condition échoue, la fonction appelle exit.

Le reste sont des fonctions de relocation du programme et de saut dans “l’entrypoint”.

La dernière fonction est le main. Ensuite le programme écrit le fichier source.

C’est au redteamer de compiler le programme. La documentation propose cette compilation :

x86_64-w64-mingw32-g++ --static \
    -O3 -flto -fwhole-program -ffast-math \
    -s -Wl,--strip-all -fno-exceptions -fno-rtti \
    -fno-ident -fexceptions \
    -Wl,--build-id=sha1 \
    -Wl,--image-base=0x10000000 \
    -fomit-frame-pointer -momit-leaf-frame-pointer \
    -finline-functions -finline-limit=1000 \
    -fno-plt -mno-red-zone \
    -static-libgcc -static-libstdc++ \
    -Wl,--gc-sections -ffunction-sections -fdata-sections \
    encrypted_pe.cpp -o yara_rule_edition.exe

Un utilisateur avancé l’aura remarqué. L’option de compilation utilise --strip-all. Petit rappel, le code source généré pour chaque variable et nom de fonction est une chaîne aléatoire… qui ne sera pas incluse dans le fichier final.

Petite expérience du programme

D’abord utilisons un programme aléatoire. Peu importe notre détection se basera sur les faiblesses du packer. Nous allons générer un premier jet sans modification du script.

Avec l’outil un nom de fonction “ckxovHItaFfEJyAfzOXl” pourrait être généré. Mais est-ce que ce nom apparaît dans le programme final ?

  • Non.

L’option --strip-all supprime les chaînes de caractère faisant référence aux noms des fonctions… Il y a quand même des exceptions qui sont lié à des particularités du C++.

Et pour les variables ? Celle-ci étant référencée par des adresses mémoires mis dans des registres, le programme ne contient absolument aucune informations sur les variables (exception si on compile avec des options de debug qui peuvent en y inclure, ce qui n’est pas le cas ici).

L’usage de chaîne de caractère aléatoire pour les noms de variables n’ont aucun impact dans un code en assembleur. À la limite, le nom des fonctions est une possibilité, mais l’option strip supprime ses noms. Les options du script python pour rendre le programme packer aléatoire ne le sont pas. C’est le nombre de chunk et de fonction de reconstitution qui l’est.

Création d’une signature sur un éventuel packer.

Parlons du chiffrement par xor sur un byte. Sachez que YARA intègre un système qui brute force toutes les combinaisons sur un byte xoré… Ce qui signifie qu’un entête PE encodé avec un byte xor est détectable par la règle YARA la plus anecdotique : trouve un entête de fichier exécutable PE 2 fois !

rule Xor_PE {
     strings:
	$cannotdos = "This program cannot be run in dos mode." xor
     condition:
	#cannotdos >= 2
}

Cette règle repose sur une propriété simple : un exécutable PE contient des signatures reconnaissables, même après un chiffrement faible.

Dans le cas d’un XOR sur un seul octet, YARA est capable de tester toutes les combinaisons possibles, rendant ce type d’obfuscation inefficace.

Ainsi une rule qui analyse deux potentiels entêtes PE dans un programme détecte les packers comme LoadThatPE.

800x400

Cet outil illustre une réalité fréquente : une complexité apparente ne garantit pas une efficacité face à des mécanismes de détection modernes. Sans compréhension des modèles de détection (signature, heuristique, entropie), certaines techniques d’obfuscation restent purement cosmétiques.

Note

LoadThatPE a reçu une mise à jour. Les versions précédentes reposaient notamment sur un chiffrement XOR simple. La version actuelle contient l’utilisation apparente d’un dictionnaire (inactif). Cependant, après analyse, ces transformations ont un impact limité sur le binaire final.

  • Des artefacts statiques apparaissent dans le binaire compilé (IOC), bien qu’ils ne soient pas visibles directement dans le code source généré
  • L’absence de chiffrement des chunks rend leur contenu directement exploitable
  • Les données sont placées de manière contiguë après compilation, annulant l’effet attendu de fragmentation
  • Certaines techniques d’obfuscation implémentées au niveau du code source ne sont pas préservées lors de la compilation
  • Dans sa version actuelle, le projet présente également des problèmes de compilation, limitant son utilisation en l’état

Cette évolution illustre une limite : sans prise en compte des optimisations du compilateur, des transformations appliquées au code source peuvent perdre toute efficacité dans le binaire final.

800x400

La suite au prochain épisode…


© -39999 to 2024. All rights reserved.

Powered by Hydejack v9.2.1