S'il vous arrive d'analyser du malware de temps à autres, il vous est peut-être déjà arrivé de faire ceci :
wget_ie http://site/vx.exe
file vx.exe vx.exe: MS-DOS executable PE for MS Windows (console) Intel 80386 32-bit, UPX compressed
upx -d vx.exe [...] Unpacked 1 file.
Oui, mais en fait non. Le raisonnement ci-dessus part du principe suivant : si upx -d marche, alors il s'agissait bien d'un binaire compressé avec UPX et le binaire unpacké aura le même comportement que le binaire packé. Sauf que qu'est-ce qui vous dit que le binaire UPX n'a pas été bidouillé ? Par exemple en ajoutant du code qui s'exécute avant le point d'entrée pour modifier des octets en mémoire juste avant que l'algorithme de décompression ne passe par là ? Par exemple en ajoutant un callback TLS ?
L'idée de ce billet blog m'est venue lorsque j'ai travaillé sur le level 2 du Khallenge 2008 et que je me suis rendu compte que le callback TLS du binaire original (celui qui pose les bases d'une détection de debugger réalisée un peu plus bas dans le code) n'était pas conservé par la décompression UPX. Plutôt marrant.
Bon, je ne vais pas m'étendre outre mesure sur le mécanisme de Thread Local Storage sous Windows. Il s'agit d'une fonctionnalité du loader permettant d'exécuter du code avant le point d'entrée défini dans l'en-tête PE. C'est par exemple utilisé par les malware pour déchiffrer le code situé à l'entry point (cf analyse de PE GRUM par N. Brulez dans MISC 31). Les structures sont les suivantes :
Un bon schéma valant mieux qu'on long discours :
On peut ajouter un callback TLS à un binaire UPX pour par exemple :
On part d'un programme très simple qui affiche une message box :
#include <windows.h>
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MessageBox(NULL, "good", "tls", MB_OK);
return 0;
}
On compile et on compresse en UPX. Toute la suite se fait dans un éditeur hexa. Première étape : trouver un endroit où stocker nos différentes structures. En parcourant vite fait le binaire, on trouve de la place à l'offset cf0 :
On y place notre structure IMAGE_TLS_DIRECTORY :
Le champ AddressOfCallBacks pointe vers l'offset d08 où on place maintenant notre tableau de pointeurs vers callbacks. On n'en a besoin que d'un seul :
Que voulons-nous faire dans ce callback ? On va prendre l'exemple très simple de la modification de la chaine "good" qui se situe à l'offset 7ae, soit à l'adresse virtuelle 4075AE. Un simple MOV sur le contenu présent à cette adresse suffit (oui, je sais c'est particulièrement peu élégant comme méthode, mais c'est juste pour l'exemple...) :
55 PUSH EBP 89 E5 MOV EBP, ESP 36 C705 AE754000 6576696C MOV DWORD PTR SS:[004075AE], 6C697665 b8 0000 0000 MOV EAX, 0 C9 LEAVE C3 RET
En résumé, on obtient ceci :
On peut ainsi modifier tout octet dans la version UPXée du binaire (attention, pour patcher certaines zones, il faut aussi patcher le checksum UPX), de façon à obtenir un binaire décompressé en mémoire différent de celui décompressé en statique avec upx -d.
Tout est en place, il ne reste plus qu'à modifier l'en-tête optionnelle PE pour rendre la structure IMAGE_DIRECTORY_ENTRY_TLS non nulle et la faire pointer sur la structure définie plus haut :
On obtient ainsi le binaire à télécharger en annexe. Si on va un peu vite à l'analyse et qu'on exécute la version décompressée avec upx -d, il affiche le message défini dans le source, à savoir "good" :
En revanche, quand on exécute le binaire UPX lui-même sous XP, surprise :
Archive : sylv1_tls.zip
Ce billet est publié sous la licence CC-BY-SA.