Skip to content

Leçon 4.5 · Désassemblage & analyse de code· 50 min

Références croisées et flot de données

Utiliser les références croisées pour passer des chaînes et imports au code qui s’en sert, remonter les valeurs et restaurer les xrefs perdues.

Objectifs

  • Distinguer les références croisées code vers code, code vers données et données vers données, et expliquer comment un désassembleur les calcule
  • Utiliser les xrefs pour passer d’une chaîne, d’un import ou d’une constante au code qui l’utilise
  • Remonter une valeur grâce au raisonnement use-def, au suivi des registres et de la pile, et à la propagation des constantes
  • Expliquer pourquoi les appels indirects et la résolution dynamique des API cassent les xrefs, et les restaurer avec des types et des références manuelles
  • Calculer vous-même les xrefs relatives à RIP avec Capstone et pefile

L’essentiel du temps passé dans un désassembleur sert à naviguer, pas à lire de haut en bas : d’une chaîne suspecte à la fonction qui l’utilise, de cette fonction à ses appelants, d’un argument à l’endroit où il a été calculé. Les arcs que vous parcourez sont des références croisées (xrefs) ; le raisonnement que vous appliquez en chemin est l’analyse de flot de données, menée surtout de tête avec l’aide de l’outil.

Cette leçon clôt le module 4. Les décompilateurs et leurs limites a montré comment rendre une fonction lisible ; ici, vous apprenez à trouver les quelques fonctions qui comptent dans un binaire qui en contient des milliers, et que faire quand l’outil ne voit pas les liens.

Qu’est-ce qu’une référence croisée

Une xref est une relation enregistrée « l’élément à l’adresse A fait référence à l’adresse B », assortie d’un type. Tout désassembleur sérieux les conserve dans les deux sens : vous pouvez donc demander « à quoi cette instruction fait-elle référence ? » et, plus utile encore, « qu’est-ce qui fait référence à cette adresse ? ».

SorteExempleType de référence GhidraSuffixe IDA
Code → code, appelcall 0x1400014b0UNCONDITIONAL_CALLp (procédure)
Code → code, sautjne 0x1400015c0CONDITIONAL_JUMP, UNCONDITIONAL_JUMPj
Code → code, indirectcall qword ptr [rip+0x6db5] via l’IATCOMPUTED_CALLp
Code → données, lecturecmp dword ptr [rip+0x1234], 0READr
Code → données, écrituremov qword ptr [rip+0x1234], raxWRITEw
Code → données, adresse priselea rdx, [rip+0x2bb1]DATAo (offset)
Données → donnéesUn emplacement de l’IAT pointant vers une fonction importée, une table de pointeurs de chaînesDATAo
Données → codeUne vtable, une table de callbacks, une entrée de déroulement d’exceptionDATAo

La ligne « adresse prise » est importante : un lea qui charge l’adresse d’une chaîne ne lit pas la chaîne, il transmet le pointeur, généralement comme argument. Les chaînes et les pointeurs de fonction sont presque toujours référencés de cette façon.

Comment l’outil calcule les xrefs

Les xrefs sont un sous-produit du désassemblage. À mesure que le désassembleur décode chaque instruction (voir Désassemblage linéaire vs récursif), il évalue tout opérande dont la cible est une constante :

  • Les branchements et appels directs portent un déplacement relatif ; la cible est l’adresse de l’instruction suivante plus le déplacement.
  • Les opérandes mémoire relatifs à RIP, la façon normale dont le code x86-64 adresse les variables globales (voir modes d’adressage), fonctionnent de la même manière : lea rdx, [rip+0x2bb1] en 0x140001490 (7 octets de long) fait référence à 0x140001497 + 0x2bb1 = 0x140004048.
  • Les adresses absolues du code 32 bits (push offset aHello) sont des valeurs immédiates qui tombent dans l’image. Les entrées de relocation aident à confirmer qu’un nombre est réellement une adresse et non une coïncidence.
  • Le balayage des données cherche, dans les sections de données, des valeurs de la taille d’un pointeur qui pointent dans l’image : vtables, tables de sauts, tableaux de callbacks.

Tout ce qui est calculé à l’exécution — un registre chargé depuis la mémoire, une somme de deux registres, un pointeur renvoyé par une API — ne produit aucune xref. Gardez cela en tête : c’est la racine de tous les problèmes de la seconde moitié de la leçon.

La boucle principale de l’analyste

L’analyse statique du code d’un malware (logiciel malveillant) est avant tout une boucle :

  1. Choisissez un point d’ancrage : une chaîne, un import, une constante, un motif d’octets issu d’une correspondance YARA, une fonction signalée par capa.
  2. Utilisez les xrefs pour sauter au code qui l’utilise.
  3. Comprenez juste assez de cette fonction pour la nommer, ainsi que ses variables intéressantes.
  4. Suivez les xrefs vers l’extérieur (vers les appelants, via les xrefs vers la fonction) ou vers l’intérieur (vers les fonctions appelées et les données qu’elle touche), et recommencez.

Chaque renommage apparaît partout où la fonction est appelée : le binaire devient donc plus facile à lire à mesure que vous avancez. Une liste pratique de pivots :

Je vois…Je pivote via…Je cherche…
Une URL, un domaine, un user agent ou une chaîne de format %sLes xrefs vers la chaîneLa routine de C2, de téléchargement ou de journalisation
Un chemin de registre comme …\CurrentVersion\RunLes xrefs vers la chaîne, puis vers le RegSetValueEx voisinLe code de persistance
Un import intéressant (CreateRemoteThread, CryptEncrypt)Les xrefs vers l’importL’implémentation de la capacité et ses arguments
Des imports GetProcAddress / LoadLibraryLes xrefs vers eux, puis l’argument de nomLes API résolues dynamiquement (voir plus bas)
Une constante comme 0x5A4D (MZ), 0xEDB88320 (CRC-32) ou un hash d’APIUne recherche de la valeur scalaire, puis les xrefs vers la fonction qui la contientL’analyse de PE, les sommes de contrôle, la recherche d’API par hash
Une variable globale écrite une fois et lue à de nombreux endroitsD’abord la xref d’écriture, puis les xrefs de lectureUne configuration ou un état fixé au démarrage
Une fonction avec de nombreux appelantsLes xrefs vers la fonctionDes routines utilitaires : déchiffrement de chaînes, journalisation, allocation
Une fonction sans appelantLes xrefs vers son adresse dans les donnéesCallbacks, routines de démarrage de thread, méthodes de vtable
Un bloc de données à forte entropieLes xrefs vers le bloc, puis la boucle qui le litLa routine de déchiffrement ou de décompression

Astuce : Une fonction qui compte des dizaines d’appelants, chacun passant un pointeur différent vers le même bloc de données, est très souvent un déchiffreur de chaînes. Nommez-la une fois, et chaque site d’appel devient un endroit où retrouver une chaîne en clair — l’automatisation par script est traitée dans le module 8.

Remonter les valeurs : le flot de données

Les xrefs vous mènent à la bonne fonction. Le flot de données répond à la question suivante : que contient réellement ce registre au moment de cet appel ?

Chaînes use-def et def-use

Chaque instruction utilise certaines valeurs et en définit d’autres. Pour un usage donné — par exemple l’argument rcx d’un appel —, la chaîne use-def liste les définitions qui ont pu le produire. La chaîne def-use est l’inverse : pour une définition, tous les endroits qui la lisent.

Formellement, les compilateurs calculent les définitions atteignantes (reaching definitions) : une définition atteint un point s’il existe un chemin de la définition à ce point le long duquel la valeur n’est pas écrasée. Vous n’avez pas besoin des équations ensemblistes pour utiliser l’idée. En remontant depuis l’appel, vous vous arrêtez à la première écriture du registre sur chaque chemin ; si des chemins se rejoignent depuis des blocs différents, chacun fournit un candidat. Au sein d’un même bloc de base, il y a exactement une réponse ; au-delà d’une jonction du CFG, il peut y en avoir plusieurs.

Voici la fonction du lab de cette leçon, telle que désassemblée par objdump :

text
1400014b0:  push   rsi
1400014b1:  push   rbx
1400014b2:  sub    rsp,0x28
1400014b6:  lea    rcx,[rip+0x2b51]        # 0x14000400e  "kernel32.dll"
1400014bd:  call   QWORD PTR [rip+0x6dbd]  # 0x140008280  GetModuleHandleA
1400014c3:  lea    rdx,[rip+0x2b51]        # 0x14000401b  "GetTickCount"
1400014ca:  mov    rcx,rax
1400014cd:  call   QWORD PTR [rip+0x6db5]  # 0x140008288  GetProcAddress
1400014d3:  mov    rbx,rax
1400014d6:  test   rax,rax
1400014d9:  je     0x1400014f5
1400014db:  call   rax
1400014dd:  mov    ecx,0x32
1400014e2:  mov    esi,eax
1400014e4:  call   QWORD PTR [rip+0x6dbe]  # 0x1400082a8  Sleep
1400014ea:  call   rbx
1400014ec:  sub    eax,esi

(Les noms de chaînes et d’API sont des annotations ajoutées pour la lisibilité.) Pour identifier le call rbx en 0x1400014ea, remontez : rbx a été défini pour la dernière fois en 0x1400014d3 par mov rbx,rax ; ce rax est la valeur de retour de l’appel en 0x1400014cd, qui lit son emplacement d’IAT 0x140008288 → GetProcAddress. Son argument rdx a été défini en 0x1400014c3 comme un pointeur vers "GetTickCount". Les deux appels indirects invoquent donc GetTickCount.

Les registres à travers les appels

La remontée ci-dessus a franchi un appel à Sleep sans s’inquiéter. Ce n’est sûr qu’en raison de la convention d’appel Windows x64 : rax, rcx, rdx, r8–r11 sont volatils (une fonction appelée peut les détruire), tandis que rbx, rbp, rdi, rsi, rsp et r12–r15 sont non volatils (la fonction appelée doit les préserver). Le compilateur le savait aussi : il a copié le pointeur de fonction dans rbx et le premier compteur de ticks dans esi précisément pour qu’ils survivent à Sleep. Quand vous suivez une valeur à travers un appel, ne faites confiance qu’aux registres non volatils et à la mémoire — et rappelez-vous qu’un malware écrit en assembleur ou avec des conventions maison peut ne pas respecter ces règles.

Variables de pile

Les valeurs circulent aussi par la pile. Le compilateur déverse des registres à des décalages fixes par rapport à rsp (ou rbp), et Ghidra comme IDA transforment ces décalages en variables locales nommées (local_38, var_28). Suivre une variable de pile, c’est trouver chaque écriture dans cet emplacement, ce que l’outil peut vous montrer : dans Ghidra, les variables locales en tête d’une fonction dans le Listing portent leurs propres listes XREF avec des marqueurs R et W. Attention au problème d’aliasing de la leçon précédente : dès que l’adresse d’une variable locale est passée à une fonction (lea rcx,[rsp+0x20] puis call), toute écriture via ce pointeur est invisible pour un suivi simple.

Propagation des constantes

Beaucoup d’arguments sont des constantes fixées quelques instructions avant l’appel. La propagation des constantes reporte ces valeurs vers l’avant, de sorte que mov ecx,0x32 suivi de call Sleep se lit Sleep(50). Cela compte surtout pour les flags et les énumérations : VirtualAlloc(..., 0x3000, 0x40) signifie MEM_COMMIT | MEM_RESERVE avec PAGE_EXECUTE_READWRITE, et reconnaître ces nombres à vue est une vraie compétence. Les décompilateurs le font automatiquement ; dans le listing, vous le faites à la main. La même technique résout les adresses calculées quand une base et un décalage sont tous deux constants, et c’est ainsi que les outils retrouvent les cibles des tables de sauts.

Le slicing : « d’où vient ce tampon ? »

Une tranche arrière (backward slice) d’une valeur est l’ensemble des instructions qui ont pu y contribuer ; une tranche avant (forward slice) est tout ce que la valeur peut influencer. Quand vous demandez « d’où vient le tampon passé à send ? », vous calculez une tranche arrière à la main : via l’argument de l’appel, jusqu’au memcpy qui l’a rempli, puis à la boucle de déchiffrement qui précède, puis à la ressource d’où il a été chargé.

Le Decompiler de Ghidra peut surligner les tranches pour vous : faites un clic droit sur une variable et utilisez les actions de surlignage Def-use, Forward Slice et Backward Slice. Elles fonctionnent au sein d’une seule fonction et ne valent que ce que vaut la vision de l’aliasing qu’a le décompilateur, mais elles sont un excellent moyen de voir d’un coup d’œil quelles lignes comptent. Le suivi dynamique des données à travers tout un programme (l’analyse de teinte) est traité dans le module 11.

Attention : Le flot de données statique s’arrête à tout ce que l’outil ne sait pas résoudre : appels indirects, valeurs lues depuis des fichiers ou le réseau, pointeurs écrits via des alias. Quand une tranche se termine sur « inconnu », passez à un débogueur et lisez la valeur à l’exécution plutôt que de deviner.

Quand les xrefs cassent

Appels indirects

Un call rax ou un call qword ptr [rbx+0x18] n’a pas de cible constante : aucune xref de code n’est donc enregistrée. La fonction appelée perd un appelant, le graphe d’appels a un trou, et le décompilateur ne peut pas propager le prototype de la fonction appelée. Au mieux, l’outil enregistre une xref « adresse prise » là où le pointeur est chargé, aucune là où il est appelé. Les callbacks, les vtables et les tables de répartition du C et du C++ ordinaires ressemblent à cela.

Résolution dynamique des imports et hachage d’API

Les malwares vont plus loin, délibérément. Avec la résolution dynamique des imports, les API sont obtenues via GetProcAddress et n’apparaissent jamais dans la table des imports ; « xrefs vers CreateRemoteThread » ne renvoie donc rien, car il n’existe aucun symbole CreateRemoteThread à référencer. Les chaînes de noms peuvent elles-mêmes être cachées sous forme de stack strings ou chiffrées. Avec le hachage d’API, même GetProcAddress disparaît : le code parcourt les tables d’exports et compare des hashs, ne laissant qu’une constante de 32 bits à chaque recherche.

Dans les deux cas, la table des pivots cesse de fonctionner précisément pour les API que l’auteur voulait cacher. C’est alors le résolveur qui devient votre point d’ancrage.

Restaurer ce qui a été perdu

  1. Trouvez le résolveur. Les xrefs vers GetProcAddress/LoadLibrary, ou, dans le cas du hachage, la fonction qui parcourt PEB → données du loader → répertoire des exports. Il a généralement de nombreux appelants ou remplit une table.
  2. Retrouvez les noms. Lisez les arguments de type chaîne, déchiffrez-les, ou calculez le hash de chaque export des DLL habituelles et faites correspondre les constantes (un petit script ; voir la page sur le hachage d’API).
  3. Typez les pointeurs. Retypez la variable ou la globale qui stocke chaque résultat en pointeur vers le vrai prototype de la fonction. L’archive Windows de Ghidra contient les prototypes des API documentées ; un type comme GetTickCount * restaure donc le type de retour et les arguments à chaque site d’appel. Quand le malware remplit toute une table de pointeurs, définissez une struct dont les champs sont des pointeurs de fonction typés et appliquez-la à la table : chaque (*api->VirtualAlloc)(...) devient lisible d’un coup.
  4. Ajoutez les références. Les types corrigent le pseudo-code ; ils n’ajoutent pas de xrefs. Pour que les appels apparaissent dans les listes de références et les graphes d’appels, ajoutez-les explicitement : References > Add/Edit (R) de Ghidra sur l’instruction d’appel, ou ida_xref.add_cref en IDAPython. Pour les gros échantillons, scriptez-le.
  5. Ou observez la résolution. Un point d’arrêt de débogueur sur GetProcAddress, ou une trace des API, vous donne directement les noms résolus — le module Analyse dynamique couvre les deux.

Les conventions de l’API Windows qui facilitent les étapes 2 et 3 sont traitées dans la future leçon L’API Windows pour les analystes (module 5), et les attaques délibérées contre le désassemblage lui-même dans la future leçon Anti-désassemblage (module 9).

Lab : suivre les xrefs et restaurer un appel indirect

Ce lab utilise un petit programme inoffensif : une chaîne utilisée par deux fonctions de journalisation, et GetTickCount appelée via un pointeur obtenu avec GetProcAddress. La sortie de Ghidra ci-dessous provient de Ghidra 12.1.4 (via son analyseur headless et des scripts ; l’interface graphique affiche les mêmes données) sur un build MinGW-w64 GCC 15.2. Les adresses peuvent différer avec d’autres versions du compilateur.

  1. Enregistrez sous xrefdemo.c :

    c
    /* xrefdemo.c: one string used by two functions, one API called through a pointer */
    #include <windows.h>
    #include <stdio.h>
    
    static const char LOG_PREFIX[] = "[xrefdemo] ";
    
    typedef DWORD (WINAPI *tick_fn)(void);
    
    __attribute__((noinline))
    static void log_start(const char *what) {
        printf("%sstarting %s\n", LOG_PREFIX, what);
    }
    
    __attribute__((noinline))
    static void log_result(DWORD value) {
        printf("%sresult %lu\n", LOG_PREFIX, (unsigned long)value);
    }
    
    __attribute__((noinline))
    static DWORD measure(void) {
        HMODULE k32 = GetModuleHandleA("kernel32.dll");
        tick_fn get_ticks = (tick_fn)GetProcAddress(k32, "GetTickCount");
        if (!get_ticks) return 0;
        DWORD t0 = get_ticks();
        Sleep(50);
        return get_ticks() - t0;
    }
    
    int main(void) {
        log_start("measurement");
        log_result(measure());
        return 0;
    }

    Compilez et exécutez :

    bash
    x86_64-w64-mingw32-gcc -O2 -s -o xrefdemo.exe xrefdemo.c
    text
    [xrefdemo] starting measurement
    [xrefdemo] result 49
  2. Pivotez depuis la chaîne. Importez xrefdemo.exe dans un projet Ghidra et analysez-le avec les options par défaut. Ouvrez Search > For Strings, filtrez sur xrefdemo et double-cliquez sur le résultat. Dans le Listing, faites un clic droit sur la chaîne et choisissez References > Show References to Address. Vous obtenez deux références :

    text
    xrefs to "[xrefdemo] " @ 140004048
      140001490  DATA  in FUN_140001490  : LEA RDX,[0x140004048]
      140001507  DATA  in FUN_140001500  : LEA RDX,[0x140004048]

    Ce sont toutes deux des références « adresse prise » issues d’un lea. Les chaînes de format voisines vous indiquent quelle fonction est log_start et laquelle est log_result ; renommez-les (L sur le nom de la fonction).

  3. Pivotez depuis l’import. Dans le Symbol Tree, dépliez Imports > KERNEL32.DLL et affichez les références à GetProcAddress :

    text
    xrefs to GetProcAddress (Function) @ EXTERNAL:00000005
      140008288  DATA  in ?  : null
      1400014cd  COMPUTED_CALL  in FUN_1400014b0  : CALL qword ptr [0x140008288]

    La première entrée est l’emplacement de l’IAT (une référence données → données) ; la seconde est le vrai site d’appel. Notez qu’il n’y a aucun GetTickCount sous Imports.

  4. Lisez la sortie du résolveur. Ouvrez FUN_1400014b0. Le Decompiler affiche :

    c
    int FUN_1400014b0(void)
    
    {
      int iVar1;
      HMODULE hModule;
      FARPROC pFVar2;
      INT_PTR IVar3;
      INT_PTR IVar4;
    
      hModule = GetModuleHandleA("kernel32.dll");
      pFVar2 = GetProcAddress(hModule,"GetTickCount");
      if (pFVar2 == (FARPROC)0x0) {
        iVar1 = 0;
      }
      else {
        IVar3 = (*pFVar2)();
        Sleep(0x32);
        IVar4 = (*pFVar2)();
        iVar1 = (int)IVar4 - (int)IVar3;
      }
      return iVar1;
    }

    Les types Windows ont été appliqués automatiquement, mais FARPROC est un pointeur générique « qui renvoie INT_PTR » : les valeurs de retour sont donc mal typées et castées.

  5. Restaurez le type. Faites un clic droit sur pFVar2, choisissez Retype Variable (Ctrl+L) et saisissez GetTickCount *. Le prototype vient de l’archive windows_vs12_64 de Ghidra ; si le sélecteur de types ne le propose pas, ouvrez d’abord cette archive dans le Data Type Manager. Renommez ensuite la variable en pGetTickCount et la fonction en measure_ticks :

    c
    int measure_ticks(void)
    
    {
      DWORD DVar1;
      DWORD DVar2;
      int iVar3;
      HMODULE hModule;
      GetTickCount *pGetTickCount;
    
      hModule = GetModuleHandleA("kernel32.dll");
      pGetTickCount = (GetTickCount *)GetProcAddress(hModule,"GetTickCount");
      if (pGetTickCount == (GetTickCount *)0x0) {
        iVar3 = 0;
      }
      else {
        DVar1 = (*pGetTickCount)();
        Sleep(0x32);
        DVar2 = (*pGetTickCount)();
        iVar3 = DVar2 - DVar1;
      }
      return iVar3;
    }

    Les valeurs de retour sont maintenant des DWORD, et la soustraction n’a plus besoin de casts.

  6. Restaurez les xrefs. Le pseudo-code est corrigé, mais les références à GetTickCount n’existent toujours pas. Dans le Listing, cliquez sur le CALL RAX en 0x1400014db, appuyez sur R (References > Add/Edit) et ajoutez une référence externe vers KERNEL32.DLL / GetTickCount de type COMPUTED_CALL. Recommencez pour le CALL RBX en 0x1400014ea. Afficher les références au nouveau symbole externe liste désormais les deux sites d’appel :

    text
    xrefs to GetTickCount (Label) @ EXTERNAL:0000002c
      1400014db  COMPUTED_CALL  in measure_ticks  : CALL RAX
      1400014ea  COMPUTED_CALL  in measure_ticks  : CALL RBX
  7. Calculez les xrefs vous-même. Pour constater qu’il n’y a rien de magique, ce script trouve chaque instruction dont l’opérande relatif à RIP pointe vers une cible, donnée sous forme de chaîne, de nom d’import ou de RVA. Préparez un environnement virtuel avec pip install capstone pefile (testé avec Capstone 5.0.7 et pefile 2024.8.26) :

    python
    # findrefs.py: list instructions whose RIP-relative operand points at a target
    # usage: python findrefs.py file.exe (--string TEXT | --import NAME | --rva 0xRVA)
    import argparse
    import pefile
    from capstone import Cs, CS_ARCH_X86, CS_MODE_64
    from capstone.x86 import X86_OP_MEM, X86_REG_RIP
    
    ap = argparse.ArgumentParser()
    ap.add_argument("exe")
    g = ap.add_mutually_exclusive_group(required=True)
    g.add_argument("--string")
    g.add_argument("--import", dest="imp")
    g.add_argument("--rva", type=lambda s: int(s, 0))
    args = ap.parse_args()
    
    pe = pefile.PE(args.exe)
    base = pe.OPTIONAL_HEADER.ImageBase
    
    # 1. Resolve the target to an RVA
    if args.string:
        needle = args.string.encode() + b"\0"
        for sec in pe.sections:
            off = sec.get_data().find(needle)
            if off >= 0:
                target = sec.VirtualAddress + off
                break
        else:
            raise SystemExit("string not found")
    elif args.imp:
        iat = {imp.name.decode(): imp.address - base            # IAT slot RVA
               for d in pe.DIRECTORY_ENTRY_IMPORT for imp in d.imports if imp.name}
        if args.imp not in iat:
            raise SystemExit(f"{args.imp} is not in the import table")
        target = iat[args.imp]
    else:
        target = args.rva
    print(f"target RVA 0x{target:x} (VA 0x{base + target:x})")
    
    # 2. Linear sweep over executable sections, computing every RIP-relative address
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = True
    md.skipdata = True
    for sec in pe.sections:
        if not sec.Characteristics & 0x20000000:          # IMAGE_SCN_MEM_EXECUTE
            continue
        code = sec.get_data()[: sec.Misc_VirtualSize]
        for insn in md.disasm(code, base + sec.VirtualAddress):
            if insn.id == 0:                              # skipped data byte
                continue
            for op in insn.operands:
                if op.type == X86_OP_MEM and op.mem.base == X86_REG_RIP:
                    ea = insn.address + insn.size + op.mem.disp
                    if ea - base == target:
                        print(f"  0x{insn.address:x}  {insn.mnemonic} {insn.op_str}")

    Lancez-le de trois façons :

    bash
    python findrefs.py xrefdemo.exe --string "[xrefdemo] "
    python findrefs.py xrefdemo.exe --import GetProcAddress
    python findrefs.py xrefdemo.exe --import GetTickCount
    text
    target RVA 0x4048 (VA 0x140004048)
      0x140001490  lea rdx, [rip + 0x2bb1]
      0x140001507  lea rdx, [rip + 0x2b3a]
    target RVA 0x8288 (VA 0x140008288)
      0x1400014cd  call qword ptr [rip + 0x6db5]
      0x140002aa8  jmp qword ptr [rip + 0x57da]
    GetTickCount is not in the import table

    Le résultat pour la chaîne correspond à Ghidra. Le résultat pour l’import contient une occurrence de plus : un stub jmp en 0x140002aa8, issu de la bibliothèque d’import MinGW, que rien n’appelle. Le désassemblage récursif de Ghidra ne l’a jamais atteint et a laissé ces octets non définis ; le balayage linéaire les a décodés quand même. GetTickCount n’a pas d’emplacement dans l’IAT et donc aucune xref ; sa seule trace est la chaîne, que --string GetTickCount trouve.

Questions : Pourquoi les références à la chaîne sont-elles de type DATA et non READ ? Quels registres le compilateur aurait-il pu utiliser pour garder le pointeur de fonction en vie à travers Sleep, et pourquoi pas rcx ? Dans un vrai échantillon qui résout cinquante API dans une table globale, laquelle des étapes 5 et 6 scripteriez-vous, et de quelles entrées le script aurait-il besoin ? Que signalerait le script à balayage linéaire si la section .text contenait une table de sauts ou des données embarquées ?

À retenir

  • Les xrefs sont des relations enregistrées « A fait référence à B » : code → code (appels, sauts), code → données (lecture, écriture, adresse prise) et données → données ou code (tables de pointeurs, emplacements de l’IAT, vtables).
  • Les outils les calculent à partir des opérandes constants : déplacements des branchements, adresses relatives à RIP, adresses absolues relocalisées et balayage de pointeurs. Tout ce qui est calculé à l’exécution ne produit aucune xref.
  • La boucle principale de l’analyste est : ancrage → xref → compréhension → renommage → pivot. Les bons points d’ancrage sont les chaînes, les imports, les constantes et les fonctions au nombre d’appelants inhabituel.
  • Remontez les valeurs par le raisonnement use-def ; ne faites confiance qu’aux registres non volatils à travers les appels ; méfiez-vous de l’aliasing de la pile ; propagez les constantes pour décoder les flags.
  • Les appels indirects, la résolution dynamique et le hachage d’API suppriment les arcs par lesquels vous naviguez. Restaurez-les en trouvant le résolveur, en retrouvant les noms, en typant les pointeurs et en ajoutant explicitement les références — ou en observant la résolution dynamiquement.