Select your country

Not finding what you are looking for, select your country from our regional selector:

Rechercher

Bypass des anti-debugs de Neurevt

Comment contourner les différents outils de protection rencontrés lors d’une investigation post-incident de cybersécurité ?

Focus sur le bypass des anti-debugs de Neurevt grâce à l’analyse d’Ismaël Keddar, incider responser et malware analyst chez Orange Cyberdefense.

Partager l’article :

Betabot/Neurevt est un cheval de Troie apparu fin 2012. Un petit tour sur Malpedia[1] permet de récupérer une bibliographie assez exhaustive, de ses fonctionnalités vues du panel d’un centre de commande[2], au procédé d’extraction de sa configuration[3], à des analyses plus ou moins remplies de code en assembleur[4],[5],[6],[7].

Dans un récent article[8], un expert évoque un débordement de pile (stack overflow) qui l’a empêché de conduire son analyse plus en profondeur. Ayant récupéré un dropper de Neurevt suite à une réponse à incident quelques temps plus tôt, l’occasion s’est présentée d’y jeter un œil.

Cet article vise ainsi à présenter une session de reverse-engineering avec quelques outils de base mais aussi à expliquer comment passer outre les différents mécanismes de protection que nous rencontrerons. L’objectif final sera d’avoir Neurevt, qui s’exécute dans un debugger, et envoie des requêtes à son centre de commande.

Le setup utilisé est le suivant :

  • machine virtuelle Windows 7 – 64 bits ;
  • x32dbg (debugger) ;
  • FakeNet-NG (surveillance des requêtes) ;
  • ProcessHacker (manipulation de l’espace mémoire utilisateur) ;
  • MD5 du dropper : c7b7e579b9450936d6a7e29875e945cc.

Dropper

Le dropper se présente sous la forme d’un exécutable DotNet, qui a pour unique fonction d’exécuter le loader via la technique du runPE. Dans les grandes lignes, le runPE se découpe en 3 étapes :

  • la création d’un nouveau processus dans un état suspendu,
  • le remplacement d’une partie du contenu de son espace d’adressage par le code que l’on souhaite exécuter (ici le code du loader),
  • la reprise de l’exécution du processus modifié.

En conséquence directe, le code exécuté par le nouveau processus le sera hors du debugger et donc en dehors de notre contrôle. Une astuce simple consiste à patcher le code à l’entrypoint du nouveau processus de manière à le faire boucler sur lui-même. Cette action nous laisse ainsi le temps nécessaire pour y attacher une nouvelle instance du debugger. Le patch en lui-même se compose de 2 opcodes, 0xEB et 0xFE :

  • 0xEB représente le mnémonique JMP: un saut inconditionnel.
  • 0xFE correspond au nombre d’octets à sauter : 0xFE = -2.

Lorsque le nouveau processus sera exécuté, il commencera donc par jouer l’instruction JMP -2.  Puisque cette dernière fait 2 octets, le processus entrera dans une boucle infinie.

Récupération du loader

Il faut poser un premier breakpoint sur l’API CreateProcessA[9] et exécuter le dropper. Au premier break, nous conseillons de regarder les arguments passés sur la pile ; le chemin du nouveau processus (une seconde instance du dropper) et l’attribut « CREATE_SUSPENDED » apparaissent alors.

Figure 1 : CreateProcessA

Nous posons ensuite un breakpoint sur l’API WriteProcessMemory[10] et continuons l’exécution. Au retour du premier appel, les arguments ayant été poussés sur la pile doivent être inspectés. Nous pouvons voir que 0x400 octets ont été écrits depuis l’adresse 0x03ABF448 (processus parent) vers l’adresse 0x00400000 (processus enfant). Ces 0x400 octets correspondent à l’entête PE[11] du loader. En examinant les données écrites, nous voyons notamment :

  • A l’offset 0xD0, la signature ‘PE\0\0’ ;
  • A l’offset PE+0x06, le nombre de sections du loader : 5 ;

A l’offset PE+0x28, la RVA (adresse virtuelle relative) de l’entrypoint du loader : 0x000015C6 (0xC6150000 en little-endian).

Figure 2 : WriteProcessMemory

Nous pouvons, à ce stade, raisonnablement parier qu’au moins cinq autres appels à WriteProcessMemory auront lieu, à raison d’un pour chaque section du loader. Nous continuons l’exécution et procédons à un deuxième break sur WriteProcessMemory, cette fois-ci pour écrire la section .text qui contiendra le code du loader. Au retour de ce deuxième appel à WriteProcessMemory, nous allons chercher le code qui sera exécuté après que le loader ait été complètement reconstruit. Avec ProcessHacker, nous éditons la mémoire à l’offset 0x000015C6 (entrypoint) du loader en cours de reconstruction, et remplaçons les deux octets qu’on y trouve (0x55 et 0x8B) par 0xEB et 0xFE.

Figure 3 : Patch de l’entrypoint du loader

Nous désactivons ensuite les breakpoints du processus parent et reprenons son exécution jusqu’à ce qu’il se termine. Le processus enfant est en cours d’exécution et boucle sur lui-même. Nous y attachons une nouvelle instance du debugger et restaurons les premiers opcodes de l’entrypoint : 0xEB 0xFE -> 0x55 0x8B :

Figure 4 : restauration de l’entrypoint de loader

Loader

Le loader se charge d’unpacker la charge finale et de l’exécuter. Le premier anti-debug que l’on rencontre est un une vérification du flag « BeingDebugged » du PEB (Process Environment Block) : l’adresse du PEB est ainsi récupérée via l’instruction mov eax, FS:[0x30][12], et le flag vérifié via cmp byte [eax+2], 1[12].

 

Figure 5 : PEB BeingDebugged

Si le flag est à 1, le loader se termine proprement via un saut conditionnel et un appel à ExitProcess, sans exécuter la charge finale. Pour passer cet anti-debug, il suffit de modifier le flot du code de manière à ne pas exécuter le saut conditionnel qui nous amène à ExitProcess. Nous avons ici l’embarras du choix :

  • remplacer le saut conditionnel JE par des NOP ou un JNE (clic droit -> assemble),
  • placer le pointeur d’instruction EIP sur l’instruction suivante (ctrl+*),
  • inverser l’état du ZeroFlag (double clic sur ZF),
  • mettre le flag « BeingDebugged » à 0 à la main ou via Debug -> Advanced -> Hide debugger.

Nous traçons maintenant dans le code et en 0x0040168B (le CALL juste avant l’appel à ExitProcess en cas de détection du debugger). Si nous entrons dans la procédure (F7 / Step Into), nous voyons finalement un CALL EBX (en 0x401DD7).

Figure 6 : CALL EBX

Le registre EBX contient une adresse calculée dynamiquement. Celle-ci qui pointe vers la charge finale unpackée. Step into, donc, pour atterrir à l’entrypoint de cette dernière.

Figure 7 : Entrypoint de la charge finale

Payload

Anti-debug : ProcessDebugPort et stack overflow

Cet anti-debug repose sur deux éléments :

  • Chaque fois qu’une instruction CALL est exécutée, l’adresse de retour (c.à.d. l’adresse de l’instruction qui suit le CALL) est placée sur la pile.
  • Pour chaque appel système (les API Nt*), l’instruction CALL FS:[0xC0] est exécutée (l’adresse pointée par FS :[0xC0] étant une valeur interne réservée à Windows).

La présence d’un debugger est vérifiée via un appel à l’API NtQueryInformationProcess avec sept (« ProcessDebugPort ») comme valeur pour le paramètre « ProcessInformationClass »[14].

Figure 8 : ProcessDebugPort

Si un debugger est détecté, la variable « isRemoteDebuggerPresent » est mise à 1, sinon à 0. Plus loin dans le code, le contenu de cette dernière variable est vérifié.  Si un debugger a été détecté, le contenu de FS :[0xC0] est écrasé avec l’adresse de l’API NtCreateFile. A noter que le nom de l’API n’a pas d’importance en lui-même, du moment que c’est une API Nt*.

Figure 9 : Ecrasement de l’adresse à FS:0xC0

Au prochain appel système, l’instruction CALL FS :[0xC0] sera exécutée. Cependant, l’adresse pointée par FS :[0xC0] a été remplacée par l’adresse de l’API NtCreateFile, c’est donc cette dernière qui sera appelée. Puisque NtCreateFile est un appel système, l’instruction CALL FS :[0xC0] sera de nouveau exécutée. NtCreateFile va ainsi s’appeler en boucle, poussant au passage une adresse de retour sur la pile. Cette boucle infinie s’exécute jusqu’à provoquer un stack overflow.

Figure 10 Stack overflow

Le contournement peut se faire en posant un breakpoint sur l’API NtQueryInformationProcess pour chacun des deux appels qui sont faits avec la valeur « ProcessDebugPort » comme pour le paramètre « ProcessInformationClass », en mettant le contenu du paramètre « IsDebuggerPresent » à 0x00000000 au lieu de 0xFFFFFFFF au retour du syscall.

Figure 11 : Bypass de l’anti-debug ProcessDebugPort

Une fois la seconde vérification passée, nous pouvons alors désactiver le breakpoint.

Injection dans explorer.exe

Une seconde instance du processus explorer.exe est créée (dans l’état suspendu) via un appel à l’API CreateProcessInternalW[15].

Figure 12 : Création d’un nouveau processus explorer.exe

Ici, l’injection n’est pas faite via WriteProcessMemory mais via la création d’une vue de section partagée, permettant aux modifications faites dans le processus parent d’être réalisées en même temps au sein du processus enfant. Tout d’abord, une section RWX (read/write/execute) avec le flag « ALL_ACCESS » (0xF001F) est créée par le processus parent.

Figure 13 : Section RWX

Le processus parent (le malware) charge ensuite cette section en mémoire via l’API NtMapViewOfSection[16], une première fois dans son propre espace d’adressage et une seconde dans l’espace du processus enfant (explorer.exe).

Figure 14 : Mapping de la section RWX dans le processus explorer.exe

De cette manière, les écritures mémoires que fait le processus parent dans la section chargée au sein de son propre espace d’adressage se répercutent également dans la section chargée dans l’espace d’adressage du processus enfant. La payload est injectée de cette manière dans explorer.exe.

Figure 15 : Injection dans explorer via une section partagée. A : avant écriture. B : après écriture. A gauche : processus parent. A droite : processus enfant.

L’injection terminée, il serait tentant de réappliquer la technique du patch à l’entrypoint présentée dans l’analyse du dropper, cependant, elle ne fonctionnera pas. En effet, avant de passer la main (via l’API NtResumeProcess[17]) au code injecté dans le processus explorer.exe, la payload installe dans l’espace mémoire de ce dernier un bout de code qui va interférer avec le mécanisme interne d’initialisation des nouveaux processus de Windows et permettre d’exécuter du code malveillant avant l’entrypoint.

Anti-debug : patch de NtContinue

L’API NtContinue[18] est impliquée entre-autres dans le mécanisme d’initialisation des nouveaux processus, et est donc exécutée avant le code à l’entrypoint d’un processus donné. Ici, la charge malveillante (processus parent) installe un hook sur l’API NtContinue dans l’espace d’adressage du processus enfant (qui est à ce moment encore dans l’état suspendu) via les instructions push adresse/ret.

Figure 16 Hook de NtContinue

Lorsque le processus parent va appeler NtResumeProcess pour lancer l’exécution du processus enfant explorer.exe, le flot du code ira d’abord à l’adresse 0x2713EF (l’adresse sera différente à chaque exécution du malware) avant l’entrypoint. C’est pour cette raison que la technique du patch à l’entrypoint est ici inefficace.

Avec ProcessHacker, nous trouvons la suite du code à l’offset 0x1513EF (adresse virtuelle 0x2713EF – imagebase 0x120000).

Figure 17 : Code exécuté par le hook de NtContinue (1)

En assembleur :

Figure 18 : Code exécuté par le hook de NtContinue (2)

Le corps du hook réalise trois actions :

  • la restauration des premières instructions de NtContinue ;
  • la mise à jour de la valeur du registre EIP (pointeur d’instruction) dans la structure « CONTEXT » du thread;
  • le transfert du flot d’exécution vers l’API NtContinue restaurée.

La restauration des premières instructions se fait via la récupération de l’adresse de NtContinue pour y écrire les suites d’octets 0xB8 0x40 0x00 0x00 0x00 et 0x33 0xC9, correspondant respectivement aux instructions mov eax, 0x40 et  xor ecx, ecx (cf. figures 16A et 18).

Quant à la structure CONTEXT[19], il s’agit d’une structure interne propre à chaque thread et décrivant entre autres, l’état de ses registres. Un pointeur sur cette structure est récupéré avec l’instruction mov eax, [ebp+0x8], et l’entrée « CONTEXT.EIP », qui contenait initialement l’adresse de l’entrypoint du processus enfant, est mis à jour avec l’instruction mov [eax+0xB8], 0x1584C7.

Le corps du hook se termine par les instructions push NtContinue/ret, permettant d’exécuter le code de l’API NtContinue tout juste restauré ; finalement, le flot d’exécution continuera à l’adresse CONTEXT.EIP, ici 0x1584C7. Nous avons la cible de notre nouveau patch 0xEB 0xFE : l’adresse virtuelle 0x1584C7, à laquelle nous soustrayons l’imagebase 0x120000 pour arriver à l’offset 0x384C7. De là, la procédure est identique à ce qui a été présenté dans la section « récupération du loader ».

Inside explorer.exe

Une fois dans explorer.exe, nous retrouvons l’anti-debug basé sur ProcessDebugPort que nous pouvons passer comme expliqué dans la section « Anti-debug : ProcessDebugPort et stack overflow ».

Nous en rencontrons également un nouveau : l’appel à l’API NtSetInformationThread avec le paramètre ThreadInformationClass ayant pour valeur ThreadHideFromDebugger[20]. Succinctement, cet appel fait en sorte qu’aucune exception ne soit passée au debugger[21], rendant de fait les breakpoints software inutiles. Pour le passer, nous pouvons faire en sorte que l’appel à NtSetInformationThread n’ait pas lieu : par exemple avec l’instruction nop, en prenant soin d’équilibrer la pile : 4 arguments * taille d’un argument = 4*4 = 0x10, donc : add esp, 0x10.

Figure 19 : NtSetInformationThread et ThreadHideFromDebugger

Une fois ce dernier anti-debug passé, on peut forcer le reste de l’exécution de la payload en ignorant les exceptions éventuellement levées, pour enfin voir passer une requête vers l’un des centres de commande du cheval de Troie.

Figure 20 : Requête enregistrée par Fakenet

Finalement, si nous souhaitons nous concentrer uniquement sur le protocole de communication entre le cheval de Troie et son centre de commande, nous pouvons passer tous les anti-debug avec TitanHide et les options suivantes d’activées : ProcessDebugPort + SetThreadContext + ThreadHideFromDebugger + NtClose.

[1] https://malpedia.caad.fkie.fraunhofer.de/details/win.betabot

[2] http://www.xylibox.com/2015/04/betabot-retrospective.html

[3] http://www.malwaredigger.com/2013/09/how-to-extract-betabot-config-info.html

[4] http://resources.infosecinstitute.com/beta-bot-analysis-part-1/#gref

[5] https://www.cybereason.com/blog/betabot-banking-trojan-neurevt

[6] https://www.sophos.com/en-us/medialibrary/PDFs/technical-papers/BetaBot.pdf?la=en

[7] https://www.virusbulletin.com/virusbulletin/2014/05/neurevt-botnet-new-generation

[8] https://medium.com/@woj_ciech/betabot-still-alive-with-multi-stage-packing-fbe8ef211d39

[9] https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessa

[10] https://docs.microsoft.com/en-us/windows/desktop/api/memoryapi/nf-memoryapi-writeprocessmemory

[11] https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#file-headers

[12] http://terminus.rewolf.pl/terminus/structures/ntdll/_TEB_x86.html

[13] http://terminus.rewolf.pl/terminus/structures/ntdll/_PEB_x86.html

[14] https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntqueryinformationprocess

[15] https://doxygen.reactos.org/d9/dd7/dll_2win32_2kernel32_2client_2proc_8c.html#a13a0f94b43874ed5a678909bc39cc1ab

[16] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-zwmapviewofsection

[17] https://www.pinvoke.net/default.aspx/ntdll/NtResumeProcess.html

[18] http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FThread%2FNtContinue.html

[19] http://terminus.rewolf.pl/terminus/structures/ntdll/_CONTEXT_x86.html

[20] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntddk/nf-ntddk-zwsetinformationthread

[21] http://www.ivanlef0u.tuxfamily.org/?p=48