Skip to content

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.

PartieSignificationExemples en analyse de malwares
SourceLà où la teinte est introduiteOctets 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 teinteArguments de send, WriteFile, CreateProcessW, connect ; la cible d’un call/jmp indirect ; la condition d’un branchement
Politique de propagationComment la teinte se déplace à travers chaque instructionUne 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’instructionRègle
mov, movzx, push, popTeinte de la destination := teinte de la source
add, sub, and, or, xorTeinte de la destination := teinte de la destination ∪ teinte de la source
xor reg, reg, sub reg, regTeinte de la destination := propre (le résultat vaut toujours zéro)
Chargement ou stockage d’une valeur immédiateTeinte de la destination := propre
cmp, testAucune 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 :

DispositionCoût fantôme par octet du programmeCouleurs
Bitmap1 bit1
Un octet par octet1 octet8 (un bit chacune)
Un mot de 32 bits par octet4 octets32
Ensemble ou table d’étiquettesVariableIllimité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 :

c
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

OutilConstruit surModèlePoints fortsLimites
libdftIntel PinGranularité à l’octet, 1 bit ou 8 couleurs, mémoire fantôme et processeur virtuelRapide pour la DTA ; vous écrivez un outil Pin qui hooke les appels système comme sources et puitsL’original est Linux 32 bits ; des portages 64 bits existent (libdft64) ; faible couverture SIMD
TritonLe traceur de votre choix ou sa propre émulationTeinte 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 valeursVous pilotez vous-même l’exécution, une instruction à la fois
PANDAÉmulateur de système complet QEMU avec enregistrement et rejeuPlugin taint2 avec ensembles d’étiquettes ; plugins file_taint, tainted_branch, tainted_instrSystè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 teinteTrè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.

  1. 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
  2. Enregistrez la routine sous build_packet.s et 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 .text du 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
        ret
    bash
    clang -target x86_64-linux-gnu -c build_packet.s -o build_packet.o

    La 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).

  3. 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, eax et rax partagent 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 : get sur un opérande mémoire ne regarde que les octets lus, jamais la teinte de rsi ou rcx servant à les adresser.

  4. Exécutez-le avec le tampon réseau par défaut, 01 70 69 6e 67 (un octet de commande valant 1 suivi de ping) :

    bash
    ./venv/bin/python taint.py
    text
    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  clean

    Lisez-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. Le xor eax, eax de mise à zéro a nettoyé eax comme le prévoit la politique. Et le traceur a signalé que le jne en 0x10029 est décidé par net[0] : c’est la réponse à « quels octets d’entrée contrôlent un branchement ».

  5. Modifiez maintenant uniquement l’octet de commande et relancez :

    bash
    ./venv/bin/python taint.py 0270696e67
    text
    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  clean

    send[7] est passé de 0x01 à 0x00 parce que net[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é.