Skip to content

Leçon 6.3 · Analyse dynamique· 50 min

Déboguer un malware avec x64dbg

Comment un débogueur arrête un processus, comment piloter x64dbg et la boucle d’analyse : point d’arrêt sur une API, arguments, branche inversée.

Objectifs

  • Expliquer comment fonctionnent, sous le capot d’un débogueur, les points d’arrêt logiciels, matériels et mémoire, le pas-à-pas et les exceptions de première et de seconde chance
  • Se repérer dans les vues CPU, dump, pile, registres, carte mémoire, symboles, points d’arrêt, pile d’appels et handles de x64dbg
  • Dérouler la boucle d’analyse : s’arrêter sur une API, suivre ses arguments, exécuter jusqu’au retour et inspecter le résultat
  • Utiliser des points d’arrêt conditionnels et de journalisation, et patcher une branche en mémoire pour explorer un chemin que l’échantillon ne prendrait pas
  • Déboguer des DLL via un hôte rundll32 et suivre les processus enfants, et reconnaître quand un échantillon a remarqué le débogueur

Références croisées et flux de données se terminait sur un conseil : quand une tranche statique aboutit à « inconnu », lisez la valeur à l’exécution. Un débogueur est l’outil pour cela. Il arrête un processus en cours d’exécution là où vous le décidez, vous permet de lire et de modifier registres et mémoire, puis reprend une instruction ou une fonction à la fois. L’échantillon fait le travail difficile (déchiffrer ses chaînes, résoudre ses API, se dépaqueter) et vous lisez les résultats. Comme l’échantillon s’exécute réellement, tout se passe ici dans la VM Windows isolée de Construire un laboratoire d’analyse sûr.

Nous utilisons x64dbg, le débogueur en mode utilisateur gratuit et open source vers lequel se tournent la plupart des analystes sous Windows (x64dbg.exe pour les cibles 64 bits, x32dbg.exe pour les 32 bits). WinDbg reste l’outil du débogage noyau et des vidages sur plantage ; les concepts s’appliquent aux deux.

Comment fonctionne un débogueur

La boucle de débogage

Un débogueur Windows en mode utilisateur est un processus ordinaire qui a créé la cible avec DEBUG_ONLY_THIS_PROCESS ou s’y est attaché avec DebugActiveProcess. Le noyau suspend alors la cible à chaque événement de débogage et le signale ; le débogueur boucle sur WaitForDebugEvent et ContinueDebugEvent :

Événement de débogageQuand il arrive
CREATE_PROCESS_DEBUG_EVENTLe processus existe, avant que la moindre partie de son code s’exécute
LOAD_DLL_DEBUG_EVENT / UNLOAD_DLL_DEBUG_EVENTUn module est mappé ou démappé
CREATE_THREAD_DEBUG_EVENT / EXIT_THREAD_DEBUG_EVENTUn thread démarre ou se termine
EXCEPTION_DEBUG_EVENTToute exception, y compris les points d’arrêt et les pas-à-pas
OUTPUT_DEBUG_STRING_EVENTLa cible a appelé OutputDebugString (x64dbg l’affiche dans le Log)
EXIT_PROCESS_DEBUG_EVENTLe processus s’est terminé

L’idée clé : les points d’arrêt sont des exceptions. Chacun des mécanismes ci-dessous fait lever une exception au CPU au bon moment.

Points d’arrêt logiciels

Un point d’arrêt logiciel remplace le premier octet d’une instruction par 0xCC, l’instruction int3 sur un octet. Quand le CPU l’exécute, il lève une exception de point d’arrêt (EXCEPTION_BREAKPOINT, 0x80000003). Le débogueur reconnaît l’adresse comme l’une des siennes, remet l’octet d’origine, recule RIP d’un octet (puisque int3 a déjà été exécutée) et vous montre l’instruction comme si de rien n’était. Pour conserver le point d’arrêt pour la fois suivante, il exécute en pas-à-pas l’instruction restaurée puis réécrit 0xCC.

Les points d’arrêt logiciels sont illimités, et c’est ce que pose F2. Leur faiblesse est qu’ils modifient le code : un échantillon qui calcule sa propre somme de contrôle, ou qui lit le premier octet d’une API à la recherche de 0xCC, les voit. N’en posez jamais sur du code sur le point d’être déchiffré en place ; votre 0xCC serait déchiffré avec lui.

Points d’arrêt matériels

Les CPU x86 disposent de quatre registres de débogage, DR0 à DR3, qui contiennent chacun une adresse. DR7 indique lesquels sont activés et, pour chacun, s’il faut se déclencher à l’exécution, à l’écriture ou en lecture/écriture, et sur combien d’octets (1, 2, 4 ou 8). DR6 indique lequel s’est déclenché. Les points d’arrêt matériels ne modifient aucune mémoire ; ils survivent donc aux sommes de contrôle et au déchiffrement en place, et ils sont le seul moyen de s’arrêter quand une adresse de données précise est écrite ou lue.

Deux détails comptent en pratique. Les points d’arrêt sur données sont des traps : l’exception est levée une fois l’instruction d’accès terminée, si bien que RIP pointe sur l’instruction qui suit l’écriture. Et comme les registres de débogage font partie du contexte de chaque thread, le code peut les lire avec GetThreadContext ; voir la détection des points d’arrêt matériels.

Points d’arrêt mémoire

Un point d’arrêt mémoire surveille toute une région en changeant la protection des pages (PAGE_GUARD, ou en retirant un droit d’accès) et en interceptant l’exception qui en résulte. Il fonctionne par page de 4 Ko, si bien que les accès sans rapport sur la même page déclenchent aussi une exception et sont repris silencieusement, ce qui est lent. C’est le bon outil pour « s’arrêter dès que quoi que ce soit s’exécute dans ce tampon fraîchement alloué », un grand classique face à un échantillon qui se dépaquette lui-même.

Pas-à-pas

Positionner le trap flag (TF, bit 8 de RFLAGS) fait lever au CPU une exception de pas-à-pas (EXCEPTION_SINGLE_STEP, 0x80000004) après l’instruction suivante : c’est le step into (F7). Le step over (F8) sur un call pose au contraire un point d’arrêt temporaire après celui-ci et lance l’exécution ; si l’appelé ne revient jamais (il quitte ou lève une exception), vous perdez le contrôle. Voir le pas-à-pas.

Exceptions : première et seconde chance

Pour toute exception qui n’est pas l’un de ses propres points d’arrêt, le débogueur est notifié en premier, avant l’exécution des gestionnaires SEH ou vectorisés du programme : c’est la notification de première chance. Il peut absorber l’exception ou la transmettre. Si un gestionnaire du programme la traite, l’exécution continue ; si aucun ne le fait, le débogueur reçoit une notification de seconde chance et le processus est sur le point de mourir.

Les malwares (logiciels malveillants) lèvent des exceptions exprès, comme flot de contrôle ou comme test anti-débogage (mon gestionnaire s’est-il exécuté ?). Donc transmettez les exceptions de première chance que vous n’avez pas provoquées (Shift+F9 dans x64dbg), et posez un point d’arrêt sur le gestionnaire si ce qu’il fait vous intéresse. Une exception de seconde chance, c’est un plantage.

Se repérer dans x64dbg

Quand vous ouvrez un exécutable, x64dbg marque des pauses aux étapes définies dans Options > Preferences > Events : par défaut le point d’arrêt système dans ntdll (avant tout code du programme), puis les éventuels callbacks TLS, puis le point d’arrêt d’entrée sur le point d’entrée de l’image. Laissez les callbacks TLS activés ; les malwares les utilisent précisément parce qu’ils s’exécutent en premier. Les vues que vous utilisez à chaque session :

VueRaccourciCe qu’elle montreUsage typique
CPUAlt+CDésassemblage, registres et flags, arguments de l’appel, un panneau de dump et la pile, le tout dans un ongletLà où vous passez l’essentiel de votre temps
Dump (dans CPU)Ctrl+G dans le panneauLa mémoire en hexadécimal et en texte ; cinq onglets de dump en basRegarder un tampon se remplir pendant qu’un décodeur s’exécute
Stack (dans CPU)La mémoire à RSP, avec adresses de retour et pointeurs de chaînes annotésLire les adresses de retour et les arguments sur la pile
Memory MapAlt+MChaque région : adresse, taille, module propriétaire, section, type, protectionRepérer de nouvelles régions privées exécutables ; les dumper
SymbolsCtrl+Alt+SModules chargés, leurs imports et leurs exportsPoser des points d’arrêt par nom ; vérifier ce qu’exporte une DLL
BreakpointsAlt+BTous les points d’arrêt logiciels, matériels, mémoire, DLL et exceptions avec leurs compteursActiver, désactiver et modifier les conditions
Call StackAlt+KLa chaîne des adresses de retour du thread courant« Qui a appelé cette API ? »
ThreadsAlt+TLes threads, leurs adresses de départ et leurs étatsBasculer sur un nouveau thread lancé par un échantillon
HandlesHandles ouverts avec leur type et leur nomNoms de mutex, fichiers et clés que détient l’échantillon
LogAlt+LÉvénements de débogage, messages des points d’arrêt, chaînes de débogage, votre sortie de journalisationLire les points d’arrêt de journalisation sans s’arrêter

La barre de commande en bas reproduit la plupart des actions de l’interface. Trois conventions comptent. Les nombres sont en hexadécimal sauf s’ils sont préfixés d’un point (.100 est décimal). Les noms d’API sont des adresses, donc bp OutputDebugStringA fonctionne, et les exports redirigés se résolvent vers leur implémentation finale. Enfin, comme l’ASLR déplace l’image, désignez les adresses de l’échantillon sous la forme module plus RVA : dbglab:$2AAE est la RVA 0x2AAE dans dbglab.exe, où qu’il ait été chargé. Les RVA issues de votre analyse statique (adresse moins la base d’image préférée) fonctionnent alors à chaque exécution.

Le contrôle de l’exécution, en bref : F9 exécuter, F7 step into, F8 step over, Ctrl+F9 exécuter jusqu’au retour, Alt+F9 exécuter jusqu’au code utilisateur (sortir d’une DLL système pour revenir dans l’échantillon), F4 exécuter jusqu’à l’instruction sélectionnée, F2 basculer un point d’arrêt logiciel.

La boucle d’analyse

La plupart des sessions de débogage répètent quatre étapes.

  1. S’arrêter sur une API choisie lors du triage (voir Lire les capacités dans les imports) : CreateFileW pour un chemin, InternetConnectW pour un hôte C2, VirtualAlloc pour un tampon d’unpacking, GetProcAddress pour les noms résolus. S’arrêter sur l’API plutôt que sur un site d’appel attrape tous les appelants, y compris ceux que le désassembleur a manqués.
  2. Suivre les arguments. Sur la première instruction de l’API, la convention d’appel Windows x64 place les quatre premiers arguments dans rcx, rdx, r8, r9 ; les suivants se trouvent au-dessus de l’espace fantôme (shadow space) de 32 octets, et [rsp] est l’adresse de retour. Clic droit sur un registre, Follow in Dump, pour lire un tampon.
  3. Exécuter jusqu’au retour. Ctrl+F9 exécute jusqu’au ret, un F8 revient dans l’appelant : rax contient le résultat et les tampons de sortie sont remplis.
  4. Inspecter et décider. Étiquetez le site d’appel, choisissez l’API suivante, ou modifiez le résultat et observez ce que l’échantillon fait différemment.

Pour les échantillons 32 bits sous x32dbg, les arguments sont sur la pile ([esp+4] est le premier après l’adresse de retour, pour stdcall et cdecl).

Astuce : Avant de lancer quoi que ce soit, posez des points d’arrêt sur les API qui terminent ou masquent l’exécution : ExitProcess, TerminateProcess, CreateProcessW et CreateProcessInternalW, ResumeThread, WriteProcessMemory. Le pire dénouement d’une session de débogage, c’est de voir le processus se terminer, ou passer sa charge utile à un autre processus, avant d’avoir atteint quoi que ce soit d’intéressant.

Points d’arrêt conditionnels et de journalisation

Les DLL système appellent CreateFileW des centaines de fois ; s’arrêter à chaque appel ne sert à rien. Modifiez un point d’arrêt (ou utilisez les commandes SetBreakpoint…) pour lui donner :

  • une condition d’arrêt, une expression évaluée à chaque passage ; le débogueur ne s’arrête que si elle est non nulle ;
  • un texte de journal, une chaîne de format affichée dans le Log à chaque passage ;
  • une commande, exécutée à chaque passage ;
  • Fast Resume, qui saute les mises à jour de l’interface quand la condition est fausse.

Une condition à 0 avec un texte de journal transforme le point d’arrêt en point d’arrêt de journalisation : il ne s’arrête jamais, il se contente d’enregistrer. Deux formes utiles :

text
break condition:  mod.user([rsp])
    stop only when the return address is in a user module (the sample), not in a system DLL

break condition:  0
log text:         CreateFileW from {a:[rsp]} -> {utf16@rcx}
    never stop; log the caller and the UTF-16 path of every call

{utf16@rcx} et {utf8@rcx} lisent une chaîne à une adresse, et {a:[rsp]} affiche l’adresse de retour avec son module et son étiquette. Le compteur de passages ($breakpointcounter) permet de s’arrêter au n-ième appel, ce qui aide quand un décodeur est appelé une fois par chaîne et que celle qui vous intéresse est la dix-septième. La leçon compagne Tracer les appels d’API et les appels système étend cette idée à des traces d’API complètes.

Patcher une branche pour explorer un chemin

Les échantillons conditionnent leur comportement à des vérifications : un mutex existe, un argument manque, la machine ressemble à une sandbox (bac à sable). Le débogueur vous permet de prendre le côté que l’échantillon a refusé, de trois façons, de la moins à la plus invasive :

MéthodeCommentPersistant ?
Inverser un flagS’arrêter sur le jcc, double-cliquer sur ZF (ou CF, SF…) dans le panneau des registresCette exécution seulement
Modifier un registre ou une valeur de retourAprès le retour de la fonction de vérification, mettre dans rax la valeur qu’attend le chemin de succèsCette exécution seulement
Patcher l’instructionLa sélectionner, appuyer sur Space pour assembler (je → jne, ou remplir de nop)Pour la durée de vie du processus ; File > Patch file (Ctrl+P) peut l’écrire sur disque

Préférez les deux premières : elles sont réversibles et laissent le code intact. Patchez quand une vérification s’exécute de nombreuses fois. Le patching persistant est le sujet de Patcher des binaires pour neutraliser les vérifications. Sachez quel saut conditionnel lit quel flag : je saute quand ZF=1, jne quand ZF=0.

Attention : Un chemin forcé est une expérience, pas une observation. Signalez-le comme forcé et nommez la vérification que vous avez contournée : la capacité est réelle, mais la séquence des événements peut ne pas correspondre à ce que verrait une victime.

DLL et processus enfants

Déboguer une DLL

x64dbg peut ouvrir une DLL directement (il la charge via un petit processus chargeur et s’arrête sur DllMain), mais les DLL malveillantes attendent en général un hôte précis, le plus souvent rundll32.exe dll,Export comme dans Processus, threads et DLL. Pour reproduire cela :

  1. Ouvrez C:\Windows\System32\rundll32.exe dans x64dbg.
  2. File > Change Command Line et réglez-la sur "C:\Windows\System32\rundll32.exe" C:\lab\sample.dll,ExportName args.
  3. Arrêtez-vous à l’arrivée de la DLL : bpdll sample.dll (ou activez DLL Load dans les réglages d’événements), puis lancez l’exécution.
  4. Au moment de la pause, la DLL est mappée mais aucune partie de son code ne s’est exécutée. Posez un point d’arrêt sur son point d’entrée (DllMain) et sur l’export (bp sample:ExportName), puis lancez l’exécution. regsvr32.exe fonctionne de la même façon avec DllRegisterServer.

Suivre les processus enfants

Un débogueur attaché au parent ne suit pas automatiquement un enfant que celui-ci lance. Plutôt que de dépendre d’options propres à une version, attrapez l’enfant avant qu’il ne s’exécute :

  1. Posez un point d’arrêt sur CreateProcessInternalW, le chemin commun sous CreateProcessA et CreateProcessW. Si les options incluent déjà CREATE_SUSPENDED (0x4), le parent compte modifier l’enfant, ce qui est l’ouverture du process hollowing : continuez à observer le parent.
  2. Sinon, ajoutez vous-même CREATE_SUSPENDED, exécutez jusqu’au retour et lisez le PID de l’enfant dans PROCESS_INFORMATION.
  3. Attachez un second x64dbg à ce PID, posez vos points d’arrêt et reprenez son thread principal depuis la vue Threads.

Quand l’échantillon vous remarque

De nombreux échantillons vérifient la présence d’un débogueur, d’un simple appel d’API (IsDebuggerPresent, CheckRemoteDebuggerPresent) à la lecture des structures que modifie le débogueur (NtGlobalFlag, flags du tas), en passant par la recherche de vos points d’arrêt (détection des points d’arrêt matériels) et la mesure du temps d’exécution du code (chronométrage RDTSC, chronométrage GetTickCount). Symptômes : sortie prématurée, sommeil sans fin, plantage après une exception que vous avez absorbée, ou chemin inoffensif qu’il ne prend pas hors du débogueur.

La première réponse est ScyllaHide, un plugin anti-anti-débogage open source maintenu dans le cadre du projet x64dbg (installez-le dans le dossier plugins de x64dbg ; à ne pas confondre avec Scylla, le reconstructeur d’imports fourni avec l’outil). Il patche les vérifications courantes et masque les artefacts du PEB et du tas que laisse un débogueur. Quand une vérification lui résiste, utilisez les outils de cette leçon : trouvez-la avec un point d’arrêt sur API, puis inversez le flag ou patchez la branche. Anti-débogage couvre le catalogue en détail.

Lab : décoder, conditionner et déboguer

Un programme inoffensif décode une URL par XOR, vérifie une variable d’environnement, et ne passe l’URL à OutputDebugStringA que si la vérification réussit. Les étapes 1 à 4 s’exécutent sur n’importe quelle machine de compilation ; les étapes 5 à 8 dans la VM Windows avec x64dbg. Les listings proviennent de MinGW-w64 GCC 15.2.0, objdump de binutils, Capstone 5.0.9, pefile 2024.8.26 et Wine 11.0 ; les adresses peuvent différer avec d’autres compilateurs.

  1. Enregistrez sous dbglab.c :

    c
    /* dbglab.c: decode a string, gate on an environment variable, hand the result to an API */
    #include <windows.h>
    #include <stdio.h>
    
    /* "http://update.example.com/lab" XOR 0x5A */
    static const unsigned char ENC_URL[] = {
        0x32, 0x2e, 0x2e, 0x2a, 0x60, 0x75, 0x75, 0x2f, 0x2a, 0x3e, 0x3b, 0x2e,
        0x3f, 0x74, 0x3f, 0x22, 0x3b, 0x37, 0x2a, 0x36, 0x3f, 0x74, 0x39, 0x35,
        0x37, 0x75, 0x36, 0x3b, 0x38
    };
    
    __attribute__((noinline, noipa))
    static void decode(char *out, const unsigned char *in, size_t n, unsigned char key) {
        for (size_t i = 0; i < n; i++)
            out[i] = (char)(in[i] ^ key);
        out[n] = '\0';
    }
    
    __attribute__((noinline, noipa))
    static int gate_open(void) {
        char value[8];
        return GetEnvironmentVariableA("LAB_GO", value, sizeof value) != 0;
    }
    
    int main(void) {
        char url[64];
        decode(url, ENC_URL, sizeof ENC_URL, 0x5A);
        if (gate_open()) {
            OutputDebugStringA(url);
            puts("sent to debugger");
        } else {
            OutputDebugStringA("dbglab: gate closed");
            puts("gate closed");
        }
        return 0;
    }

    noipa empêche GCC de spécialiser les fonctions auxiliaires pour leurs arguments constants, si bien qu’elles conservent une convention d’appel normale. Compilez-le sans symboles :

    bash
    x86_64-w64-mingw32-gcc -O2 -s -o dbglab.exe dbglab.c
    strings -n 6 dbglab.exe | grep -i -E "http|update|gate|LAB_GO"
    text
    LAB_GO
    dbglab: gate closed
    gate closed

    L’URL ne figure pas dans le fichier, et c’est tout l’intérêt du chiffrement de chaînes par XOR.

  2. Trouvez le site d’appel statiquement. En étendant le chercheur de xrefs du module précédent, ce script associe les emplacements de l’IAT aux noms d’imports et tire les bornes des fonctions de .pdata (les fichiers PE x64 y listent toutes les fonctions non-feuilles, qu’ils soient strippés ou non), puis affiche chaque fonction qui appelle un import donné :

    python
    # apisites.py: find every call to an import and disassemble the function around it
    # usage: python apisites.py file.exe ImportName
    import sys
    import pefile
    from capstone import Cs, CS_ARCH_X86, CS_MODE_64
    from capstone.x86 import X86_OP_MEM, X86_REG_RIP
    
    pe = pefile.PE(sys.argv[1])
    base = pe.OPTIONAL_HEADER.ImageBase
    iat = {imp.address: imp.name.decode()
           for d in pe.DIRECTORY_ENTRY_IMPORT for imp in d.imports if imp.name}
    image = pe.get_memory_mapped_image()
    
    def cstring(va):
        rva = va - base
        end = image.find(b"\0", rva)
        s = image[rva:end]
        return s.decode() if s and all(32 <= b < 127 for b in s) and len(s) >= 4 else None
    
    # x64 PE files list every non-leaf function's bounds in .pdata (RUNTIME_FUNCTION)
    funcs = [(base + e.struct.BeginAddress, base + e.struct.EndAddress)
             for e in pe.DIRECTORY_ENTRY_EXCEPTION]
    
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = True
    
    def annotate(insn):
        for op in insn.operands:
            if op.type == X86_OP_MEM and op.mem.base == X86_REG_RIP:
                ea = insn.address + insn.size + op.mem.disp
                if ea in iat:
                    return iat[ea]
                s = cstring(ea)
                if s:
                    return f'"{s}"'
                return hex(ea)
        return ""
    
    target = sys.argv[2]
    for start, end in funcs:
        code = image[start - base:end - base]
        insns = list(md.disasm(code, start))
        if not any(annotate(i) == target and i.mnemonic == "call" for i in insns):
            continue
        print(f"function 0x{start:x}-0x{end:x} calls {target}:")
        for i in insns:
            note = annotate(i)
            print(f"  {i.address:x}  {i.mnemonic:6} {i.op_str:34} {'; ' + note if note else ''}".rstrip())
    bash
    python apisites.py dbglab.exe OutputDebugStringA
    text
    function 0x140002a80-0x140002aea calls OutputDebugStringA:
      140002a80  push   rbx
      140002a81  sub    rsp, 0x60
      140002a85  call   0x1400015f0
      140002a8a  mov    r9d, 0x5a
      140002a90  mov    r8d, 0x1d
      140002a96  lea    rdx, [rip + 0x15a3]                ; "2..*`uu/*>;.?t?";7*6?t957u6;8"
      140002a9d  lea    rcx, [rsp + 0x20]
      140002aa2  call   0x1400014a0
      140002aa7  call   0x1400014e0
      140002aac  test   eax, eax
      140002aae  je     0x140002acf
      140002ab0  lea    rcx, [rsp + 0x20]
      140002ab5  call   qword ptr [rip + 0x57e5]           ; OutputDebugStringA
      140002abb  lea    rcx, [rip + 0x1545]                ; "sent to debugger"
      140002ac2  call   0x140002938
      140002ac7  xor    eax, eax
      140002ac9  add    rsp, 0x60
      140002acd  pop    rbx
      140002ace  ret
      140002acf  lea    rcx, [rip + 0x1542]                ; "dbglab: gate closed"
      140002ad6  call   qword ptr [rip + 0x57c4]           ; OutputDebugStringA
      140002adc  lea    rcx, [rip + 0x1549]                ; "gate closed"
      140002ae3  call   0x140002938
      140002ae8  jmp    0x140002ac7

    C’est main. L’appel en 0x140002aa2 reçoit un tampon sur la pile, un blob, une longueur de 0x1d (29) et 0x5a : c’est le décodeur. (Le blob se trouve être imprimable, donc le script l’a étiqueté ; un texte dénué de sens à côté d’une petite constante est en soi un indice.) Le résultat de l’appel en 0x140002aa7 pilote le je en 0x140002aae, qui choisit l’un des deux appels à OutputDebugStringA : le tampon sur la pile, ou un message fixe « gate closed ».

  3. Confirmez les fonctions auxiliaires. Le décodeur en 0x1400014a0 (objdump -d -M intel, nop d’alignement retirés) est une boucle XOR octet par octet avec la clé dans r9d :

    text
    1400014a0:	4d 85 c0             	test   r8,r8
    1400014a3:	74 30                	je     0x1400014d5
    1400014a5:	31 c0                	xor    eax,eax
    1400014c0:	44 0f b6 14 02       	movzx  r10d,BYTE PTR [rdx+rax*1]
    1400014c5:	45 31 ca             	xor    r10d,r9d
    1400014c8:	44 88 14 01          	mov    BYTE PTR [rcx+rax*1],r10b
    1400014cc:	48 83 c0 01          	add    rax,0x1
    1400014d0:	49 39 c0             	cmp    r8,rax
    1400014d3:	75 eb                	jne    0x1400014c0
    1400014d5:	42 c6 04 01 00       	mov    BYTE PTR [rcx+r8*1],0x0
    1400014da:	c3                   	ret

    apisites.py dbglab.exe GetEnvironmentVariableA trouve la vérification :

    text
    function 0x1400014e0-0x140001509 calls GetEnvironmentVariableA:
      1400014e0  sub    rsp, 0x38
      1400014e4  mov    r8d, 8
      1400014ea  lea    rcx, [rip + 0x2b0f]                ; "LAB_GO"
      1400014f1  lea    rdx, [rsp + 0x28]
      1400014f6  call   qword ptr [rip + 0x6d84]           ; GetEnvironmentVariableA
      1400014fc  test   eax, eax
      1400014fe  setne  al
      140001501  movzx  eax, al
      140001504  add    rsp, 0x38
      140001508  ret

    Notez les RVA (adresse moins 0x140000000 ; l’image a DYNAMIC_BASE, elle sera donc chargée ailleurs) : décodeur 0x14A0, vérification 0x14E0, je en 0x2AAE, appel avec l’URL en 0x2AB5 (retour en 0x2ABB), appel « gate closed » en 0x2AD6 (retour en 0x2ADC).

  4. Vérifiez la prédiction. Le canal debugstr de Wine affiche les chaînes de débogage, ce qui confirme les deux chemins sur la machine de compilation :

    bash
    WINEDEBUG=-all,+debugstr wine dbglab.exe
    LAB_GO=1 WINEDEBUG=-all,+debugstr wine dbglab.exe
    text
    0024:warn:debugstr:OutputDebugStringA "dbglab: gate closed"
    gate closed
    0024:warn:debugstr:OutputDebugStringA "http://update.example.com/lab"
    sent to debugger
  5. Arrêtez-vous sur l’API. Dans la VM, avec LAB_GO non défini, ouvrez dbglab.exe dans x64dbg et exécutez (F9) au-delà des points d’arrêt système et d’entrée. Saisissez bp OutputDebugStringA et lancez l’exécution. À l’arrêt, Follow in Dump sur rcx affiche « dbglab: gate closed », et [rsp] vaut dbglab.exe + 0x2ADC, le site d’appel « gate closed » de l’étape 3. Ctrl+F9, puis F8, ramène dans main.

  6. Prenez le décodeur sur le fait. Redémarrez (Ctrl+F2), bp dbglab:$2AA2 (l’appel au décodeur) et lancez l’exécution. rcx est le tampon de sortie : suivez-le dans le dump et posez un point d’arrêt matériel en écriture sur son premier octet (bph rcx, w, 1). Lancez : RIP s’arrête en dbglab:$14CC, l’instruction après le mov qui a écrit h, car les points d’arrêt sur données sont des traps. Supprimez-le et appuyez sur Ctrl+F9 : l’URL entière apparaît dans le dump.

  7. Inversez la branche. Exécutez jusqu’à dbglab:$2AAE, le je. La vérification a renvoyé 0, donc ZF vaut 1 et le saut serait pris. Double-cliquez sur ZF pour le remettre à zéro et appuyez sur F8 : l’exécution passe à dbglab:$2AB0. Au point d’arrêt sur OutputDebugStringA, rcx pointe sur le tampon de pile qui contient http://update.example.com/lab et [rsp] vaut dbglab.exe + 0x2ABB.

  8. Remplacez l’arrêt par un journal. Modifiez le point d’arrêt sur OutputDebugStringA : condition d’arrêt 0, texte de journal ODS from {a:[rsp]}: {utf8@rcx}. Exécutez une fois tel quel ; puis redémarrez, arrêtez-vous sur le je et patchez-le en nop (Space, avec le remplissage NOP activé ; les patchs en mémoire ne survivent pas à un redémarrage) avant de relancer. Chaque exécution va jusqu’au bout en journalisant l’appelant et la chaîne.

Questions : Si l’échantillon lisait le premier octet de OutputDebugStringA pendant que votre point d’arrêt logiciel est posé, que verrait-il, et quel type de point d’arrêt de cette leçon éviterait cela ? Pendant que le point d’arrêt matériel de l’étape 6 est posé, quels registres de débogage contiennent quoi, et qu’apprendrait l’échantillon en appelant GetThreadContext sur son propre thread ? À l’étape 7, qu’est-il plus sûr de rapporter comme observation : l’URL, ou le fait que le programme l’envoie ? Quelle modification unique de dbglab.c ferait se déclencher le point d’arrêt matériel de l’étape 6 dans une autre fonction ?

À retenir

  • Les points d’arrêt et les pas-à-pas sont des exceptions que le débogueur revendique comme siennes.
  • Les points d’arrêt logiciels (0xCC) sont illimités mais visibles ; les points d’arrêt matériels utilisent quatre registres de débogage et peuvent surveiller des données ; les points d’arrêt mémoire modifient la protection des pages pour couvrir des régions.
  • Transmettez les exceptions de première chance que vous n’avez pas provoquées ; une seconde chance signifie un plantage.
  • Désignez les adresses en module plus RVA (dbglab:$2AAE) pour que vos trouvailles statiques survivent à l’ASLR, et laissez les points d’arrêt sur callbacks TLS activés.
  • La boucle d’analyse : s’arrêter sur une API, lire rcx/rdx/r8/r9 et [rsp], exécuter jusqu’au retour, inspecter, choisir l’API suivante.
  • Les points d’arrêt conditionnels et de journalisation réduisent le bruit ; inverser un flag ou patcher un jcc explore un chemin, que vous signalez comme forcé.
  • Les DLL ont besoin d’un hôte et d’un point d’arrêt au chargement de DLL ; les enfants doivent être attrapés suspendus et attachés séparément.
  • Face à l’anti-débogage, commencez par ScyllaHide, puis neutralisez à la main ce qui résiste.