Leçon 11.3 · Analyse automatisée & avancée· 50 min
Analyse de teinte dynamique
Comment le suivi de teinte suit les données des sources aux puits, quels choix causent sur- et sous-teinte, et comment l’employer face aux malwares.
Objectifs
- Définir sources de teinte, puits et politiques de propagation, et formuler une question d’analyse comme une requête de la source au puits
- Expliquer la granularité de la teinte, les couleurs de teinte et la mémoire fantôme, et ce que coûte chacune
- Reconnaître les causes de sur-teinte et de sous-teinte, y compris les flux implicites passant par des branchements
- Choisir entre libdft, Triton et PANDA pour une question de teinte portant sur un échantillon
- Construire un traceur de teinte à l’octet sur une routine x86-64 émulée et observer où il manque un flux
Certaines questions sur un échantillon ne portent pas sur ce que fait le code
mais sur où vont les données. Quelque chose de la réponse du C2 finit-il dans
la commande passée à CreateProcessW ? Quels octets de la configuration
déchiffrée deviennent le nom d’hôte du prochain connect ? Parmi les 400 octets
que l’échantillon lit dans un fichier, lesquels décident s’il va ensuite
s’installer ? Vous pouvez répondre à chacune de ces questions à la main avec
les références croisées et le raisonnement sur le flot de
données dans un désassembleur, mais
dès que les données passent par une boucle de déchiffrement, quelques tampons
et deux ou trois fonctions utilitaires, cela prend un après-midi.
L’analyse de teinte dynamique (DTA, aussi appelée suivi de flot de données) automatise exactement cela. Vous marquez certaines données comme teintées lorsqu’elles entrent dans le programme, vous laissez le programme s’exécuter, et l’analyse propage la marque à travers chaque copie et chaque calcul. Quand des données teintées atteignent un endroit qui vous intéresse, vous recevez une alerte, souvent accompagnée de la liste des octets d’entrée dont elles proviennent. Cette leçon couvre le modèle, les choix de conception qui rendent un traceur de teinte précis ou inutile, les outils existants, et un petit traceur que vous construirez et mettrez en défaut vous-même.
La question à laquelle répond la teinte
Toute analyse de teinte est une requête d’une seule forme : les données de cette source atteignent-elles ce puits, et par quels octets ? Elle comporte trois parties.
| Partie | Signification | Exemples en analyse de malwares |
|---|---|---|
| Source | Là où la teinte est introduite | Octets renvoyés par recv, InternetReadFile ou ReadFile ; un tampon déchiffré ; le résultat de RegQueryValueExW ; les arguments de ligne de commande |
| Puits (sink) | Là où l’on vérifie la teinte | Arguments de send, WriteFile, CreateProcessW, connect ; la cible d’un call/jmp indirect ; la condition d’un branchement |
| Politique de propagation | Comment la teinte se déplace à travers chaque instruction | Une copie transporte la teinte ; un résultat arithmétique porte l’union des teintes de ses entrées ; une constante l’efface |
Les sources et les puits sont généralement posés avec des hooks sur les appels
d’API, la même technique que dans Instrumentation binaire
dynamique :
quand recv retourne, on marque son tampon de sortie ; quand on entre dans
send, on inspecte son tampon d’entrée. La propagation est la partie
difficile, car elle doit avoir lieu à chaque instruction exécutée par le
programme. C’est pourquoi les traceurs de teinte utilisables en pratique sont
construits sur un moteur d’instrumentation dynamique, un émulateur, ou un
émulateur de système complet : quelque chose doit voir chaque instruction et
mettre à jour l’état de teinte en parallèle.
Propagation, instruction par instruction
Pour chaque instruction, le traceur détermine quels opérandes sont lus et lesquels sont écrits, puis applique une règle. Une politique typique pour x86-64 :
| Forme d’instruction | Règle |
|---|---|
mov, movzx, push, pop | Teinte de la destination := teinte de la source |
add, sub, and, or, xor | Teinte de la destination := teinte de la destination ∪ teinte de la source |
xor reg, reg, sub reg, reg | Teinte de la destination := propre (le résultat vaut toujours zéro) |
| Chargement ou stockage d’une valeur immédiate | Teinte de la destination := propre |
cmp, test | Aucune donnée écrite ; éventuellement, noter que les flags sont teintés |
lea, et chargements via un pointeur teinté | Choix de politique : voir la teinte d’adresse plus bas |
L’idiome de mise à zéro est le piège classique. Traiter xor eax, eax comme un
xor ordinaire laisse eax teinté pour toujours, et chaque utilisation
ultérieure propage la teinte encore plus loin.
Choix de conception : granularité, couleurs, mémoire fantôme
Granularité
La granularité est la plus petite unité qui porte sa propre teinte. Le suivi au
bit est le plus précis et le plus coûteux ; le suivi au mot est bon marché et
approximatif (un octet teinté dans un mot de 4 octets teinte les quatre). La
plupart des systèmes utilisables en pratique, libdft compris, suivent des
octets : ils correspondent à l’unité adressable du x86 et gardent les
masques et les écritures partielles de registres raisonnablement précis. Les
registres sont alors eux aussi suivis octet par octet, de sorte que
mov al, [x] ne teinte que l’octet de poids faible de rax.
Couleurs
Un seul bit de teinte par octet indique seulement « teinté ou non ». Cela suffit
pour « quelque chose venant du réseau atteint-il CreateProcessW ? » mais pas
pour « quels octets du réseau ? ». Les couleurs distinguent les sources.
Avec un masque de huit couleurs, vous pouvez donner à chaque source son propre
bit (0x01, 0x02, 0x04...), et quand deux flux se rencontrent dans un
add, le résultat est simplement le OU bit à bit de leurs couleurs, si bien que
des données mélangées conservent toutes leurs origines. Certains outils vont
plus loin et stockent un ensemble complet d’étiquettes par octet (c’est ce que
fait taint2 de PANDA), ce qui permet de donner à chaque octet d’entrée sa
propre étiquette, au prix d’un coût important en mémoire et en vitesse.
Mémoire fantôme
L’état de teinte réside dans une mémoire fantôme (shadow memory) : une structure parallèle avec une entrée de teinte par octet de la mémoire du programme, plus un jeu de registres fantômes (libdft l’appelle un processeur virtuel). Sa disposition découle des deux choix ci-dessus :
| Disposition | Coût fantôme par octet du programme | Couleurs |
|---|---|---|
| Bitmap | 1 bit | 1 |
| Un octet par octet | 1 octet | 8 (un bit chacune) |
| Un mot de 32 bits par octet | 4 octets | 32 |
| Ensemble ou table d’étiquettes | Variable | Illimitées |
Les implémentations réelles allouent les pages fantômes paresseusement, uniquement pour la mémoire que le programme utilise vraiment, et font pointer les zones non touchées vers une page de zéros partagée. Même ainsi, la mémoire fantôme et la comptabilité à chaque instruction expliquent pourquoi la DTA ralentit couramment un programme d’un ordre de grandeur ou plus.
Quand la teinte ment : sur-teinte et sous-teinte
Un traceur de teinte est une approximation, et il échoue dans deux directions.
La sur-teinte (over-tainting) marque des données comme teintées alors qu’elles ne sont pas réellement influencées par la source. Causes : granularité grossière, idiomes de mise à zéro non pris en compte, suivi des registres entiers, et teinte d’adresse agressive (voir plus bas). La sur-teinte se propage : dès que le pointeur de pile ou une variable globale fréquemment utilisée est teinté, presque tout l’est, et le résultat n’est que du bruit.
La sous-teinte (under-tainting) manque une influence réelle. Causes : instructions non modélisées (SIMD, instructions de chaînes, appels système qui copient des données à l’intérieur du noyau), données qui quittent le processus puis y reviennent (écrites dans un fichier, puis relues), et les deux cas particuliers qui suivent.
Teinte d’adresse
Quand une valeur teintée sert d’index, comme dans
movzx eax, byte ptr [table + rcx] avec rcx teinté, la valeur chargée
est-elle teintée ? Les octets de
table ne le sont pas, mais quel octet vous obtenez dépend entièrement de
l’entrée. Les décodeurs Base64, les tables de conversion de casse et les
S-box cryptographiques fonctionnent tous ainsi. Ignorez la teinte d’adresse et
une commande C2 décodée en Base64 paraît propre ; propagez-la et chaque accès à
un tableau avec un index teinté propage la teinte, souvent trop loin. La
plupart des outils en font une option ; sachez dans quel sens la vôtre est
réglée.
Flux implicites
Considérez :
status = 0;
if (net[0] == 1)
status = 1;status finit égal à une fonction de net[0], mais aucune instruction ne le
copie ni ne le calcule jamais à partir de net[0]. Le seul lien est un
branchement conditionnel, une dépendance de contrôle. La DTA standard ne
suit que le flot de données explicite, donc status reste propre : un flux
implicite est perdu. Propager la teinte à travers chaque branchement vers tout
ce qui est écrit sous son contrôle corrige cela en théorie et inonde le
programme de teinte en pratique, c’est pourquoi presque aucun traceur de
production ne le fait. Beaucoup d’outils signalent plutôt les branchements
teintés : un saut conditionnel dont les flags proviennent de données teintées.
Cela ne teinte pas status, mais vous indique exactement où l’entrée oriente
l’exécution, ce qui est souvent la question que vous vous posiez de toute façon.
Le lab montre les deux comportements.
Un malware n’a pas besoin de savoir tout cela pour mettre en échec un traceur
naïf ; un décodeur à table de correspondance ou une reconstruction d’une chaîne
octet par octet à coups de if/switch suffit. Quand un flux que vous
attendiez disparaît, soupçonnez d’abord ces deux cas.
Outils
| Outil | Construit sur | Modèle | Points forts | Limites |
|---|---|---|---|---|
| libdft | Intel Pin | Granularité à l’octet, 1 bit ou 8 couleurs, mémoire fantôme et processeur virtuel | Rapide pour la DTA ; vous écrivez un outil Pin qui hooke les appels système comme sources et puits | L’original est Linux 32 bits ; des portages 64 bits existent (libdft64) ; faible couverture SIMD |
| Triton | Le traceur de votre choix ou sa propre émulation | Teinte par registre et par octet aux côtés d’un moteur symbolique, depuis Python ou C++ | Combiner teinte et exécution symbolique : la teinte dit quels octets comptent, le symbolique dit quelles valeurs | Vous pilotez vous-même l’exécution, une instruction à la fois |
| PANDA | Émulateur de système complet QEMU avec enregistrement et rejeu | Plugin taint2 avec ensembles d’étiquettes ; plugins file_taint, tainted_branch, tainted_instr | Système complet : la teinte suit les données à travers le noyau et dans d’autres processus ; on enregistre une fois, on rejoue plus tard avec la teinte | Très lent avec la teinte ; mise en place lourde |
Le schéma pratique avec PANDA mérite d’être énoncé : enregistrez une fois l’exécution de l’échantillon dans un invité, à vitesse quasi normale, puis rejouez l’enregistrement autant de fois que vous le souhaitez avec la teinte activée. Le ralentissement n’affecte plus les vérifications de timing de l’échantillon, puisque celui-ci ne s’exécute plus en direct. Le lab ci-dessous réutilise la configuration Unicorn d’Analyse de shellcode, et la leçon à venir sur l’exécution symbolique reprend là où s’arrête la teinte.
Cas d’usage pour l’analyste
- Quels octets d’entrée contrôlent un branchement ? Teintez chaque octet d’un fichier, d’une réponse réseau ou d’une ligne de commande avec sa propre couleur et signalez les branchements teintés. Pour un échantillon qui analyse une commande C2, vous obtenez les offsets de l’octet d’opcode, des champs de longueur et de toute valeur magique qu’il vérifie, sans lire le parseur.
- Où va une configuration déchiffrée ? Placez la source sur le tampon de sortie de la routine de déchiffrement (après une boucle XOR, par exemple) et les puits sur les API de réseau, de fichiers et de processus. Les alertes vous disent quel champ est l’hôte C2, lequel est le port et lequel est l’identifiant de campagne, ce qui représente l’essentiel du travail d’un extracteur de configuration (l’extraction de configuration a sa propre leçon dans le module encodage et cryptographie).
- Que contient un tampon exfiltré ? Teintez les résultats des lectures de
fichiers, des requêtes au registre et des API d’identifiants avec des
couleurs différentes, et inspectez les couleurs de chaque octet arrivant à
send. Un tampon dont les octets portent la couleur « fichier de profil du navigateur » répond directement à la question des capacités. - Une entrée de l’attaquant atteint-elle quelque chose de dangereux ? La même machinerie, pointée sur un service vulnérable au lieu d’un échantillon, vérifie si des données réseau atteignent la cible d’un appel indirect ou une chaîne de commande.
Lab : construire un traceur de teinte à l’octet
Vous allez écrire une courte routine x86-64 qui copie un « tampon réseau » vers un « tampon d’envoi » avec une transformation, une somme de contrôle et un octet de statut, puis l’émuler avec Unicorn pendant qu’un traceur Python tient un état fantôme pour chaque registre et chaque octet de mémoire. Chaque octet réseau reçoit sa propre couleur. Les sorties ci-dessous sont réelles, obtenues avec Unicorn 2.1.4, Capstone 5.0.9 et pyelftools 0.33 dans un environnement virtuel Python 3.14 sous macOS (Apple silicon). La routine ne fait que déplacer des octets entre des tampons à l’intérieur de l’émulateur ; rien n’est envoyé nulle part.
-
Créez un répertoire de travail et un environnement virtuel :
bash mkdir m11b && cd m11b python3 -m venv venv ./venv/bin/pip install unicorn capstone pyelftools -
Enregistrez la routine sous
build_packet.set assemblez-la. L’assembleur intégré de Clang peut cibler x86-64 Linux depuis n’importe quel hôte, donc aucune chaîne de compilation croisée n’est nécessaire ; vous n’utilisez que les octets de.textdu fichier objet :asm # build_packet.s - copy a "network" buffer into a "send" buffer, transformed # System V: rdi = send buffer, rsi = network buffer, rdx = length .intel_syntax noprefix .text .globl build_packet build_packet: mov byte ptr [rdi], 0x42 # header byte: a constant xor ecx, ecx # i = 0 xor r8d, r8d # checksum = 0 next: movzx eax, byte ptr [rsi + rcx] # b = net[i] xor al, 0x5a # b ^= 0x5a add al, cl # b += i mov byte ptr [rdi + rcx + 1], al # send[1 + i] = b xor r8b, al # checksum ^= b inc rcx cmp rcx, rdx jb next mov byte ptr [rdi + rdx + 1], r8b # send[1 + len] = checksum xor eax, eax # status = 0 cmp byte ptr [rsi], 0x01 # if net[0] == 1 ... jne done mov al, 1 # status = 1 done: mov byte ptr [rdi + rdx + 2], al # send[2 + len] = status retbash clang -target x86_64-linux-gnu -c build_packet.s -o build_packet.oLa routine contient un exemple de chaque cas de cette leçon : une constante (l’en-tête), une copie avec transformation, un mélange plusieurs-vers-un (la somme de contrôle), un idiome de mise à zéro, et un flux implicite (l’octet de statut).
-
Enregistrez
taint.py. L’état fantôme se compose de deux dictionnaires d’ensembles de couleurs, l’un pour les registres, l’autre pour les octets de mémoire. Un hook de code s’exécute avant chaque instruction, la désassemble avec Capstone, calcule les adresses effectives à partir des valeurs réelles des registres, et applique la table de propagation vue plus haut :python # taint.py - a byte-level dynamic taint tracker for one emulated x86-64 routine import sys from elftools.elf.elffile import ELFFile from capstone import Cs, CS_ARCH_X86, CS_MODE_64 from capstone.x86 import X86_OP_REG, X86_OP_MEM, X86_OP_IMM from unicorn import Uc, UC_ARCH_X86, UC_MODE_64, UC_HOOK_CODE from unicorn import x86_const CODE, NET, SEND, STACK = 0x10000, 0x20000, 0x30000, 0x40000 net = bytes.fromhex(sys.argv[1]) if len(sys.argv) > 1 else b"\x01ping" code = ELFFile(open("build_packet.o", "rb")).get_section_by_name(".text").data() # --- shadow state: a set of colours per register and per memory byte ------- # colour i means "derived from network byte i" FAMILY = {"a": "rax", "b": "rbx", "c": "rcx", "d": "rdx", "si": "rsi", "di": "rdi"} def canon(name): # al/eax/rax -> rax, r8b -> r8 if name.startswith("r") and name[1:2].isdigit(): return "r" + "".join(ch for ch in name[1:] if ch.isdigit()) core = name.lstrip("er").rstrip("xlhw") or name return FAMILY.get(core, name) reg_taint, mem_taint, flags_taint = {}, {}, set() for i in range(len(net)): mem_taint[NET + i] = {i} # SOURCE: every network byte md = Cs(CS_ARCH_X86, CS_MODE_64); md.detail = True mu = Uc(UC_ARCH_X86, UC_MODE_64) def reg_value(reg_id): return mu.reg_read(getattr(x86_const, "UC_X86_REG_" + md.reg_name(reg_id).upper())) def addr_of(op): m = op.mem a = m.disp if m.base: a += reg_value(m.base) if m.index: a += reg_value(m.index) * m.scale return a def get(op): if op.type == X86_OP_REG: return set(reg_taint.get(canon(md.reg_name(op.reg)), set())) if op.type == X86_OP_MEM: a = addr_of(op) return set().union(*(mem_taint.get(a + k, set()) for k in range(op.size))) return set() # immediates are never tainted def put(op, t): if op.type == X86_OP_REG: reg_taint[canon(md.reg_name(op.reg))] = t else: a = addr_of(op) for k in range(op.size): mem_taint[a + k] = t def where(op): if op.type != X86_OP_MEM: return md.reg_name(op.reg) a = addr_of(op) return f"send[{a - SEND}]" if a >= SEND else f"net[{a - NET}]" def show(t): return "{" + ",".join(f"net[{i}]" for i in sorted(t)) + "}" if t else "clean" def on_insn(uc, address, size, _): global flags_taint insn = next(md.disasm(code[address - CODE:address - CODE + size], address)) ops, mn = insn.operands, insn.mnemonic text = f"{address:#x} {mn} {insn.op_str}" if mn in ("mov", "movzx"): # copy t = get(ops[1]); put(ops[0], t) elif mn in ("xor", "sub") and ops[0].type == ops[1].type == X86_OP_REG \ and ops[0].reg == ops[1].reg: # zeroing idiom t = set(); put(ops[0], t) elif mn in ("add", "sub", "xor", "and", "or"): # combine t = get(ops[0]) | get(ops[1]); put(ops[0], t) elif mn in ("cmp", "test"): # flags only flags_taint = get(ops[0]) | get(ops[1]) if flags_taint: print(f"{text:46} flags <- {show(flags_taint)}") return elif mn.startswith("j") and mn != "jmp": if flags_taint: print(f"{text:46} BRANCH decided by {show(flags_taint)}") return else: return # inc, ret ... print(f"{text:46} {where(ops[0])} <- {show(t)}") mu.mem_map(CODE, 0x1000); mu.mem_map(NET, 0x1000) mu.mem_map(SEND, 0x1000); mu.mem_map(STACK, 0x1000) mu.mem_write(CODE, code); mu.mem_write(NET, net) for reg, val in (("RDI", SEND), ("RSI", NET), ("RDX", len(net)), ("RSP", STACK + 0x800)): mu.reg_write(getattr(x86_const, "UC_X86_REG_" + reg), val) mu.hook_add(UC_HOOK_CODE, on_insn) mu.emu_start(CODE, CODE + len(code) - 1) # stop at `ret` print("\nSINK: send buffer as it would be passed to send()") out = mu.mem_read(SEND, len(net) + 3) for i, b in enumerate(out): print(f" send[{i}] = {b:#04x} {show(mem_taint.get(SEND + i, set()))}")Notez les simplifications. Les registres sont suivis en entier (
al,eaxetraxpartagent une entrée) plutôt qu’octet par octet, seule la poignée de mnémoniques utilisée par cette routine est modélisée, et la teinte d’adresse est désactivée :getsur un opérande mémoire ne regarde que les octets lus, jamais la teinte dersiourcxservant à les adresser. -
Exécutez-le avec le tampon réseau par défaut,
01 70 69 6e 67(un octet de commande valant1suivi deping) :bash ./venv/bin/python taint.pytext 0x10000 mov byte ptr [rdi], 0x42 send[0] <- clean 0x10003 xor ecx, ecx ecx <- clean 0x10005 xor r8d, r8d r8d <- clean 0x10008 movzx eax, byte ptr [rsi + rcx] eax <- {net[0]} 0x1000c xor al, 0x5a al <- {net[0]} 0x1000e add al, cl al <- {net[0]} 0x10010 mov byte ptr [rdi + rcx + 1], al send[1] <- {net[0]} 0x10014 xor r8b, al r8b <- {net[0]} 0x10008 movzx eax, byte ptr [rsi + rcx] eax <- {net[1]} ... 0x10014 xor r8b, al r8b <- {net[0],net[1],net[2],net[3],net[4]} 0x1001f mov byte ptr [rdi + rdx + 1], r8b send[6] <- {net[0],net[1],net[2],net[3],net[4]} 0x10024 xor eax, eax eax <- clean 0x10026 cmp byte ptr [rsi], 1 flags <- {net[0]} 0x10029 jne 0x1002d BRANCH decided by {net[0]} 0x1002b mov al, 1 al <- clean 0x1002d mov byte ptr [rdi + rdx + 2], al send[7] <- clean SINK: send buffer as it would be passed to send() send[0] = 0x42 clean send[1] = 0x5b {net[0]} send[2] = 0x2b {net[1]} send[3] = 0x35 {net[2]} send[4] = 0x37 {net[3]} send[5] = 0x41 {net[4]} send[6] = 0x33 {net[0],net[1],net[2],net[3],net[4]} send[7] = 0x01 cleanLisez-le en analyste. L’en-tête est propre, comme doit l’être une constante. Chacun des octets
send[1]àsend[5]porte exactement une couleur : chaque octet de sortie dérive donc d’un seul octet d’entrée malgré le XOR et l’ajout de l’index ; la transformation a changé les valeurs, pas le flux.send[6]porte les cinq couleurs, la signature d’une somme de contrôle ou d’un hachage. Lexor eax, eaxde mise à zéro a nettoyéeaxcomme le prévoit la politique. Et le traceur a signalé que lejneen0x10029est décidé parnet[0]: c’est la réponse à « quels octets d’entrée contrôlent un branchement ». -
Modifiez maintenant uniquement l’octet de commande et relancez :
bash ./venv/bin/python taint.py 0270696e67text 0x10029 jne 0x1002d BRANCH decided by {net[0]} 0x1002d mov byte ptr [rdi + rdx + 2], al send[7] <- clean SINK: send buffer as it would be passed to send() send[0] = 0x42 clean send[1] = 0x58 {net[0]} send[2] = 0x2b {net[1]} send[3] = 0x35 {net[2]} send[4] = 0x37 {net[3]} send[5] = 0x41 {net[4]} send[6] = 0x30 {net[0],net[1],net[2],net[3],net[4]} send[7] = 0x00 cleansend[7]est passé de0x01à0x00parce quenet[0]a changé, et pourtant le traceur le déclare toujours propre. C’est le flux implicite vu plus haut, pris la main dans le sac : l’octet de statut ne dépend de l’entrée qu’à travers le branchement, et un suivi pur du flot de données ne peut pas le voir. Le rapport de branchement teinté est l’indice que quelque chose en aval dépend de l’entrée.
Questions : Pourquoi send[6] porte-t-il toutes les couleurs, et à quoi
ressemblerait le tampon d’un vrai échantillon si un octet portait les couleurs
de deux sources différentes, comme une lecture de fichier et une requête au
registre ? Réécrivez la logique de statut sous la forme sete al après le
cmp : de quelle règle de propagation auriez-vous besoin pour que le traceur
l’attrape, et pourquoi n’est-ce plus un flux implicite ? Si vous remplaciez
movzx eax, byte ptr [rsi + rcx] par une consultation d’une table de
256 octets indexée par l’octet réseau, que signalerait ce traceur pour
send[1] à send[5], et quelle option changerait cela ? Puisque les registres
sont ici suivis en entier, construisez une séquence de deux instructions qui
provoque une sur-teinte du traceur.
À retenir
- L’analyse de teinte répond à une seule question : les données d’une source atteignent-elles un puits, et par quels octets ? Les sources et les puits sont généralement des hooks d’API ; la propagation a lieu à chaque instruction.
- Les règles de propagation copient la teinte lors des déplacements, en font l’union lors des opérations arithmétiques et l’effacent sur les constantes et les idiomes de mise à zéro ; se tromper sur les idiomes provoque une sur-teinte galopante.
- La granularité (généralement l’octet), les couleurs (un masque ou un ensemble d’étiquettes par octet) et la mémoire fantôme (du bitmap aux ensembles par octet) arbitrent entre précision d’un côté, mémoire et vitesse de l’autre ; des ralentissements d’un ordre de grandeur sont normaux.
- La sur-teinte vient d’un suivi grossier et d’une teinte d’adresse agressive ; la sous-teinte vient des instructions non modélisées, des tables de correspondance et des flux implicites à travers les branchements, que la plupart des traceurs ignorent délibérément mais peuvent signaler comme branchements teintés.
- libdft offre une teinte rapide à l’octet sur Pin, Triton associe la teinte à l’exécution symbolique, et PANDA fournit une teinte sur système complet fondée sur l’enregistrement et le rejeu.
- Face aux malwares, utilisez la teinte pour trouver quels octets d’entrée orientent un parseur, où circule une configuration déchiffrée, et de quoi est composé un tampon exfiltré.