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ébogage | Quand il arrive |
|---|---|
CREATE_PROCESS_DEBUG_EVENT | Le processus existe, avant que la moindre partie de son code s’exécute |
LOAD_DLL_DEBUG_EVENT / UNLOAD_DLL_DEBUG_EVENT | Un module est mappé ou démappé |
CREATE_THREAD_DEBUG_EVENT / EXIT_THREAD_DEBUG_EVENT | Un thread démarre ou se termine |
EXCEPTION_DEBUG_EVENT | Toute exception, y compris les points d’arrêt et les pas-à-pas |
OUTPUT_DEBUG_STRING_EVENT | La cible a appelé OutputDebugString (x64dbg l’affiche dans le Log) |
EXIT_PROCESS_DEBUG_EVENT | Le 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 :
| Vue | Raccourci | Ce qu’elle montre | Usage typique |
|---|---|---|---|
| CPU | Alt+C | Désassemblage, registres et flags, arguments de l’appel, un panneau de dump et la pile, le tout dans un onglet | Là où vous passez l’essentiel de votre temps |
| Dump (dans CPU) | Ctrl+G dans le panneau | La mémoire en hexadécimal et en texte ; cinq onglets de dump en bas | Regarder 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és | Lire les adresses de retour et les arguments sur la pile | |
| Memory Map | Alt+M | Chaque région : adresse, taille, module propriétaire, section, type, protection | Repérer de nouvelles régions privées exécutables ; les dumper |
| Symbols | Ctrl+Alt+S | Modules chargés, leurs imports et leurs exports | Poser des points d’arrêt par nom ; vérifier ce qu’exporte une DLL |
| Breakpoints | Alt+B | Tous les points d’arrêt logiciels, matériels, mémoire, DLL et exceptions avec leurs compteurs | Activer, désactiver et modifier les conditions |
| Call Stack | Alt+K | La chaîne des adresses de retour du thread courant | « Qui a appelé cette API ? » |
| Threads | Alt+T | Les threads, leurs adresses de départ et leurs états | Basculer sur un nouveau thread lancé par un échantillon |
| Handles | Handles ouverts avec leur type et leur nom | Noms de mutex, fichiers et clés que détient l’échantillon | |
| Log | Alt+L | Événements de débogage, messages des points d’arrêt, chaînes de débogage, votre sortie de journalisation | Lire 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.
- S’arrêter sur une API choisie lors du triage (voir
Lire les capacités dans les imports) :
CreateFileWpour un chemin,InternetConnectWpour un hôte C2,VirtualAllocpour un tampon d’unpacking,GetProcAddresspour 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. - 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. - Exécuter jusqu’au retour.
Ctrl+F9exécute jusqu’auret, unF8revient dans l’appelant :raxcontient le résultat et les tampons de sortie sont remplis. - 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,CreateProcessWetCreateProcessInternalW,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 :
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éthode | Comment | Persistant ? |
|---|---|---|
| Inverser un flag | S’arrêter sur le jcc, double-cliquer sur ZF (ou CF, SF…) dans le panneau des registres | Cette exécution seulement |
| Modifier un registre ou une valeur de retour | Après le retour de la fonction de vérification, mettre dans rax la valeur qu’attend le chemin de succès | Cette exécution seulement |
| Patcher l’instruction | La 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 :
- Ouvrez
C:\Windows\System32\rundll32.exedans x64dbg. - File > Change Command Line et réglez-la sur
"C:\Windows\System32\rundll32.exe" C:\lab\sample.dll,ExportName args. - 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. - 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.exefonctionne de la même façon avecDllRegisterServer.
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 :
- Posez un point d’arrêt sur
CreateProcessInternalW, le chemin commun sousCreateProcessAetCreateProcessW. 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. - Sinon, ajoutez vous-même
CREATE_SUSPENDED, exécutez jusqu’au retour et lisez le PID de l’enfant dansPROCESS_INFORMATION. - 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.
-
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; }noipaempê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 closedL’URL ne figure pas dans le fichier, et c’est tout l’intérêt du chiffrement de chaînes par XOR.
-
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 OutputDebugStringAtext 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 0x140002ac7C’est
main. L’appel en0x140002aa2reçoit un tampon sur la pile, un blob, une longueur de0x1d(29) et0x5a: 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 en0x140002aa7pilote lejeen0x140002aae, qui choisit l’un des deux appels àOutputDebugStringA: le tampon sur la pile, ou un message fixe « gate closed ». -
Confirmez les fonctions auxiliaires. Le décodeur en
0x1400014a0(objdump -d -M intel,nopd’alignement retirés) est une boucle XOR octet par octet avec la clé dansr9d: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 retapisites.py dbglab.exe GetEnvironmentVariableAtrouve 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 retNotez les RVA (adresse moins
0x140000000; l’image aDYNAMIC_BASE, elle sera donc chargée ailleurs) : décodeur0x14A0, vérification0x14E0,jeen0x2AAE, appel avec l’URL en0x2AB5(retour en0x2ABB), appel « gate closed » en0x2AD6(retour en0x2ADC). -
Vérifiez la prédiction. Le canal
debugstrde 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.exetext 0024:warn:debugstr:OutputDebugStringA "dbglab: gate closed" gate closed 0024:warn:debugstr:OutputDebugStringA "http://update.example.com/lab" sent to debugger -
Arrêtez-vous sur l’API. Dans la VM, avec
LAB_GOnon défini, ouvrezdbglab.exedans x64dbg et exécutez (F9) au-delà des points d’arrêt système et d’entrée. Saisissezbp OutputDebugStringAet lancez l’exécution. À l’arrêt, Follow in Dump surrcxaffiche « dbglab: gate closed », et[rsp]vautdbglab.exe+0x2ADC, le site d’appel « gate closed » de l’étape 3.Ctrl+F9, puisF8, ramène dansmain. -
Prenez le décodeur sur le fait. Redémarrez (
Ctrl+F2),bp dbglab:$2AA2(l’appel au décodeur) et lancez l’exécution.rcxest 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 :RIPs’arrête endbglab:$14CC, l’instruction après lemovqui a écrith, car les points d’arrêt sur données sont des traps. Supprimez-le et appuyez surCtrl+F9: l’URL entière apparaît dans le dump. -
Inversez la branche. Exécutez jusqu’à
dbglab:$2AAE, leje. La vérification a renvoyé0, doncZFvaut1et le saut serait pris. Double-cliquez surZFpour le remettre à zéro et appuyez surF8: l’exécution passe àdbglab:$2AB0. Au point d’arrêt surOutputDebugStringA,rcxpointe sur le tampon de pile qui contienthttp://update.example.com/labet[rsp]vautdbglab.exe+0x2ABB. -
Remplacez l’arrêt par un journal. Modifiez le point d’arrêt sur
OutputDebugStringA: condition d’arrêt0, texte de journalODS from {a:[rsp]}: {utf8@rcx}. Exécutez une fois tel quel ; puis redémarrez, arrêtez-vous sur lejeet patchez-le ennop(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/r9et[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
jccexplore 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.