CTI

TEHTRIS publie un nouvel outil open source d’extraction de shellcode

L’utilisation de packer, l’exécution de shellcodes et le chargement de DLL (Dynamic Loaded Library) en mémoire sont très courants dans le domaine des malwares. Il peut être assez fastidieux d’extraire la véritable charge utile seulement grâce à l’analyse statique. Une approche dynamique peut aider l’analyste à trouver une méthode quasi-générique pour désobfusquer les étapes n+1. C’est ce que TEHTRIS vous propose avec l’outil open source « Extraction de shellcode ». Cet outil n’a pas la prétention d’être fiable à 100% mais trouve rapidement une solution dans la plupart des cas.

Cet article décrit les capacités de cet outil, réalisé par TEHTRIS, qui permet d’extraire les payloads d’un fichier PE (Portable Executable). Il vise à fournir un environnement de sandboxing minimaliste pour traquer le Self Modifying Code (SMC) avec une consommation de mémoire limitée.

L’interface de programmation d’application (API) de pin [0] offre déjà un mécanisme de détection des SMC callbacks[1] :

typedef VOID(* LEVEL_PINCLIENT::SMC_CALLBACK) (ADDRINT traceStartAddress, ADDRINT traceEndAddress, VOID *v)

Malheureusement, cette API ne détecte pas toujours les SMC dans les pages  mémoires allouées par kernel32!VirtualAlloc. De plus, d’autres fonctionnalités sont nécessaires pour aider le reverser :

  • Dump automatique du shellcode
  • Dump du fichier PE en cas de chargement manuel de DLL (en dehors de kernel32!LoadLibrary)
  • Dump de la trace (trace : séquence d’instruction avec une entrée et plusieurs sorties) qui effectue la désobfuscation
  • Détection du OEP (Original Entry Point)

Lorsque ces mécanismes sont détectés, l’analyste a immédiatement accès aux payloads et à l’adresse/dump de la routine de désobfuscation. Cela peut permettre de gagner du temps lors de la désobfuscation et de générer des signatures de détections. Le débogage manuel, ou l’analyse statique, est un processus chronophage, et un analyste ne peut pas toujours se permettre de gaspiller du temps lorsqu’il doit réagir dans l’urgence.

Analyse

Un exemple générique d’astuces d’obfuscation couramment rencontrées sur la toile a été généré à des fins de démonstration.

Code

Un échantillon de test a été généré en utilisant mingw32, puis a été packé avec UPX [5] :

#include <windows.h>
#include <stdio.h>

int main()
{
    char shellcode[] = "\x91\x91\x91\xc2";
    // Alloc memory
    LPVOID addressPointer = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if(!addressPointer) {
    printf("Fail to allocate\n");
    return	0;
}

// unxor

    for(size_t	i=0; i<sizeof(shellcode); i++) {
        ((BYTE	*)addressPointer)[i] = shellcode[i] ^ 1;
    }
    // Create thread pointing to shellcode address
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addressPointer, NULL, 0, 0);
    // Sleep for a second to wait for the thread
    Sleep(1000);
    return 0;
}
					
				

Il y a 2 couches d’obfuscation : UPX + xor 1. Le shellcode sera exécuté dans un thread séparé.

Le payload se compose uniquement d’instructions nop/ret : x91x91x91xc2 avant le xor, x90x90x90 xc3. Même s’il s’agit d’un exemple assez trivial, cela peut suffire à cacher des chaînes de caractères et à tromper les règles YARA portant sur le code malveillant. En conjonction avec les techniques d’anti-débogage, l’analyse peut être très fastidieuse.

Comportement

Au niveau de la mémoire on a le comportement suivant :

Comportement du shellcode en mémoire

Démonstration sur l’échantillon de test

Il est temps de montrer l’outil d’extraction de shellcode en action avec l’échantillon précédent. Pour analyser, il suffit d’exécuter le script :

quickShellcodeDetector /home/user/samples/shellcode.exe /dev/shm

Les logs montrent que l’échantillon a été correctement unpacké et le shellcode a été correctement désobfusqué. Les logs de sortie du programme sont présent dans le bloc ci dessous. Chaque trace est préfixée par le PID (Process IDentifier) du processus pour éviter les collisions avec les process enfants si l’adresse du shellcode est la même :

[INFO] ShellcodeDetector.cpp:503	Starting program: pid=7360
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: F:\pin-3.18-98332gaebd7b1e6-msvc-windows\source\tools\QuickDetector\shellcode.exe addr=0x00370000 id=1 size=0x1c000
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\ KernelBase.dll addr=0x76d80000 id=2 size=0x214000
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\ kernel32.dll addr=0x77300000 id=3 size=0xf0000
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\ntdll .dll addr=0x77560000 id=4 size=0x1a3000
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\ apphelp.dll addr=0x74c80000 id=5 size=0x9f000
[INFO] ShellcodeDetector.cpp:113	Found obfuscation routine at: 0 x37105e (F:\pin-3.18-98332-gaebd7b1e6-msvc-windows\source\tools\ QuickDetector\shellcode.exe+0x105e)
[INFO] ShellcodeDetector.cpp:115	Dumping trace into: C:\Users\user\ AppData\Local\Temp\ShellcodeDetector\0x1cc0_0x0037105e.trc
[INFO] ShellcodeDetector.cpp:239	Dumping ShellCode: C:\Users\user\ AppData\Local\Temp\ShellcodeDetector\0x1cc0_0x0b230000.bin ep=0 x00000000 size=0x1000
[INFO] ShellcodeDetector.cpp:254	Dumped: 4096 bytes
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\ kernel.appcore.dll addr=0x74000000 id=6 size=0xf000
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\
msvcrt.dll addr=0x77400000 id=7	size=0xbf000
[INFO] ShellcodeDetector.cpp:446	IMG_LOAD: C:\Windows\SysWOW64\
rpcrt4.dll addr=0x76150000 id=8	size=0xc0000
[INFO]	ShellcodeDetector.cpp:463	Done in 3 seconds
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 1
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 2
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 3
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 4
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 5
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 6
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 7
[INFO]	ShellcodeDetector.cpp:467	Freing	image: 8

La routine d’obfuscation et le payload ont été trouvés. Le fichier de logs indique l’adresse de la trace qui désobfusque le payload :

Found obfuscation routine at: 0x37105e (F:\pin-3.18-98332-gaebd7b1e6msvc-windows\source\tools\QuickDetector\shellcode.exe+0x105e)

La trace trouvée à shellcode.exe+0x105e est exportée dans le fichier 0x1cc0_0x0037105e.trc :

La trace IDA [4] correspondant à la RVA (Relative Virtual Address) (à 0x37105e) aide à trouver le xor 1 (c’est la même trace que celle extraite par l’outil) :

Capture 1 : Extraction de la trace : 0x1cc0_0x0037105e.trc
Capture 2 : Extraction de la trace

Le fichier de logs indique l’adresse de shellcode ainsi que son point d’entrée relatif à la base d’adresses d’allocation de mémoire :

[INFO] ShellcodeDetector.cpp:239	Dumping ShellCode: C:\Users\user\ AppData\Local\Temp\ShellcodeDetector\0x1cc0_0x0b230000.bin ep=0 x00000000 size=0x1000

Voici le shellcode extrait, trouvé à l’adresse 0x0b230000 exportée dans le fichier 0x1cc0_0x0b230000.bin :

Capture 3 : Extraction de shellcode : 0x1cc0_0x0b230000.bin

La taille du shellcode est de 4096 octets en raison de l’alignement des pages forcé par la librairie l’allocation de mémoire.

Architecture

L’outil est une extension de pin, il ne nécessite aucune dépendance. Le schéma suivant décrit l’architecture globale. Il s’agit d’une architecture simple fondée sur une machine virtuelle (VM) utilisant la CLI VirtualBox [3].

Les commandes sont passées en lançant des additions invitées, ce qui n’est pas idéal en matière de discrétion. Cela devrait être remplacé par un agent dans le futur.

Elements internes de l’outil

L’outil essaie de garder chaque accès à la mémoire avec une consommation de mémoire minimale. Pour y parvenir, l’outil enregistre pour chaque trace son accès W/X aux blocs de mémoire déterminés par kernel32!VirtualQuery.

Ce raccourci permet de ne pas doubler la mémoire allouée et de comparer chaque copie.

Seules la trace et la région mémoire sont enregistrées. Les structures suivantes sont utilisées pour enregistrer les évènements :

typedef struct _TRACEACCESS {
UINT32 access_type;
ADDRINT membase;
} TRACEACCESS, *PTRACEACCESS;

typedef struct _TRACE {
ADDRINT address;
USIZE length;
size_t accessnb;
PTRACEACCESS access;
} *PTRACE;

typedef struct _MEMACCESS {
size_t tracenb;
PTRACE trace;
} MEMACCESS, PMEMACCESS;

Ces structures sont provisionnées par les callbacks W/X.

La détection des collisions est effectuée en temps réel, garantissant que le dump mémoire est traité dès que la page est exécutée pour la première fois. Cela permet dans la plupart des cas d’extraire un payload entièrement désobfusqué.

Un analyseur PE, très simple mais efficace, est inclus pour analyser l’en-tête PE puis d’extraire les DLL.

Conclusion

Cet outil est très pratique lorsqu’un analyste a besoin d’extraire un payload obfusqué dans un échantillon avec peu de protections. Il existe de nombreuses solutions de contournement et de détection possibles, ce qui pourrait être amélioré à l’avenir.

Références

[0] https://www.intel.com/content/www/us/en/developer/articles/tool/pin-a-dynamic-binary-instrumentation-tool.html
[1] https://software.intel.com/sites/landingpage/pintool/docs/98484/Pin/html/group__TRACE.html#gad80d434b4df6285334079c19df32a2e8
[2] https://github.com/tehtris-hub/ShellCodeDetector
[3] https://www.virtualbox.org/
[4] https://www.hex-rays.com/ida-pro/
[5] https://upx.github.io/