Leçon 6.4 · Analyse dynamique· 45 min
Tracer les appels d’API et les appels système
Enregistrez les appels d’API et système d’un échantillon au lieu de les suivre pas à pas, réduisez le bruit, lisez la trace et sachez où elle est aveugle.
Objectifs
- Choisir entre API Monitor, la journalisation x64dbg, les piles Procmon, frida-trace, ETW et strace/ltrace selon la question posée
- Restreindre une trace aux appels qui comptent et filtrer le bruit du runtime et du système
- Reconstituer un comportement à partir d’une trace, en suivant les handles, les valeurs de retour et la chaîne d’appels de Win32 vers l’API native
- Expliquer comment les appels système directs, le décrochage des hooks et l’attachement tardif mettent en échec le traçage, et quels points d’observation y survivent
Déboguer un malware avec x64dbg s’est terminé sur un point d’arrêt de journalisation qui ne s’arrête jamais et se contente d’enregistrer. Généralisez-le et vous obtenez le traçage : instrumentez un ensemble de fonctions, laissez l’échantillon s’exécuter presque à pleine vitesse, et lisez le journal. Une trace répond à « qu’a-t-il appelé, avec quoi, dans quel ordre, et qu’est-ce qui est revenu ? » pour des milliers d’appels à la fois.
Elle se situe entre les vues dont vous disposez déjà. La surveillance comportementale montre les effets sur le système mais pas la façon dont l’échantillon y est parvenu ; le débogage montre un instant dans son intégralité. Une trace montre les propres requêtes de l’échantillon sur toute l’exécution. Le flux de travail habituel : surveiller, tracer pour expliquer ce que la surveillance a montré, et ne déboguer que ce que la trace ne peut pas expliquer.
Où se place un traceur
Tout traceur intercepte les appels à une certaine couche, et cette couche détermine ce qu’il peut voir et avec quelle facilité on peut le tromper. Sous Windows, une écriture de fichier typique en traverse trois :
sample code ──► kernel32 / kernelbase CreateFileW, WriteFile (Win32 API)
──► ntdll NtCreateFile, NtWriteFile (native API)
──► syscall instruction ──► kernel (system call)| Outil | Où il intercepte | Voit | Aveugle à |
|---|---|---|---|
| API Monitor | Hooks inline dans le processus cible | Appels Win32 et natifs avec arguments, structures et valeurs de retour décodés | Appels système directs ; chemins de code décrochés ou résolus à la main |
| Points d’arrêt de journalisation x64dbg | Points d’arrêt du débogueur (int3) | N’importe quelle adresse choisie, avec n’importe quelle expression formatable | Tout ce sur quoi vous n’avez pas posé de point d’arrêt ; les vérifications anti-débogage le voient |
| frida-trace | Hooks inline posés par un agent JavaScript injecté | Exports et adresses arbitraires, avec des gestionnaires scriptables | Comme API Monitor ; l’agent est visible dans le processus |
| Process Monitor | Callbacks noyau et minifiltre du système de fichiers | Événements fichier, registre, processus, chargement d’image et réseau, avec une pile par événement | Tout ce qui n’appartient pas à ces types d’événements |
| ETW (SilkETW, logman, capteurs EDR) | Fournisseurs dans le noyau et dans des composants en mode utilisateur | Événements de processus, d’images, de réseau et de nombreux sous-systèmes, depuis l’extérieur du processus | Tout ce que les fournisseurs activés n’émettent pas |
| strace (Linux) | Arrêts ptrace à chaque appel système | Chaque appel système avec ses arguments décodés | La logique au niveau des bibliothèques qui ne fait aucun appel système |
| ltrace (Linux) | Points d’arrêt sur les entrées PLT des binaires liés dynamiquement | Appels de bibliothèque comme strcmp et getenv | Binaires statiques ; appels directs qui contournent la PLT |
La distinction qui compte le plus est dans le processus ou hors du
processus. API Monitor, Frida et les points d’arrêt du débogueur vivent dans
l’espace d’adressage de l’échantillon ou s’y attachent : arguments riches, mais
l’échantillon peut les voir et les défaire. Procmon, ETW et strace observent
depuis le noyau : plus grossiers, mais l’échantillon ne peut pas les supprimer
depuis le mode utilisateur. L’entrée API hooking du
glossaire couvre la mécanique du hooking ; la façon dont l’Interceptor de Frida
réécrit le prologue d’une fonction est traitée en détail dans
Instrumentation binaire dynamique.
Les outils en pratique
API Monitor
API Monitor est le moyen le plus rapide d’obtenir une trace Win32 lisible sous Windows. Ses définitions pour des milliers d’API décodent options, structures et codes d’erreur. Utilisez la version 64 bits pour les échantillons 64 bits : cochez les fonctions dans le panneau API Filter, lancez l’échantillon avec Monitor New Process, et lisez le panneau Summary, une ligne par appel avec module appelant, arguments, valeur de retour et erreur. Les panneaux Parameters et Call Stack détaillent l’appel sélectionné, et ses points d’arrêt peuvent marquer une pause avant ou après un appel pour vous permettre d’en modifier les arguments.
x64dbg : points d’arrêt de journalisation et trace
Des points d’arrêt avec une condition d’arrêt à 0 et un texte de journal sont
déjà un traceur. Pour un travail au niveau de l’instruction, Trace into/over
until condition évalue un texte de journal à chaque instruction jusqu’à ce
qu’une condition d’arrêt soit remplie (par exemple, RIP qui quitte le module de
l’échantillon) ; TraceSetLogFile l’écrit sur disque. C’est lent, alors
visez une seule fonction, comme un décodeur.
Les piles de Process Monitor
La fonction de traçage sous-exploitée de Procmon, c’est la pile : l’onglet
Stack d’un événement montre la chaîne d’appels au moment de l’opération, les
trames noyau marquées K et les trames utilisateur U. Avec les symboles
configurés (Options > Configure Symbols), les trames se résolvent en noms comme
KernelBase.dll!CreateFileW, et la première trame située dans le module de
l’échantillon vous donne la RVA de la fonction responsable.
frida-trace
frida-trace génère un petit gestionnaire JavaScript pour chaque fonction que
vous nommez, injecte l’agent de Frida et affiche une ligne par appel. Il
fonctionne de la même façon sous Windows, Linux et macOS :
frida-trace -f <program> -i <export glob> [-i ...] [-x <exclude glob>] [-a module!offset]
frida-trace -p <pid> | -n <name> attach to a running process instead-i accepte des motifs (-i "Reg*Value*"), -I/-X incluent ou excluent des
modules entiers, et -a hooke une fonction non exportée par module et offset.
Les gestionnaires générés sous __handlers__/ sont faits pour être modifiés :
afficher une valeur de retour, dumper un tampon ou filtrer par appelant. C’est
exactement ce que fait le lab ci-dessous.
Traçage basé sur ETW
Event Tracing for Windows est le système de journalisation propre au noyau, et
c’est ce que consomment les capteurs EDR. Les fournisseurs noyau comme
Microsoft-Windows-Kernel-Process, -Kernel-File et -Kernel-Network émettent
des événements quoi que fasse l’échantillon à son propre ntdll. SilkETW
(Mandiant) s’abonne à des fournisseurs noyau ou en mode utilisateur, filtre, et
écrit du JSON que vous pouvez fouiller ou confronter à des règles YARA. Sous
Linux, des outils eBPF comme Tetragon jouent le même rôle avec des sondes
noyau définies par des politiques. Le compromis porte sur la granularité : ETW
dit qu’un fichier a été créé et par qui, pas quel tampon déchiffré une fonction
interne a reçu.
strace et ltrace sous Linux
Pour les échantillons ELF
(ELF pour les analystes de malware),
strace enregistre chaque appel système et ltrace
chaque appel passant par la PLT vers les bibliothèques partagées. Ils répondent
à des questions différentes : strace montre openat, connect et execve ;
ltrace montre le strcmp contre un mot de passe codé en dur qui n’atteint
jamais le noyau.
strace -f -tt -s 256 -y -o strace.txt ./sample # follow forks, timestamps, long strings, fd paths
strace -f -e trace=%file,%network,%process ./sample # only the syscall classes you care about
ltrace -f -i ./sample # library calls with the caller's addressltrace ne voit que les appels qui passent par la PLT, si bien que les binaires
liés statiquement (courants en Go et pour les malwares (logiciels malveillants) IoT) ne lui offrent rien à
hooker ; strace fonctionne toujours sur eux.
Que tracer, et comment limiter le bruit
Une trace complète de tout est illisible : le runtime C à lui seul fait des
centaines d’appels avant main, et les DLL système s’appellent constamment les
unes les autres. Délimitez-la :
- Partez d’une question. « Où obtient-il son C2 ? » implique les API réseau, de chaînes et de cryptographie. « Comment persiste-t-il ? » implique les API de registre, de fichiers, de services et de tâches. La liste des imports de votre triage (voir Lire les capacités dans les imports) est le menu.
- Tracez les appels faits par l’échantillon, pas par le système. Filtrez par
appelant : ne gardez un appel que si son adresse de retour se trouve dans le
module de l’échantillon (ou dans de la mémoire qu’il a allouée). La plupart
des outils savent le faire : le filtre de modules d’API Monitor, une condition
x64dbg comme
mod.user([rsp]), ou unifdans un gestionnaire Frida. - Choisissez une seule couche par question. Tracer à la fois
CreateFileWetNtCreateFiledouble chaque entrée. Tracez Win32 pour la lisibilité ; n’ajoutez la couche native que si vous soupçonnez l’échantillon de court-circuiter Win32. - Sautez le démarrage. Attachez-vous après l’initialisation, ou ignorez tout ce qui précède le premier appel issu du propre code de l’échantillon, si vous savez que le démarrage n’est pas la partie intéressante.
- Journalisez les retours autant que les arguments. Une trace d’appels sans résultats ne permet pas de distinguer une vérification échouée d’une vérification réussie.
Le lab montre l’effet de l’étape 2 sur une vraie trace : environ 184 000 lignes réduites à 27 appels, dont trois constituent le comportement propre du programme.
Astuce : Les API résolues à la main (
GetProcAddressou hachage d’API) n’apparaissent jamais dans la table des imports, si bien que les hooks qui patchent l’IAT les manquent. Les hooks inline, qui patchent la fonction elle-même, les attrapent d’où que vienne le pointeur. Sachez quel type utilise votre outil.
Lire une trace
Une trace est une liste de requêtes. La transformer en comportement relève surtout de la comptabilité :
- Suivez les handles. Un handle renvoyé par un appel est un argument du
suivant.
CreateFileWrenvoie0x1a4; ensuite,WriteFile(0x1a4, …)etCloseHandle(0x1a4)concernent ce fichier. Il en va de même pour les clés de registre, les sockets et les handles de processus. - Lisez les valeurs de retour et les erreurs. Un
RegOpenKeyExWqui renvoieERROR_FILE_NOT_FOUND, suivi d’une sortie prématurée, est une vérification de l’environnement. Une valeur que l’échantillon lit puis compare est souvent un interrupteur de configuration. - Regroupez les appels en actions. Ouvrir, écrire, fermer, c’est « a déposé
un fichier » ;
OpenProcess,VirtualAllocEx,WriteProcessMemory,CreateRemoteThread, c’est une injection. Nommez chaque groupe et rédigez la chronologie en ces termes. - Surveillez les arguments pour y trouver des données décodées. Les chaînes chiffrées par XOR dans le fichier arrivent en clair à l’API qui les utilise. C’est souvent le moyen le moins coûteux de retrouver une adresse de C2.
- Faites attention à la chronologie. De longs intervalles autour de
SleepouWaitForSingleObjectsont des délais ; un traceur qui ralentit l’échantillon peut aussi déclencher des vérifications qui mesurent le temps écoulé, comme la détection de l’accélération du sommeil.
API native et Win32 dans une trace
Quand une trace inclut les deux couches, chaque appel Win32 se décompose en appels natifs en dessous. Quelques paires courantes :
| Win32 (kernel32 / kernelbase) | Native (ntdll) |
|---|---|
CreateFileW | NtCreateFile / NtOpenFile |
WriteFile, ReadFile | NtWriteFile, NtReadFile |
VirtualAlloc(Ex), VirtualProtect(Ex) | NtAllocateVirtualMemory, NtProtectVirtualMemory |
CreateProcessW | NtCreateUserProcess |
RegSetValueExW | NtSetValueKey |
CreateMutexW | NtCreateMutant |
GetEnvironmentVariableA | RtlQueryEnvironmentVariable_U (aucun appel système) |
Deux conséquences. Les fonctions ANSI (A) convertissent leurs chaînes en
UTF-16 et appellent la variante large, si bien qu’une trace peut montrer les
deux. Et un appel natif qui apparaît sans son parent Win32, avec une adresse
de retour dans le module de l’échantillon, signifie que l’échantillon a appelé
ntdll directement : un choix délibéré qui mérite d’être noté. La prochaine
leçon Mode utilisateur, mode noyau et API native (module 5) couvre ces couches
en détail.
Là où le traçage échoue
Les traceurs dans le processus supposent que l’échantillon passe par les fonctions qu’ils ont hookées. Un malware qui s’attend à être tracé ne le fait pas :
| Évasion | Fonctionnement | Ce qui la voit encore |
|---|---|---|
| Appels système directs | L’échantillon charge le numéro d’appel système dans eax et exécute lui-même syscall, sans jamais entrer dans ntdll | Les sources côté noyau : ETW, Procmon, callbacks noyau des EDR |
| Appels système indirects | Idem, mais en sautant vers une instruction syscall située dans ntdll pour que l’adresse de retour paraisse légitime | Comme ci-dessus ; un débogueur avec un point d’arrêt sur le stub de transition vers le noyau |
| Décrochage des hooks (unhooking) | Mappe une copie neuve de ntdll depuis le disque ou \KnownDlls et copie sa .text propre par-dessus celle qui est hookée, ou restaure les prologues patchés octet par octet | Les sources côté noyau ; la comparaison du code de ntdll en mémoire avec le fichier |
| Détection des hooks | Lit les premiers octets d’une API à la recherche d’un jmp, ou cherche le module ou les threads du traceur | Les outils hors processus ; ou masquer le traceur et patcher la vérification |
| Aveuglement d’ETW en mode utilisateur | Patche ntdll!EtwEventWrite dans son propre processus | Les fournisseurs noyau, qui ne passent pas par cette fonction |
| Attachement tardif ou absent | Le travail intéressant a lieu avant que vous vous attachiez, ou dans un processus enfant que vous ne tracez pas | Traçage en mode spawn ; suivi des enfants ; sources à l’échelle du système |
Reconnaissez-les dans la trace elle-même : un échantillon dont la trace devient
silencieuse alors que Procmon le montre toujours en train d’écrire des fichiers
contourne vos hooks. Les appels système directs laissent aussi une empreinte
statique : un mov eax, <number> suivi de syscall dans le propre code de
l’échantillon, ce que les applications normales ne contiennent jamais. Les
neutraliser fait partie du module Évasion et unpacking ; pour l’instant, le
réflexe défensif consiste à associer chaque trace dans le processus à une source
côté noyau pour la même exécution.
Lab : tracer un échantillon des deux côtés
La partie A trace dbglab.exe de
Déboguer un malware avec x64dbg avec le canal
relay de Wine, qui journalise chaque appel traversant une frontière de DLL. La
partie B exécute frida-trace sur une compilation macOS native de la même
logique ; la partie C donne les équivalents pour la VM Windows. Les sorties
proviennent de Wine 11.0, Frida 17.19.0 avec frida-tools 14.10.4 (venv Python
3.14) et Apple clang 21 sous macOS 26 arm64, abrégées là où c’est indiqué. Wine
réimplémente Windows : les appels du programme sont les mêmes, mais tout ce qui
se trouve sous kernel32 est du code Wine, pas du code Microsoft.
Partie A : une trace Win32 de dbglab.exe
-
Exécutez le programme sous le canal relay et comptez ce que vous obtenez :
bash LAB_GO=1 WINEDEBUG=-all,+relay wine dbglab.exe > relay.txt 2>&1 wineserver -w # wait for Wine's helper processes to finish writing wc -l < relay.txt grep 'GetEnvironmentVariableA' relay.txt | head -1 | cut -d: -f1 grep -c '^0024:' relay.txt grep '^0024:Call' relay.txt | grep -c 'ret=14000'text 183696 0024 868 27Le fichier contient tous les processus Wine démarrés pour l’exécution, si bien que le total varie d’une exécution à l’autre. Filtrer sur le thread du programme (
0024) laisse 868 lignes ; ne garder que les appels dont l’adresse de retour se trouve dans l’image (ret=14000…) en laisse 27. La plupart relèvent du code de démarrage MinGW. La fin de la liste :text 0024:Call ucrtbase._crt_atexit(140001520) ret=1400013ad 0024:Call KERNEL32.GetEnvironmentVariableA(140004000 "LAB_GO",0031fdb8,00000008) ret=1400014fc 0024:Call KERNEL32.OutputDebugStringA(0031fdf0 "http://update.example.com/lab") ret=140002abb 0024:Call ucrtbase.puts(140004007 "sent to debugger") ret=140002ac7 0024:Call ucrtbase.exit(00000000) ret=14000140eLes trois du milieu constituent le comportement du programme ;
_crt_atexitetexitsont du code de démarrage autour demain. Les adresses de retour correspondent aux sites d’appel trouvés statiquement dans le lab de débogage :0x1400014fcsuit l’appel dans la fonction de vérification,0x140002abbl’appel avec l’URL dansmain. L’URL décodée, absente du fichier, figure en clair dans la trace. -
Regardez sous un appel. Les lignes non filtrées du thread montrent ce que chaque appel Win32 a fait dans la couche inférieure (extrait, lignes intermédiaires supprimées là où c’est indiqué) :
text 0024:Call KERNEL32.GetEnvironmentVariableA(140004000 "LAB_GO",0031fdb8,00000008) ret=1400014fc 0024:Call ntdll.RtlCreateUnicodeStringFromAsciiz(0031fcc0,140004000 "LAB_GO") ret=6fffffc17954 0024:Call ntdll.RtlQueryEnvironmentVariable_U(00000000,0031fcc0,0031fcd0) ret=6fffffc17982 ... 0024:Ret KERNEL32.GetEnvironmentVariableA() retval=00000001 ret=1400014fc 0024:Call KERNEL32.OutputDebugStringA(0031fdf0 "http://update.example.com/lab") ret=140002abb 0024:Call ntdll.RtlInitUnicodeString(0031fa10,6fffffee74b6 L"DBWinMutex") ret=6fffffc05703 ... 0024:Call ntdll.NtCreateMutant(0031fa08,00100000,0031fa20,00000000) ret=6fffffc05733 ... 0024:Call ntdll.RtlInitUnicodeString(0031fa00,6fffffee74cc L"DBWIN_BUFFER") ret=6fffffc2ed51 ... 0024:Call ntdll.NtOpenSection(0031f9f8,00000002,0031fa10) ret=6fffffc2edb1 0024:Ret ntdll.NtOpenSection() retval=c0000034 ret=6fffffc2edb1 ... 0024:Ret KERNEL32.OutputDebugStringA() retval=7ffc0000 ret=140002abb 0024:Call ucrtbase.puts(140004007 "sent to debugger") ret=140002ac7 ... 0024:Call KERNEL32.WriteFile(0000000c,0031eb50,00000012,0031eb4c,00000000) ret=6fffffa0334d 0024:Call ntdll.NtWriteFile(0000000c,00000000,00000000,00000000,0031ea40,0031eb50,6fff00000012,00000000,00000000) ret=6fffffc68908La fonction
Aconvertit d’abord sa chaîne en UTF-16.GetEnvironmentVariableAne quitte jamais le mode utilisateur, si bien qu’aucun outil au niveau des appels système ne la verrait.OutputDebugStringAouvre le mutexDBWinMutexet essaie la sectionDBWIN_BUFFERque créent les visionneuses de sortie de débogage (c0000034estSTATUS_OBJECT_NAME_NOT_FOUND: aucune visionneuse ne tournait). Et le texte deputsn’atteintNtWriteFilequ’à la sortie, quand le runtime vide ses tampons.
Partie B : frida-trace sur une compilation native
-
Enregistrez
tracelab.c, la même logique pour macOS ou Linux, avec un délai de démarrage facultatif pour vous entraîner à l’attachement :c /* tracelab.c: harmless program for API tracing — reads a setting, decodes a string, writes a file */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.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 }; /* external linkage + noinline: keeps clang from specialising or folding the decoder */ __attribute__((noinline)) 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'; } int main(void) { char url[64]; unsigned delay = getenv("LAB_DELAY") ? (unsigned)atoi(getenv("LAB_DELAY")) : 0; if (delay) sleep(delay); /* gives you time to attach */ decode(url, ENC_URL, sizeof ENC_URL, 0x5A); if (getenv("LAB_GO") == NULL) { puts("gate closed"); return 1; } FILE *f = fopen("tracelab.out", "w"); if (f) { fprintf(f, "%s\n", url); fclose(f); } puts("wrote tracelab.out"); return 0; }bash clang -O2 -o tracelab tracelab.c python3 -m venv venv && ./venv/bin/pip install frida-toolsAttention : Dans une première version,
decodeétaitstatic. Clang l’a alors spécialisée pour son unique appelant et a remplacé la boucle par des écritures constantes de l’URL en clair, si bien que la chaîne se trouvait dans le binaire, et un hook surdecodeaffichait des arguments dénués de sens (n=259, key=0x3) parce que la fonction spécialisée ne les prenait plus. Les fonctions internes n’ont pas à respecter l’ABI ; vérifiez le désassemblage avant de vous fier à un hook posé sur l’une d’elles. -
Tracez en mode spawn (
-f, avec un chemin absolu), en nommant les appels de la libc et la fonctiondecodedu programme :bash LAB_GO=1 ./venv/bin/frida-trace -f "$PWD/tracelab" -i getenv -i fopen -i puts -i 'tracelab!decode'Modifiez ensuite deux gestionnaires générés.
__handlers__/libsystem_c.dylib/getenv.jsdoit journaliser le résultat, pas seulement le nom :js defineHandler({ onEnter(log, args, state) { this.name = args[0].readUtf8String(); }, onLeave(log, retval, state) { const value = retval.isNull() ? "NULL" : `"${retval.readUtf8String()}"`; log(`getenv("${this.name}") => ${value}`); } });Et
__handlers__/tracelab/decode.jsdoit afficher le tampon de sortie une fois que la fonction l’a rempli :js defineHandler({ onEnter(log, args, state) { this.out = args[0]; // decode(out, in, n, key) log(`decode(n=${args[2].toInt32()}, key=${ptr(args[3]).and(0xff)})`); }, onLeave(log, retval, state) { log(`decode => "${this.out.readUtf8String()}"`); } }); -
Relancez avec les deux chemins. Pour l’exécution avec la vérification ouverte, nous avons aussi ajouté
-i write -i __write_nocancel, avec ce gestionnaire dans__handlers__/libsystem_kernel.dylib/__write_nocancel.js, pour attraper les octets quefclosevide dans le fichier :js defineHandler({ onEnter(log, args, state) { const n = args[2].toInt32(); log(`__write_nocancel(fd=${args[0].toInt32()}, ${JSON.stringify(args[1].readUtf8String(n))}, ${n})`); } });Vérification ouverte (lignes de démarrage retirées) :
text Started tracing 6 functions. Web UI available at http://localhost:62711/ /* TID 0x103 */ 7 ms getenv("LAB_DELAY") => NULL 7 ms decode(n=29, key=0x5a) 7 ms decode => "http://update.example.com/lab" 7 ms getenv("LAB_GO") => "1" 7 ms fopen(path="tracelab.out", mode="w") 7 ms getenv("STDBUF") => NULL 7 ms getenv("STDBUF0") => NULL 7 ms getenv("STDBUF1") => NULL 7 ms getenv("STDBUF2") => NULL 7 ms getenv("_STDBUF_I") => NULL 7 ms getenv("_STDBUF_O") => NULL 7 ms getenv("_STDBUF_E") => NULL 7 ms __write_nocancel(fd=3, "http://update.example.com/lab\n", 30) 8 ms puts(s="wrote tracelab.out") Process terminatedVérification fermée (sans
LAB_GO) :text /* TID 0x103 */ 4 ms getenv("LAB_DELAY") => NULL 4 ms decode(n=29, key=0x5a) 4 ms decode => "http://update.example.com/lab" 4 ms getenv("LAB_GO") => NULL 4 ms puts(s="gate closed") 4 ms | getenv("STDBUF") => NULL 4 ms | getenv("STDBUF0") => NULL 4 ms | getenv("STDBUF1") => NULL 4 ms | getenv("STDBUF2") => NULL 4 ms | getenv("_STDBUF_I") => NULL 4 ms | getenv("_STDBUF_O") => NULL 4 ms | getenv("_STDBUF_E") => NULL Process terminatedLes sept recherches de
STDBUFsont la libc qui configure son premier flux, pas le programme.|marque les appels faits à l’intérieur d’une autre fonction tracée (puts) ; lors de la première exécution, ils sont au niveau supérieur parce quefprintf, non tracée, a ouvert le flux. L’exécution avec la vérification fermée décode quand même l’URL avant de vérifier, si bien que hooker la fonction interne permet de récupérer l’indicateur même quand l’échantillon refuse d’agir. Une tentative de spawn a échoué avecunexpectedly timed out while initializing suspended process; une nouvelle tentative a réussi. -
Attachez-vous au lieu de lancer le processus. Démarrez le programme avec un délai, puis attachez-vous par nom :
bash LAB_GO=1 LAB_DELAY=8 ./tracelab & ./venv/bin/frida-trace -n tracelab -i getenv -i fopen -i puts -i 'tracelab!decode'text Attaching... Started tracing 4 functions. Web UI available at http://localhost:62747/ /* TID 0x103 */ 6267 ms decode(n=29, key=0x5a) 6267 ms decode => "http://update.example.com/lab" 6267 ms getenv("LAB_GO") => "1" 6267 ms fopen(path="tracelab.out", mode="w") ... 6268 ms puts(s="wrote tracelab.out") Process terminatedgetenv("LAB_DELAY")manque : il s’est exécuté avant que Frida ne s’attache. L’attachement a fonctionné parce quetracelabest notre propre binaire signé ad hoc ; un binaire système protégé refuse même le mode spawn :text Spawning `/bin/echo`... Failed to attach: unable to access process with pid 92877 from the current user accountAvec System Integrity Protection activée, les binaires de plateforme d’Apple ne peuvent pas être instrumentés ; les VM de labo Windows et Linux n’ont pas cette restriction pour les processus qui vous appartiennent.
Partie C : la même trace sous Windows
- frida-trace dans la VM. Installez
frida-toolsdans un environnement virtuel de la VM Windows, définissezLAB_GO=1dans une invite de commandes, et exécutezfrida-trace -f C:\lab\dbglab.exe -i "KERNEL32.DLL!OutputDebugStringA" -i "KERNEL32.DLL!GetEnvironmentVariableA". Qualifiez le module : de nombreux exports dekernel32ont un jumeau dansKernelBase, et un-inon qualifié hooke tous les modules qui exportent ce nom, si bien qu’un même appel peut être journalisé deux fois. Modifiez le fichier généréOutputDebugStringA.jspour journaliserargs[0].readUtf8String(). - API Monitor dans la VM. Lancez API Monitor x64, cherchez
OutputDebugStringAetGetEnvironmentVariableAdans l’API Filter et cochez-les, puis Monitor New Process surdbglab.exe. Exécutez-le une fois avecLAB_GOnon défini et une fois défini, et comparez les deux listes d’appels et leurs panneaux Call Stack avec la trace relay de la partie A. - Vérifiez depuis le côté noyau. Pendant la même exécution, faites
enregistrer
dbglab.exepar Procmon. Quels appels de la trace API Monitor produisent un événement Procmon, et lesquels n’en produisent jamais ?
Questions : Pourquoi GetEnvironmentVariableA apparaît-elle dans les traces
relay et Frida, alors qu’elle ne pourrait jamais apparaître dans une trace
strace ou ETW noyau ? Dans la partie A, quel filtre a supprimé le plus de
bruit : le thread ou l’adresse de retour, et lequel échouerait sur un échantillon
qui s’injecte dans un autre processus ? Si dbglab.exe résolvait
OutputDebugStringA par hachage d’API, lesquels des outils de ce lab
journaliseraient encore l’appel ? À quoi ressemblerait une trace de tracelab si
l’échantillon émettait son write sous forme d’appel système brut ?
À retenir
- Le traçage enregistre de nombreux appels à pleine vitesse ; le débogage examine un instant en profondeur. Surveillez, puis tracez, puis déboguez ce que la trace ne peut pas expliquer.
- Les traceurs dans le processus (API Monitor, Frida, points d’arrêt de journalisation) donnent des arguments riches mais peuvent être vus et supprimés ; les sources côté noyau (Procmon, ETW, strace, eBPF) sont plus grossières mais survivent aux astuces en mode utilisateur.
- Délimitez chaque trace par une question, filtrez selon le module de l’appelant, choisissez une couche et journalisez les valeurs de retour.
- Lisez une trace en suivant les handles, en regroupant les appels en actions et en lisant les arguments décodés ; la frontière de l’API est l’endroit où les chaînes chiffrées apparaissent en clair.
- Les appels Win32 se décomposent en appels natifs de
ntdll, et certains n’atteignent jamais le noyau. Un appel natif fait directement depuis le code de l’échantillon est une trouvaille. - Les appels système directs et indirects, le décrochage des hooks, la détection des hooks et l’attachement tardif mettent en échec le traçage dans le processus ; associez chaque trace fondée sur des hooks à un enregistrement côté noyau de la même exécution.