Afin de maintenir à jour le module de détection Codebreaker, notre équipe R&D se doit d’étudier les nouvelles menaces qui apparaissent chaque jour sur les réseaux.
De nos jours, les shellcodes sont largement utilisés par les cybercriminels dans la conception de codes malveillant, le but étant de tromper l’analyse comportementale utilisée par les moteurs antimalwares.
Nous vous proposons dans cet article en trois parties l’étude d’un shellcode découvert en juillet 2019, appelé « frenchy shellcode ».
Ce shellcode, qui amène un processus autrement légitime à exécuter un code malveillant, existe en plusieurs versions, mais celles-ci restent très similaires.
L’auteur du frenchy shellcode semble être un utilisateur de Hack Forums ayant le pseudonyme « Frenchy » devenu plus tard « SIM-3131187 ».
Figure 1 – L’auteur du frenchy shellcode
Dans cette première partie, nous allons voir :
- Le chargement des DLLs et la résolution des fonctions dans Frenchy,
- La technique employée par l’auteur pour faire l’évasion des produits de sécurité,
- La détection du shellcode par Codebreaker
Frenchy shellcode v3
Pour analyser le fonctionnement détaillé de ce shellcode, nous avons débuté avec la version 3 (0c9da7a0e3d3b2a6345bf69a22f577855f476d645cb71cd8a18123787e75a75a).
Ce shellcode prend 2 paramètres sur la pile : le chemin d’un processus cible à injecter, et la charge utile à injecter.
Chargement des DLLs et résolution des fonctions
Afin de charger les DLLs et trouver les adresses des APIs, « frenchy » utilise deux fonctions dédiées que nous allons appeler « LoadDLLFromKnownDLL » et « CustomGetProcAddress ». Ces dernières imitent le comportement de LoadLibraryA et GetProcAddress de l’API Windows tout en permettant d’échapper à la détection antivirus.
Fonctionnement de LoadDLLFromKnownDLL
Avant chaque appel à LoadDLLFromKnownDLL, le shellcode charge le pointeur vers le chemin de la DLL dans le registre EAX avec l’instruction LEA.
Figure 2 – Appels à LoadDLLFromKnownDLL
Les DLLs sont chargées depuis le répertoire \KnownDlls\<dll_name>.dll ou \KnownDlls32\<dll_name>.dll pour les DLLs compatibles WoW64.
KnownDLLs est un mécanisme de Windows NT permettant d’accélérer le chargement des bibliothèques partagées. Lorsque le système démarre, il consulte le registre HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ SessionManager\KnownDLLs\ et créé une section \KnownDlls\<nom du fichier DLL> pour chaque DLL répertoriée sous cette clé de registre. Ces sections seront utilisées par la suite au lieu de recharger ces DLL à chaque exécution.
Le shellcode charge des DLLs fréquemment utilisées (ntdll.dll, kernel32.dll …), il est donc sûr de les retrouver dans le répertoire KnownDLLs.
La capture d’écran ci-dessous montre comment une DLL est mappée en mémoire :
Figure 3 – Le pseudocode de la fonction LoadDLLFromKnownDLL
Le PEB, et sa InMemoryOrderModuleList (Figure 4) sont parcourus pour obtenir l’adresse de ntdll.dll. Cette DLL est chargée dans chaque processus Windows lorsqu’il est initialisé, le shellcode sait donc que ntdll sera déjà chargé.
Figure 4 – Chemin d’accès à l’adresse de ntdll.dll
L’adresse de ntdll.dll est ensuite utilisée par « CustomGetProcAddress » pour rechercher les fonctions NtOpenSection et NtMapViewOfSection, qui sont finalement appelées pour mapper la nouvelle DLL en mémoire.
Résolution des fonctions avec « CustomGetProcAddress »
Cette fonction parcourt l’entête d’un header PE et sa table d’export pour rechercher l’adresse du symbole demandé :
Figure 5 – CustomGetProcAddress
Le champ « EXPORT_DIRECTORY » du header PE pointe sur 3 tables :
- AddressOfNames: qui pointe sur le nom des fonctions (en ASCII)
- AddressOfFunctions qui donne l’adresse mémoire du code des fonctions
- AdressOfNameOrdinals qui fait le lien entre les 2 précédentes.
Figure 6 – Position de l’adresse d’une fonction depuis l’Export Directory Table (Source : Win32 Assembly Components )
Le shellcode va simplement parcourir ces 3 tables afin de localiser l’adresse de la fonction recherchée.
« CustomGetProcAddress » est également utilisé par la suite pour résoudre les fonctions depuis les DLLs fraichement copiés. Le shellcode obtient des pointeurs vers multiples APIs, probablement pour être plus générique et supporter d’avantages de charges utiles, mais n’en utilise qu’une infime partie.
Évasion des produits de sécurité
Le fait d’avoir reprogrammé l’import de fonction externes, sans passer par LoadLibraryA et GetProcAddress, permet au shellcode d’éviter la détection par les produits de sécurité qui utilisent les hooks d’API, comme ce qui est décrit par l’image ci-dessous :
Figure 7 – Inline hooking
Généralement placés dans les DLLs chargées au démarrage du programme, les hooks d’API ou plus précisément les « inline API hooks » permettent d’intercepter les appels aux fonctions de l’API Windows. Souvent réalisée en écrasant les premiers octets avec une instruction « jmp », cette interception donne l’occasion au produit de sécurité d’effectuer un traitement avant l’exécution de la fonction (analyse comportementale, journalisation, blocage du binaire …).
Sachant que LoadLibraryA ne recharge pas les DLLs, et donc retourne l’adresse de la DLL déjà chargée en mémoire, l’utilisation de cette fonction expose le shellcode aux hooks placés dans ces APIs.
Pour éviter ces hooks, « Frenchy » créé une copie en mémoire des DLL qu’il utilise et donc peut effectuer les mêmes appels API au travers d’une DLL non hookée.
Figure 8 – Vue des DLL chargées avec process hacker, avec plusieurs DLL en double exemplaire (WinDbg et Process Hacker)
Limitations
Les fonctions standard « LoadLibraryA » et « GetProcAddress » restent utilisées par « frenchy » :
Figure 9 – Rechargement d’ole32.dll et advapi32.dll avec LoadLibraryA (Ghidra)
La fonction manuelle « LoadDLLFromKnownDLL » ne fait que charger une DLL en mémoire, elle ne gère pas ses dépendances. Donc pour utiliser certaines DLL (comme « advapi32.dll » et « old32.dll »), « frenchy » continue d’utiliser les fonctions standard d’imports.
Bien que le mappage manuel des DLL s’avère très efficace pour échapper aux hooks d’API, il présente l’inconvénient de donner au programme une apparence anormale : une DLL n’est normalement importée qu’une fois. De plus, certains scanneurs de mémoire peuvent identifier ces copies en mémoire comme une sorte d’injection de code.
Détection par Codebreaker
L’analyseur de shellcode CodeBreaker détecte « frenchy » :
Figure 10 – Logs de l’analyse Codebreaker
Les deux schémas d’accès au PEB sont détectés comme malveillants :
Figure 11 – Logs Codebreaker: les patterns d’accès au PEB
Figure 12 – Premier pattern d’accès à la structure PEB
Figure 13 – Second pattern d’accès à la structure PEB
En effet, l’utilisation du segment FS pour extraire l’adresse de base d’une DLL est certainement la caractéristique la plus commune des shellcodes Windows.
Grâce aux patterns trouvés, Codebreaker réussit à remonter jusqu’au point d’entrée et émuler le shellcode.
... --- Call to Windows API detected --- ntdll_wcslen('wstr': '\KnownDlls32\ntdll.dll') -> 22 --- Call to Windows API detected --- ntdll_NtOpenSection('DesiredAccess': 'SECTION_MAP_EXECUTE|SECTION_MAP_READ', 'ObjectAttributes': {'RootDirectory': 0, 'ObjectName': '\\KnownDlls32\\ntdll.dll', 'SecurityQualityOfService': 0, 'SecurityDescriptor': 0, 'Length': 24, 'Attributes': 'OBJ_CASE_INSENSITIVE'}) -> 1 --- Call to Windows API detected --- ntdll_NtMapViewOfSection('SectionHandle': '\KnownDlls32\ntdll.dll (24)', 'ProcessHandle': 'Current Process (-1)', 'pBaseAddress': 0, 'ZeroBits': 0, 'CommitSize': 0, 'pSectionOffset': 0, 'pViewSize': 0, 'InheritDisposition': 1, 'AllocationType': None, 'Win32Protect': 'PAGE_READONLY') -> 0 ... --- Call to Windows API detected --- kernel32_LoadLibraryA('lpFileName': 'advapi32.dll') -> 0x70b00000 --- Call to Windows API detected --- kernel32_GetProcAddress('hModule': 'advapi32.dll', 'lpProcName': 'CryptAcquireContextW') -> 0x70b0de7c ...
Conclusion
Dans cette première partie nous avons pu voir la manière dont le shellcode charge les copies des DLLs et la résolution des fonctions depuis ces copies qui lui permet de faire l’évasion des produits de sécurités utilisant les hooks d’APIs.
Hormis les avantages de cette technique, nous avons vu qu’elle donne au programme une apparence anormale.
Quant à Codebreaker, il réussit dans la détection de toutes les versions du shellcode en se basant sur plusieurs modèles d’accès PEB.
Script de test
Le script suivant permet d’ajouter des instructions avant le shellcode pour préparer son environnement d’exécution :
import struct f = open("frenchyshellcode.bin", "rb") frenchy = f.read() f.close() f = open("c:\\windows\\system32\\calc.exe", "rb") calc = f.read() f.close() hollowpath = b"?c:\\windows\\notepad.exe\x00" # call 0x5 shellcode = b"\xe8\x00\x00\x00\x00" # pop eax shellcode += b"\x58" # add eax,0x11 -> begin of calc shellcode += b"\x83\xc0\x11" # push eax -> push address of pe to inject shellcode += b"\x50" # add eax, len(calc) -> begin of process to hollow shellcode += b"\x05" + struct.pack("<L", len(calc)) # push eax -> push address of path to process to hollow shellcode += b"\x50" # add eax, len(hollowpath) -> begin of frenchy shellcode += b"\x83\xc0" + chr(len(hollowpath)).encode() # call eax -> jump to frenchy shellcode += b"\xff\xd0" # ret shellcode += b"\xc3" shellcode += calc shellcode += hollowpath shellcode += frenchy f = open("frenchy_with_args.bin", "wb") f.write(shellcode) f.close()
Liens
https://docs.microsoft.com/fr-fr/archive/blogs/larryosterman/what-are-known-dlls-anyway
http://www.hick.org/code/skape/papers/win32-shellcode.pdf
https://thewover.github.io/Dynamic-Invoke/
http://www.peppermalware.com/2019/07/analysis-of-frenchy-shellcode.html
https://breakdev.org/defeating-antivirus-real-time-protection-from-the-inside/
Auteur : Anastasia Cotorobai