Leçon 11.1 · Analyse automatisée & avancée· 50 min
Instrumentation binaire dynamique
Comment Pin, DynamoRIO, Frida et TinyInst injectent du code d’analyse dans un échantillon pour tracer ses API, mesurer la couverture et voir l’unpacking.
Objectifs
- Expliquer ce que l’instrumentation observe là où un débogueur devient fastidieux, et distinguer instrumentation statique et dynamique
- Décrire comment un moteur DBI retraduit le code dans un cache de code et d’où vient son surcoût
- Choisir entre Intel Pin, DynamoRIO, Frida et TinyInst selon la question d’analyse
- Hooker des appels de bibliothèque et enregistrer les blocs de base exécutés avec Interceptor et Stalker de Frida
- Reconnaître comment un malware détecte l’instrumentation et comment le suivi « écrit puis exécuté » révèle le code dépaqueté
Un débogueur répond aux questions un arrêt à la fois. Vous posez un
point d’arrêt, le processus s’arrête, vous lisez les
registres, vous continuez. C’est parfait pour « que contient rcx au retour de
cette routine de déchiffrement ? » et pénible pour « lesquelles de ces
4 000 fonctions se sont exécutées ? », « journalise chaque appel à
CreateFileW avec ses arguments pendant toute l’exécution » ou « indique-moi
la première fois que l’exécution arrive dans de la mémoire que l’échantillon a
écrite lui-même ». Y répondre avec des points d’arrêt suppose des milliers
d’arrêts, un script pour les relier, et un échantillon qui remarque ces arrêts.
L’instrumentation binaire renverse la question. Au lieu d’arrêter le programme pour l’observer, vous insérez votre propre code d’analyse dans celui-ci, de sorte que le programme rende compte de lui-même pendant son exécution : un compteur à chaque bloc de base, un callback de journalisation devant chaque appel d’API, une vérification après chaque écriture mémoire. Cette leçon explique comment se fait cette insertion, ce qu’elle coûte, quels moteurs existent, et les trois usages les plus courants chez les analystes : journalisation des API, couverture de code et unpacking. Elle ouvre le module d’analyse automatisée ; les leçons suivantes remplacent l’exécution réelle par l’émulation, puis construisent le suivi de teinte et l’exécution symbolique par-dessus.
Ce que l’instrumentation vous apporte
Voyez l’instrumentation comme un débogueur programmable, toujours actif, qui ne se met jamais en pause. Sa portée est la même — il voit les registres, la mémoire et le flot de contrôle — mais il enregistre au lieu de s’arrêter.
| Question | Avec un débogueur | Avec l’instrumentation |
|---|---|---|
| Quel code s’est réellement exécuté ? | Un point d’arrêt sur chaque bloc, ou un pas-à-pas en mode trace qui se traîne | Enregistrer chaque bloc une fois, lors de sa traduction ; exporter l’ensemble à la sortie |
| Qu’a-t-il passé à chaque API ? | Points d’arrêt conditionnels plus un script, fonction par fonction | Un hook par export qui journalise arguments et valeurs de retour |
| Où a-t-il écrit, et a-t-il exécuté ce qu’il a écrit ? | Points d’arrêt matériels sur la mémoire, quatre à la fois | Instrumenter chaque écriture et chaque branchement indirect |
| Combien de fois cette boucle tourne-t-elle ? | Compteurs de passages, si le débogueur les gère | Un incrément de compteur compilé dans la boucle |
La contrepartie : vous devez décider à l’avance quoi enregistrer, et chaque morceau de code d’analyse inséré a un coût à l’exécution. Un débogueur convient mieux à l’exploration ouverte ; l’instrumentation convient mieux aux questions systématiques portant sur toute une exécution. En pratique, les analystes utilisent les deux : l’instrumentation pour trouver la zone intéressante, le débogueur (la leçon x64dbg du module d’analyse dynamique, à venir) pour l’examiner de près une fois sur place.
Instrumentation statique ou dynamique
Il y a deux endroits où insérer du code : dans le fichier avant son exécution, ou dans le processus pendant son exécution.
Instrumentation binaire statique
L’instrumentation binaire statique (SBI) réécrit l’exécutable sur disque. Insérer de nouvelles instructions décale toutes les adresses qui suivent, ce qui casse chaque saut, appel et pointeur faisant référence à l’ancienne disposition. Comme on ne peut pas trouver de façon fiable toutes ces références dans un binaire (le problème auquel Désassemblage linéaire vs récursif a consacré toute une leçon), les outils SBI évitent de déplacer le code sur place. Deux stratégies dominent :
- Remplacement par point d’arrêt. Écraser le premier octet de chaque
instruction instrumentée par
int3. Quand elle s’exécute, le gestionnaire d’interruption lance le code d’analyse, émule ou restaure l’instruction d’origine, puis reprend. C’est simple, mais chaque interruption est un aller-retour dans le noyau, si bien qu’instrumenter chaque bloc est d’une lenteur pénible. - Trampolines. Copier chaque fonction dans une nouvelle section,
instrumenter librement la copie, et écraser le début de l’original par un
jmpvers la copie. Les appels directs vers l’ancien code rebondissent vers le nouveau ; les sauts indirects qui atterrissent au milieu d’une fonction demandent des correctifs supplémentaires. Dyninst est l’outil le plus connu de cette famille.
Les deux dépendent d’un désassemblage suffisamment bon pour trouver le code. Face à un échantillon packé, la SBI est presque inutile : le code qui vous intéresse n’existe pas dans le fichier tant qu’il ne s’exécute pas.
Instrumentation binaire dynamique
L’instrumentation binaire dynamique (DBI) effectue la réécriture à l’exécution, sur le code sur le point de s’exécuter. Comme elle ne traite que du code réellement atteint, elle n’a jamais besoin d’un désassemblage complet, elle voit le code dépaqueté et le code auto-modifiant sous leur forme finale, et elle suit le code que l’échantillon génère à l’exécution. C’est pourquoi tous les outils de cette leçon sont dynamiques, et pourquoi c’est la DBI, et non la SBI, qui compte pour les analystes de malwares.
Fonctionnement d’un moteur DBI
L’idée centrale de Pin, DynamoRIO et Stalker (Frida) est la même : le code d’origine ne s’exécute jamais. Le moteur en exécute des copies instrumentées à la place.
- Le moteur prend la main avant la première instruction du programme (en lançant lui-même le processus, ou en s’y injectant).
- Un dispatcher prend l’adresse sur le point de s’exécuter et se demande : existe-t-il dans le cache de code une copie traduite du bloc qui commence ici ?
- Sinon, un compilateur JIT décode les instructions d’origine jusqu’au prochain transfert de contrôle, y entrelace les callbacks d’analyse demandés par l’utilisateur, réécrit le branchement final pour qu’il revienne au dispatcher, et stocke le résultat dans le cache.
- Le bloc traduit s’exécute nativement sur le processeur. À sa fin, le contrôle revient au dispatcher avec l’adresse d’origine suivante, et le cycle recommence.
Deux optimisations maintiennent la vitesse. Une fois les deux extrémités d’un
branchement direct traduites, le moteur modifie la copie en cache pour
qu’elle saute directement vers la copie en cache de la cible, si bien que les
boucles chaudes ne touchent plus du tout le dispatcher. Les branchements
indirects (ret, jmp rax, appels via une table) ne peuvent pas être
chaînés ainsi ; leur cible n’est connue qu’à l’exécution, donc chacun passe par
une recherche rapide de l’adresse d’origine vers l’adresse en cache.
Le moteur travaille aussi beaucoup sa transparence. Le programme doit
croire qu’il exécute son propre code à ses propres adresses : un
call doit empiler l’adresse de retour d’origine,
la lecture des octets de code doit renvoyer les octets d’origine, et les
exceptions doivent sembler provenir des adresses d’origine. Chaque faille dans
cette illusion est une occasion de détection, comme vous le verrez plus bas.
Ce que cela coûte
| Coût | D’où il vient |
|---|---|
| Démarrage | Traduction du code lors de sa première exécution ; pire pour les gros programmes à courte durée de vie |
| Régime établi | Recherches du dispatcher sur les branchements indirects, plus le code d’analyse que vous avez inséré |
| Code d’analyse | Domine généralement : un callback sur chaque instruction ou chaque accès mémoire multiplie de nombreuses fois le temps d’exécution |
| Mémoire | Le cache de code et l’état propre du moteur résident dans l’espace d’adressage de la cible |
Un moteur sans analyse attachée n’est que modérément plus lent que l’exécution native. Un outil qui déclenche un appel à chaque accès mémoire peut être d’un ou deux ordres de grandeur plus lent. La règle pratique : instrumentez à la granularité la plus grossière qui répond à votre question (la routine plutôt que le bloc, le bloc plutôt que l’instruction), et ne faites le travail lourd dans le callback que lorsqu’un filtre peu coûteux indique que c’est pertinent.
Les moteurs utilisés par les analystes
| Moteur | Plateformes | Vous écrivez | Points forts | Usage typique en analyse |
|---|---|---|---|---|
| Intel Pin | Windows, Linux (x86, x64) | Des « pintools » en C++ | Mature, hooks fins au niveau instruction, bloc, trace, routine et image | Traçage d’API avec tiny_tracer, unpackers sur mesure, outils de recherche |
| DynamoRIO | Windows, Linux, macOS (partiel), x86/x64/ARM | Des clients en C | Open source, rapide, riche bibliothèque cliente ; fourni avec drcov et drltrace | Couverture (drcov) chargée dans IDA/Binary Ninja via Lighthouse ou dans Ghidra via Cartographer ; journaux d’appels de bibliothèque |
| Frida | Windows, macOS, Linux, iOS, Android | Du JavaScript (ou du C) dans un agent injecté | Le plus rapide pour itérer ; Interceptor pour hooker des fonctions, Stalker pour le traçage DBI ; s’attache à des processus en cours | Journalisation rapide des API, modification d’arguments, échantillons mobiles et macOS |
| TinyInst | Windows, macOS, Linux | Du C++ | N’instrumente que les modules que vous nommez, le reste s’exécute à vitesse native | Couverture d’un module dans un gros processus ; fuzzing avec Jackalope |
Quelques remarques pour choisir :
- Frida réunit deux outils en un.
Interceptorn’est pas du tout de la DBI : il modifie les premières instructions d’une fonction en y plaçant un saut vers un trampoline, un hook d’API inline posé avec une précision chirurgicale.Stalkerest le véritable moteur DBI, qui recompile dans un cache les blocs d’un thread suivi. La plupart des tâches rapides n’ont besoin que d’Interceptor; passez àStalkerquand la question porte sur le flot de contrôle plutôt que sur les appels. - Pin et DynamoRIO lancent le programme sous leur contrôle dès la première instruction, ce qu’il vous faut pour un packer ou un loader Windows qui fait son travail intéressant avant qu’un attachement ait pu avoir lieu. tiny_tracer, un outil Pin écrit pour l’analyse de malwares, journalise les appels d’API et les transitions entre modules dans un fichier texte, et propose des options pour neutraliser les vérifications anti-analyse courantes.
- TinyInst repose sur un principe différent des autres : il exécute le processus sous une interface de débogage, marque le module cible comme non exécutable, et redirige l’exécution vers une copie réécrite quand les exceptions qui en résultent se déclenchent. Tout ce qui est hors du module nommé s’exécute sans modification, ce qui limite le surcoût quand seule une DLL vous intéresse.
Quel que soit votre choix, exécutez-le dans votre VM d’analyse isolée. L’instrumentation observe une exécution réelle ; l’échantillon fait tout ce qu’il ferait autrement.
Usages en analyse
Journalisation des appels d’API
L’usage le plus courant. Hookez les exports que l’échantillon importe (ou,
pour un échantillon qui résout ses API par hachage,
tous les exports des DLL concernées) et journalisez les arguments à l’entrée et
les résultats à la sortie. Par rapport à un moniteur à l’échelle du système,
vous obtenez les arguments et valeurs de retour exacts tels que les voit le
propre thread de l’échantillon, y compris les chaînes déchiffrées passées à
InternetConnectW ou les chemins de registre passés à RegSetValueExW. La
leçon sur le traçage d’API du module d’analyse dynamique comparera cette
approche au traçage au niveau du noyau.
Couverture de code : quel code s’est réellement exécuté
Enregistrez chaque bloc exécuté dans le module propre de l’échantillon, puis
reportez cet ensemble sur le désassemblage. drcov de DynamoRIO écrit un
journal des décalages de blocs relatifs au module, que le plugin Lighthouse met
en surbrillance dans IDA ou Binary Ninja. La couverture répond à des questions
autrement lentes à traiter :
- Quelles fonctions valent la peine d’être lues, et lesquelles sont mortes ou dormantes lors de cette exécution ?
- Quelle branche d’une vérification a été prise ? Comparez la couverture de deux exécutions (avec et sans débogueur, artefact de VM, argument de ligne de commande) et les blocs qui diffèrent pointent directement vers la décision.
- Quelles commandes d’un gestionnaire C2 ont été exercées par le trafic que vous avez rejoué ?
La couverture vaut pour une exécution. Un bloc absent de l’ensemble n’a pas été exécuté cette fois-ci ; ce n’est pas la preuve que le code est inatteignable.
Écrit puis exécuté : attraper le code dépaqueté
Un packer écrit la vraie charge utile en mémoire puis y saute. Un outil d’instrumentation peut surveiller les deux moitiés de cette phrase :
- Instrumenter chaque écriture mémoire et enregistrer les adresses de destination (en pratique, les pages) où l’échantillon écrit.
- Instrumenter chaque transfert de contrôle, ou simplement noter chaque bloc que le moteur traduit, et vérifier si son adresse se trouve dans une zone écrite.
- La première fois que l’exécution entre dans de la mémoire écrite, vous tenez un candidat point d’entrée original. Dumpez la zone (et l’image du processus) à ce moment-là.
Comme un moteur DBI doit retraduire tout bloc dont le code a changé, il détecte déjà les écritures sur du code qu’il a mis en cache ; un unpacker étend cette comptabilité à toute la mémoire. L’approche gère des packers que vous n’avez jamais vus, et les packers multicouches la déclenchent simplement plusieurs fois. Les faux positifs sont les suspects habituels : compilateurs JIT, thunks générés à l’exécution et code auto-modifiant légitime. La leçon d’unpacking manuel du module évasion et unpacking montre la même idée réalisée à la main avec les permissions de pages et des points d’arrêt ; ici, elle est automatisée.
Comment un malware détecte l’instrumentation
La DBI est transparente par conception, pas par garantie. Les échantillons qui la recherchent examinent :
- Le temps. La traduction et les callbacks ralentissent le code, donc les
écarts de
rdtscou deGetTickCountautour d’une boucle augmentent. Une instrumentation lourde échoue aux vérifications de timing plus souvent qu’un débogueur. - Les modules et threads étrangers. Pin, DynamoRIO et Frida vivent tous à l’intérieur de la cible. Leurs DLL ou objets partagés, leurs threads supplémentaires (l’agent de Frida exécute les siens) et les zones mémoire contenant le cache de code apparaissent quand un échantillon énumère les modules, les threads ou la mémoire exécutable.
- Les prologues modifiés. Les hooks inline comme
Interceptorde Frida réécrivent les premiers octets de la fonction hookée. Un échantillon qui compare le début deNtReadVirtualMemoryavec la copie sur disque, ou qui cherche unjmpà l’entrée d’une API, les voit. La DBI pure ne modifie pas les octets d’origine : cette vérification attrape donc les hooks plutôt que les traceurs. - L’émulation imparfaite des cas limites. Le code auto-modifiant sur la même page, des flux d’exceptions inhabituels et la lecture de l’adresse de retour sur la pile avec du code non standard ont tous servi, à diverses époques, à démasquer des moteurs DBI.
Quand un échantillon se comporte différemment sous instrumentation et sur une VM nue, traitez-le comme n’importe quelle autre branche anti-débogage : comparez la couverture des deux exécutions pour trouver la vérification, puis patchez-la ou hookez-la.
Lab : hooker la libc et tracer les blocs de base avec Frida
Vous allez construire un programme C inoffensif qui lit une variable d’environnement et un petit fichier de configuration puis prend l’un de deux chemins, et utiliser Frida pour journaliser les appels de bibliothèque et enregistrer quels blocs de base se sont exécutés sur chaque chemin. Les sorties ci-dessous sont réelles, obtenues avec Frida 17.19.0 et frida-tools 14.10.4 dans un environnement virtuel Python 3.14 sous macOS 26 (Apple silicon, arm64) avec System Integrity Protection activée ; le programme a été compilé avec l’Apple clang 21 du système. Rien n’est installé à l’échelle du système.
-
Créez un répertoire de travail et un environnement virtuel :
bash mkdir m11a && cd m11a python3 -m venv venv ./venv/bin/pip install frida-tools -
Enregistrez le programme cible sous
labprog.c. Il vérifieLAB_USER, litlab.cfg, et appelleactive_pathouquiet_pathselon le contenu du fichier :c /* labprog.c - a harmless program with one environment check and one file check */ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> __attribute__((noinline)) static int config_says_active(const char *path) { char buf[32] = {0}; int fd = open(path, O_RDONLY); if (fd < 0) return 0; read(fd, buf, sizeof buf - 1); close(fd); return strncmp(buf, "mode=active", 11) == 0; } __attribute__((noinline)) static void quiet_path(void) { puts("nothing to do"); } __attribute__((noinline)) static void active_path(const char *who) { printf("active mode for %s\n", who); } int main(void) { const char *who = getenv("LAB_USER"); if (who == NULL) who = "nobody"; if (config_says_active("lab.cfg")) active_path(who); else quiet_path(); return 0; }bash clang -O1 -g -o labprog labprog.c echo "mode=quiet" > lab.cfgclangsigne le résultat de façon ad hoc. Cela compte sous macOS : avec SIP activée, Frida a pu lancer (spawn) sans root ce binaire que nous avons compilé nous-mêmes, mais les binaires de la plateforme Apple et les applications compilées avec le hardened runtime sont protégés, et les instrumenter exige d’assouplir SIP sur une machine de test dédiée. Sur les VM de laboratoire Windows et Linux, cette restriction ne s’applique pas. -
Commencez par
frida-trace, qui génère un gestionnaire de journalisation pour chaque fonction que vous nommez. Passez un chemin absolu : avec un chemin relatif, Frida 17 a cherché un identifiant d’application et a échoué.bash ./venv/bin/frida-trace -f "$PWD/labprog" -i getenv -i opentext getenv: Auto-generated handler at ".../__handlers__/libsystem_c.dylib/getenv.js" open: Auto-generated handler at ".../__handlers__/libsystem_kernel.dylib/open.js" Started tracing 2 functions. Web UI available at http://localhost:58207/ nothing to do /* TID 0x103 */ 215 ms getenv(name="LAB_USER") 215 ms open(path="lab.cfg", oflag=0x0, ...) 215 ms getenv(name="STDBUF") 215 ms getenv(name="STDBUF0") ... 215 ms getenv(name="_STDBUF_E") Process terminatedDeux leçons en une seule trace. D’abord, les deux appels que fait le programme sont là avec leurs arguments, y compris le fichier qu’il a ouvert. Ensuite, la plupart des appels à
getenvne proviennent pas du tout delabprog.c: ils viennent de la bibliothèque C qui configure la mise en mémoire tampon destdoutlors du premierputs. La trace d’un vrai échantillon contient le même bruit issu de son runtime, c’est pourquoi de bons hooks filtrent selon l’appelant ou l’argument. -
Enregistrez
hook.js. Il hookegetenvetopenavecInterceptor, en journalisant les valeurs de retour en plus des arguments, et quand on entre dansmain, il demande àStalkerde suivre le thread, en enregistrant le début de chaque bloc qui appartient àlabproglui-même :js // hook.js - log two libc calls and record which basic blocks of main ran const app = Process.mainModule; function libc(name) { return Module.getGlobalExportByName(name); } Interceptor.attach(libc('getenv'), { onEnter(args) { this.name = args[0].readUtf8String(); }, onLeave(ret) { if (this.name.startsWith('LAB_')) console.log(`getenv("${this.name}") -> ${ret.isNull() ? 'NULL' : '"' + ret.readUtf8String() + '"'}`); } }); Interceptor.attach(libc('open'), { onEnter(args) { this.path = args[0].readUtf8String(); }, onLeave(ret) { console.log(`open("${this.path}") -> fd ${ret.toInt32()}`); } }); const blocks = new Set(); const mainFn = app.getExportByName('main'); Interceptor.attach(mainFn, { onEnter() { Stalker.follow(this.threadId, { transform(iterator) { let insn = iterator.next(); const start = insn.address; if (start.compare(app.base) >= 0 && start.compare(app.base.add(app.size)) < 0) blocks.add(start.sub(app.base).toInt32()); do { iterator.keep(); } while ((insn = iterator.next()) !== null); } }); }, onLeave() { Stalker.unfollow(this.threadId); // label each block with the function (or stub section) that contains it const stubs = app.enumerateSections().find(s => s.name === '__stubs'); const stubOff = stubs.address.sub(app.base).toInt32(); const funcs = app.enumerateSymbols() .filter(s => s.type === 'section' && s.name !== '') .map(s => ({ off: s.address.sub(app.base).toInt32(), name: s.name })) .filter(f => f.off > 0 && f.off < stubOff) .sort((a, b) => a.off - b.off); const byFunc = {}; for (const off of [...blocks].sort((a, b) => a - b)) { let name = off >= stubOff ? '(__stubs: calls into libc)' : '?'; if (off < stubOff) for (const f of funcs) if (f.off <= off) name = f.name; (byFunc[name] = byFunc[name] || []).push('0x' + off.toString(16)); } console.log(`\n${blocks.size} unique basic blocks of ${app.name} executed:`); for (const [name, offs] of Object.entries(byFunc)) console.log(` ${name.padEnd(28)} ${offs.join(' ')}`); } });Le callback
transformest l’étape JIT vue plus haut, mise à votre disposition : Stalker l’appelle une fois par bloc lorsqu’il compile ce bloc dans son cache, etiterator.keep()copie chaque instruction d’origine dans la traduction. Comme la traduction a lieu une fois par bloc, y enregistrer l’adresse de début donne l’ensemble des blocs uniques exécutés pour un coût quasi nul. Ajouter un callout par bloc (iterator.putCallout) donnerait le nombre d’exécutions, pour un prix plus élevé. -
Enregistrez un petit pilote,
run_hook.py. La ligne de commandefrida -q -f ... -l hook.jslançait le programme et affichait sa sortie, mais le processus se terminait avant l’arrivée des messages de l’agent et le journal revenait vide ; attendre l’événementdetachedde la session règle le problème :python # run_hook.py - spawn labprog suspended, load hook.js, resume, wait for exit import os, threading, frida done = threading.Event() device = frida.get_local_device() pid = device.spawn([os.path.abspath("labprog")], env={**os.environ}) session = device.attach(pid) session.on("detached", lambda reason, crash: (print(f"[detached: {reason}]"), done.set())) script = session.create_script(open("hook.js").read()) script.on("message", lambda msg, data: print(msg)) script.load() device.resume(pid) done.wait(20)Lancer le processus suspendu, charger le script, puis reprendre l’exécution : c’est aussi le schéma à utiliser pour les malwares, car les hooks sont en place avant la première instruction de l’échantillon.
-
Exécutez-le une fois sur chaque chemin :
bash echo "mode=quiet" > lab.cfg ./venv/bin/python run_hook.py echo "mode=active" > lab.cfg LAB_USER=analyst ./venv/bin/python run_hook.pytext nothing to do getenv("LAB_USER") -> NULL open("lab.cfg") -> fd 3 18 unique basic blocks of labprog executed: main 0x470 0x478 0x480 0x49c 0x4a0 config_says_active 0x4b0 0x4e8 0x4ec 0x4fc 0x504 0x540 0x558 quiet_path 0x594 (__stubs: calls into libc) 0x5ac 0x5b8 0x5c4 0x5dc 0x5e8 [detached: process-terminated]text active mode for analyst getenv("LAB_USER") -> "analyst" open("lab.cfg") -> fd 3 20 unique basic blocks of labprog executed: main 0x470 0x478 0x480 0x484 0x498 0x4a0 config_says_active 0x4b0 0x4e8 0x4ec 0x4fc 0x504 0x540 0x558 active_path 0x56c 0x588 (__stubs: calls into libc) 0x5ac 0x5b8 0x5c4 0x5d0 0x5e8 [detached: process-terminated]La sortie du programme lui-même arrive en premier parce que Frida transmet les messages de l’agent de façon asynchrone. Les lignes d’
Interceptorvous donnent la valeur de l’environnement et le descripteur de fichier, qu’une lecture statique du binaire ne pourrait que deviner. -
Comparez les deux ensembles de blocs.
config_says_activea exécuté les mêmes sept blocs les deux fois, donc la décision n’est pas à l’intérieur. Dansmain, l’exécution « quiet » a visité0x49ctandis que l’exécution « active » a visité0x484et0x498à la place ; un stub a aussi changé (0x5dccontre0x5d0, les stubs deputset deprintf). Vérifiez avec le désassemblage :bash objdump -d --no-show-raw-insn labprog | awk '/^0000000100000460/,/^00000001000004b0/'text 100000474: bl 0x1000005b8 <_read+0x1000005b8> 100000478: mov x19, x0 10000047c: bl 0x1000004b0 <_config_says_active> 100000480: cbz w0, 0x10000049c <_main+0x3c> 100000484: adrp x8, 0x100000000 <_read+0x100000000> ... 100000494: bl 0x10000056c <_active_path> 100000498: b 0x1000004a0 <_main+0x40> 10000049c: bl 0x100000594 <_quiet_path> 1000004a0: mov w0, #0x0 ; =0Le branchement est le
cbz w0en0x480, qui teste la valeur de retour deconfig_says_active. (objdumpétiquette bizarrement les appels de stubs en_read+...;0x5b8est le stub degetenv,0x5d0celui deprintfet0x5dccelui deputs.) Deux détails méritent l’attention. Stalker termine un bloc à chaquebl(appel), ses blocs sont donc plus petits que ceux qu’un désassembleur dessine dans un CFG. Et le premier bloc demain, en0x460, n’apparaît jamais :Interceptora écrasé ces premières instructions par son propre saut, et leurs copies relocalisées se sont exécutées depuis le trampoline de Frida avant que Stalker ne prenne le relais. Vos outils d’observation modifient ce qu’ils observent.
Questions : Quelle instruction unique décide entre les deux chemins, et
quelles deux entrées de couverture le prouvent ? Pourquoi la plupart des appels
à getenv dans la sortie de frida-trace ne viennent-ils pas de labprog.c,
et comment les filtreriez-vous dans hook.js ? Si un échantillon vérifiait
les premiers octets de getenv à la recherche d’une instruction de
branchement, lequel des deux mécanismes Frida utilisés ici détecterait-il, et
lequel manquerait-il ? Comment étendriez-vous hook.js pour signaler le
premier bloc exécuté depuis de la mémoire que le programme a écrite ?
À retenir
- L’instrumentation insère du code d’analyse dans le programme pour qu’il rende compte de lui-même ; elle répond aux questions portant sur toute l’exécution (chaque appel d’API, chaque bloc, chaque écriture) qu’un débogueur à arrêts rend fastidieuses.
- L’instrumentation statique réécrit le fichier avec des interruptions
int3ou des trampolines et exige un désassemblage complet ; l’instrumentation dynamique traduit le code au fil de l’exécution, et gère donc le code packé, généré et auto-modifiant. - Les moteurs DBI exécutent des copies traduites depuis un cache de code, chaînent les branchements directs, recherchent les indirects et préservent les adresses d’origine pour la transparence ; le code d’analyse que vous insérez domine généralement le surcoût.
- Pin et DynamoRIO conviennent au traçage Windows et Linux dès le lancement (tiny_tracer, drcov), Frida au hooking scripté rapide multiplateforme, et TinyInst à la couverture à faible surcoût d’un seul module.
- Les usages centraux pour l’analyste sont la journalisation des API, les comparaisons de couverture pour trouver quelle branche s’est exécutée, et le suivi « écrit puis exécuté » pour attraper le code dépaqueté.
- Un malware détecte l’instrumentation par le timing, les modules et threads étrangers, les prologues d’API modifiés et le comportement dans les cas limites ; comparez la couverture entre exécutions instrumentée et nue pour trouver la vérification.