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 ? ».
| Sorte | Exemple | Type de référence Ghidra | Suffixe IDA |
|---|---|---|---|
| Code → code, appel | call 0x1400014b0 | UNCONDITIONAL_CALL | p (procédure) |
| Code → code, saut | jne 0x1400015c0 | CONDITIONAL_JUMP, UNCONDITIONAL_JUMP | j |
| Code → code, indirect | call qword ptr [rip+0x6db5] via l’IAT | COMPUTED_CALL | p |
| Code → données, lecture | cmp dword ptr [rip+0x1234], 0 | READ | r |
| Code → données, écriture | mov qword ptr [rip+0x1234], rax | WRITE | w |
| Code → données, adresse prise | lea rdx, [rip+0x2bb1] | DATA | o (offset) |
| Données → données | Un emplacement de l’IAT pointant vers une fonction importée, une table de pointeurs de chaînes | DATA | o |
| Données → code | Une vtable, une table de callbacks, une entrée de déroulement d’exception | DATA | o |
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]en0x140001490(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 :
- 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.
- Utilisez les xrefs pour sauter au code qui l’utilise.
- Comprenez juste assez de cette fonction pour la nommer, ainsi que ses variables intéressantes.
- 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 %s | Les xrefs vers la chaîne | La routine de C2, de téléchargement ou de journalisation |
Un chemin de registre comme …\CurrentVersion\Run | Les xrefs vers la chaîne, puis vers le RegSetValueEx voisin | Le code de persistance |
Un import intéressant (CreateRemoteThread, CryptEncrypt) | Les xrefs vers l’import | L’implémentation de la capacité et ses arguments |
Des imports GetProcAddress / LoadLibrary | Les xrefs vers eux, puis l’argument de nom | Les API résolues dynamiquement (voir plus bas) |
Une constante comme 0x5A4D (MZ), 0xEDB88320 (CRC-32) ou un hash d’API | Une recherche de la valeur scalaire, puis les xrefs vers la fonction qui la contient | L’analyse de PE, les sommes de contrôle, la recherche d’API par hash |
| Une variable globale écrite une fois et lue à de nombreux endroits | D’abord la xref d’écriture, puis les xrefs de lecture | Une configuration ou un état fixé au démarrage |
| Une fonction avec de nombreux appelants | Les xrefs vers la fonction | Des routines utilitaires : déchiffrement de chaînes, journalisation, allocation |
| Une fonction sans appelant | Les xrefs vers son adresse dans les données | Callbacks, routines de démarrage de thread, méthodes de vtable |
| Un bloc de données à forte entropie | Les xrefs vers le bloc, puis la boucle qui le lit | La 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 :
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
- Trouvez le résolveur. Les xrefs vers
GetProcAddress/LoadLibrary, ou, dans le cas du hachage, la fonction qui parcourtPEB→ données du loader → répertoire des exports. Il a généralement de nombreux appelants ou remplit une table. - 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).
- 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 unestructdont les champs sont des pointeurs de fonction typés et appliquez-la à la table : chaque(*api->VirtualAlloc)(...)devient lisible d’un coup. - 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, ouida_xref.add_crefen IDAPython. Pour les gros échantillons, scriptez-le. - 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.
-
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.ctext [xrefdemo] starting measurement [xrefdemo] result 49 -
Pivotez depuis la chaîne. Importez
xrefdemo.exedans un projet Ghidra et analysez-le avec les options par défaut. Ouvrez Search > For Strings, filtrez surxrefdemoet 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 estlog_startet laquelle estlog_result; renommez-les (Lsur le nom de la fonction). -
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
GetTickCountsous Imports. -
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
FARPROCest un pointeur générique « qui renvoieINT_PTR» : les valeurs de retour sont donc mal typées et castées. -
Restaurez le type. Faites un clic droit sur
pFVar2, choisissez Retype Variable (Ctrl+L) et saisissezGetTickCount *. Le prototype vient de l’archivewindows_vs12_64de 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 enpGetTickCountet la fonction enmeasure_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. -
Restaurez les xrefs. Le pseudo-code est corrigé, mais les références à
GetTickCountn’existent toujours pas. Dans le Listing, cliquez sur leCALL RAXen0x1400014db, appuyez surR(References > Add/Edit) et ajoutez une référence externe versKERNEL32.DLL/GetTickCountde typeCOMPUTED_CALL. Recommencez pour leCALL RBXen0x1400014ea. 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 -
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 GetTickCounttext 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 tableLe résultat pour la chaîne correspond à Ghidra. Le résultat pour l’import contient une occurrence de plus : un stub
jmpen0x140002aa8, 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.GetTickCountn’a pas d’emplacement dans l’IAT et donc aucune xref ; sa seule trace est la chaîne, que--string GetTickCounttrouve.
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.