Skip to content

Leçon 3.3 · Triage statique· 35 min

Lire les capacités à partir des imports

Faites d’une table des imports PE une hypothèse de capacités : regroupez les API par comportement, repérez les combinaisons suspectes, confirmez avec capa.

Objectifs

  • Regrouper les imports d’API Windows en catégories de comportement et formuler une hypothèse de capacités
  • Reconnaître les combinaisons d’API à fort signal que les défenseurs utilisent comme indices de détection
  • Expliquer ce qu’implique une table des imports très réduite pour un échantillon
  • Lancer capa, lire sa sortie ATT&CK et MBC, et séparer le bruit du runtime des vrais constats

Dans Imports, exports et l’IAT, vous avez appris comment la table des imports est construite et comment le loader la remplit. Cette leçon porte sur sa lecture. Un programme Windows ne peut toucher ni aux fichiers, ni au registre, ni au réseau, ni aux autres processus sans le demander au système d’exploitation, et ces demandes passent par des API documentées. La liste des API qu’importe un binaire est donc, en pratique, une liste de ce qu’il est capable de faire — une hypothèse de capacités que vous pouvez formuler en quelques minutes, avant même d’ouvrir un désassembleur.

Des imports à une hypothèse

Bien lire les imports, c’est les regrouper. Les fonctions prises isolément sont rarement alarmantes ; les catégories et les combinaisons le sont. Le tableau ci-dessous couvre les groupes que vous utiliserez le plus souvent lors du triage.

ComportementAPI typiquesCe que cela peut indiquer
Système de fichiersCreateFileW, WriteFile, DeleteFileW, MoveFileExW, FindFirstFileW, GetTempPathWDépôt de fichiers, préparation, énumération de fichiers (ransomwares, stealers)
RegistreRegCreateKeyExW, RegSetValueExW, RegQueryValueExW, RegDeleteKeyWStockage de configuration, persistance (clés Run), reconnaissance du système
Création de processusCreateProcessW, ShellExecuteExW, WinExecLancement de charges utiles ou de commandes
Découverte de processusCreateToolhelp32Snapshot, Process32FirstW, EnumProcessesRecherche d’outils de sécurité, d’outils d’analyse ou d’un processus cible
Mémoire d’autres processusOpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread, QueueUserAPC, SetThreadContextInjection de code (voir plus bas)
RéseauWinINet (InternetOpenW, InternetOpenUrlW, HttpSendRequestW), WinHTTP (WinHttpConnect), Winsock (socket, connect, send, recv), URLDownloadToFileWCommunication C2, téléchargement d’étapes suivantes, exfiltration
CryptographieCryptAcquireContextW, CryptEncrypt, BCryptEncrypt, BCryptGenRandomChiffrement de la configuration ou du trafic ; un usage massif évoque un ransomware
ServicesOpenSCManagerW, CreateServiceW, StartServiceWPersistance ou usage de privilèges via des services
Anti-débogageIsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess, OutputDebugStringWVérifications de l’environnement qui modifient le comportement pendant l’analyse
Saisie et écranSetWindowsHookExW, GetAsyncKeyState, GetClipboardData, BitBltKeylogging, vol du presse-papiers, captures d’écran
Ressources propresFindResourceW, LoadResource, LockResource, SizeofResourceDonnées ou charge utile embarquées (voir Ressources et overlays)
Résolution dynamiqueLoadLibraryW, GetProcAddress, LdrGetProcedureAddressImports cachés de cette table

Notez l’hypothèse sous forme de courtes affirmations que vous comptez confirmer ou réfuter plus tard : « peut lire et écrire des valeurs de registre ; peut effectuer des requêtes HTTP ; charge sa propre ressource ». Cette liste oriente ce que vous chercherez en analyse dynamique et l’endroit où vous placerez vos points d’arrêt.

Les combinaisons portent le signal

Les ingénieurs détection s’intéressent surtout aux ensembles d’imports qui n’ont de sens qu’ensemble. Le plus connu est la séquence classique d’injection à distance :

text
  OpenProcess ─► VirtualAllocEx ─► WriteProcessMemory ─► CreateRemoteThread
  (get a handle)  (memory in target)  (copy bytes there)    (run them)

Chaque API prise seule a des usages légitimes — les débogueurs et les installeurs en appellent plusieurs. La chaîne complète dans un petit binaire non signé est un indicateur fort d’injection de processus, ce qui explique que les produits EDR la surveillent à l’exécution et que capa ait des règles pour elle. La page Injection de DLL via CreateRemoteThread décrit la technique du point de vue du défenseur.

Autres combinaisons qui méritent un second regard :

  • FindResource + LoadResource + VirtualAlloc + VirtualProtect — unpacking (dépaquetage) ou déchiffrement d’une charge utile embarquée dans de la mémoire exécutable.
  • CreateToolhelp32Snapshot + Process32Next + TerminateProcess — traque et arrêt des produits de sécurité.
  • CryptEncrypt ou BCryptEncrypt + FindFirstFile/FindNextFile + MoveFileEx — boucles « chiffrer puis renommer » typiques des ransomwares.
  • RegSetValueEx associé à une chaîne contenant Software\Microsoft\Windows\CurrentVersion\Run — persistance par clé Run.

Astuce : Lisez toujours les imports avec les chaînes (voir Chaînes et chaînes obfusquées). Un import dit comment ; une chaîne comme un chemin de registre, une URL ou un nom de processus dit souvent quoi.

Lire précisément les noms d’API

Les noms d’API Windows suivent des conventions qui en modifient légèrement le sens :

  • Suffixes A et W. CreateFileA prend des chaînes ANSI, CreateFileW prend de l’UTF-16. Le code moderne importe surtout les versions W ; beaucoup d’échantillons écrits à la main importent les versions A. Le suffixe vous indique aussi quel encodage de chaînes rechercher.
  • Suffixes Ex. Une version étendue avec davantage de paramètres — VirtualAllocEx est VirtualAlloc pour un autre processus, et c’est précisément pour cela qu’elle compte.
  • Fonctions Nt / Zw de ntdll.dll. Il s’agit de l’API native sous-jacente à la couche Win32 documentée. Les programmes légitimes les importent rarement directement. Des imports directs de ntdll comme NtAllocateVirtualMemory, NtWriteVirtualMemory ou NtQueryInformationProcess suggèrent un code qui cherche à se placer sous les hooks en mode utilisateur, ou à interroger les informations du processus à des fins d’anti-débogage (voir IsDebuggerPresent et CheckRemoteDebuggerPresent).
  • Ordinaux. Certaines DLL sont couramment importées par numéro. ws2_32.dll est le cas classique : l’ordinal 23 est socket, l’ordinal 115 est WSAStartup. pefile et la plupart des outils graphiques retraduisent les ordinaux connus en noms.

Quand la table est presque vide

Un programme compilé normal importe de quelques dizaines à quelques centaines de fonctions. Une table ne comptant qu’une poignée d’entrées est en soi un constat. Par exemple, voici l’intégralité de la liste d’imports KERNEL32.DLL d’un petit programme après l’avoir packé avec UPX :

text
KERNEL32.DLL   LoadLibraryA, ExitProcess, GetProcAddress, VirtualProtect

Avec LoadLibrary et GetProcAddress, le code peut résoudre n’importe quelle API à l’exécution : la véritable liste de capacités a donc quitté la table pour se retrouver dans le code ou dans des données chiffrées. Les explications habituelles sont :

  • Le packing. Le stub d’unpacking n’a besoin que de quelques API ; les imports d’origine sont reconstruits en mémoire. Voir Détecter les packers et l’entropie et packing UPX.
  • La résolution dynamique. L’auteur appelle GetProcAddress avec des noms construits à l’exécution, souvent sous forme de stack strings — voir résolution dynamique des imports.
  • Le hachage d’API. Même GetProcAddress disparaît : le code parcourt les tables d’exports des DLL chargées et compare des hashs de noms. Voir hachage d’API.

Dans les trois cas, l’analyse statique des imports a atteint sa limite, et l’étape suivante consiste à unpacker, ou à observer dynamiquement les API résolues.

Les imports montrent une possibilité, pas une exécution

Gardez deux réserves à l’esprit :

  1. Un import n’est pas un chemin d’appel. Une fonction peut être importée mais jamais atteinte, ou atteinte seulement dans des conditions qui ne se produisent jamais. Les imports justifient une hypothèse, pas un verdict.
  2. Les bibliothèques et les runtimes ajoutent du bruit. Les bibliothèques liées statiquement et le runtime C importent des fonctions que votre code cible n’a jamais demandées. Vous en verrez un cas concret dans le lab : le code de démarrage de MinGW importe VirtualProtect et modifie des protections mémoire, ce qui donne à un programme inoffensif l’air de manipuler de la mémoire exécutable.

capa : des capacités avec des preuves

capa (de l’équipe FLARE de Mandiant) automatise ce raisonnement et va plus loin. Il désassemble le programme et confronte un vaste ensemble de règles communautaires aux caractéristiques qu’il extrait — appels d’API, chaînes, constantes, séquences d’octets, et structure des fonctions qui les utilisent. Les règles ont une portée (un bloc de base, une fonction ou le fichier entier) : une règle comme « create HTTP request » peut ainsi exiger que InternetOpen et HttpSendRequest soient appelées depuis la même fonction, ce qui constitue une preuve bien plus solide que leur simple présence dans la table des imports.

Chaque correspondance est étiquetée avec :

  • un nom de capacité et un espace de noms, comme communication/http/client ;
  • des techniques MITRE ATT&CK, utiles pour les rapports et pour la corrélation avec les détections ;
  • des comportements MBC (Malware Behavior Catalog), une taxonomie propre aux malwares (logiciels malveillants) qui complète ATT&CK.

Lancez-le avec capa sample.exe pour un résumé, capa -v pour voir les adresses qui ont déclenché chaque règle, et capa -vv pour l’arbre de preuves complet. Les modes verbeux vous indiquent exactement quelles fonctions ouvrir dans votre désassembleur.

Attention : capa ne vaut que ce qu’il peut voir. Sur un échantillon packé, il le signalera et ne déclenchera guère que des règles « packed with … » — unpackez d’abord, puis relancez-le.

Lab : prédire, puis vérifier

Ce lab utilise trois petits programmes inoffensifs qui exercent chacun une catégorie de comportement. Il vous faut mingw-w64, Python 3 avec pefile, et capa (le binaire autonome publié, ou pip install flare-capa avec le dépôt de règles).

  1. Créez les programmes.

    regread.c — lit (et crée si elle est absente) une valeur sous HKCU\Software\LabDemo :

    c
    #include <windows.h>
    #include <stdio.h>
    
    int main(void) {
        HKEY key;
        char buf[64] = {0};
        DWORD size = sizeof(buf), type = 0;
        if (RegCreateKeyExA(HKEY_CURRENT_USER, "Software\\LabDemo", 0, NULL, 0,
                            KEY_READ | KEY_WRITE, NULL, &key, NULL) != ERROR_SUCCESS)
            return 1;
        if (RegQueryValueExA(key, "Greeting", NULL, &type, (BYTE *)buf, &size) != ERROR_SUCCESS) {
            const char *def = "hello from the lab";
            RegSetValueExA(key, "Greeting", 0, REG_SZ, (const BYTE *)def, (DWORD)strlen(def) + 1);
            strcpy(buf, def);
        }
        printf("Greeting = %s\n", buf);
        RegCloseKey(key);
        return 0;
    }

    httpget.c — télécharge http://example.com/ avec WinINet et affiche le nombre d’octets :

    c
    #include <windows.h>
    #include <wininet.h>
    #include <stdio.h>
    
    int main(void) {
        char buf[4096];
        DWORD got, total = 0;
        HINTERNET net = InternetOpenA("LabDemo/1.0", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
        if (!net) return 1;
        HINTERNET url = InternetOpenUrlA(net, "http://example.com/", NULL, 0,
                                         INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0);
        if (url) {
            while (InternetReadFile(url, buf, sizeof(buf), &got) && got) total += got;
            InternetCloseHandle(url);
        }
        InternetCloseHandle(net);
        printf("read %lu bytes\n", total);
        return 0;
    }

    proclist.c — liste les processus en cours d’exécution :

    c
    #include <windows.h>
    #include <tlhelp32.h>
    #include <stdio.h>
    
    int main(void) {
        HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (snap == INVALID_HANDLE_VALUE) return 1;
        PROCESSENTRY32 pe = { .dwSize = sizeof(pe) };
        for (BOOL ok = Process32First(snap, &pe); ok; ok = Process32Next(snap, &pe))
            printf("%6lu  %s\n", pe.th32ProcessID, pe.szExeFile);
        CloseHandle(snap);
        return 0;
    }
  2. Compilez-les :

    bash
    x86_64-w64-mingw32-gcc -s -o regread.exe regread.c -ladvapi32
    x86_64-w64-mingw32-gcc -s -o httpget.exe httpget.c -lwininet
    x86_64-w64-mingw32-gcc -s -o proclist.exe proclist.c
  3. Prédisez avant de regarder la sortie d’un quelconque outil. Échangez les fichiers avec un partenaire de lab (ou attendez un jour), puis listez uniquement les imports et rédigez une ligne d’hypothèse de capacités pour chaque binaire. Ce script regroupe les imports selon les catégories du tableau ci-dessus :

    python
    # capimports.py — group a PE's imports into capability buckets
    import re, sys
    import pefile
    
    BUCKETS = {
        "file system":  r"^(CreateFile|ReadFile|WriteFile|DeleteFile|MoveFile|CopyFile|FindFirstFile|FindNextFile)",
        "registry":     r"^Reg(Open|Create|Set|Query|Delete|Enum)",
        "process":      r"^(CreateProcess|OpenProcess|TerminateProcess|CreateToolhelp32Snapshot|Process32|ShellExecute|WinExec)",
        "memory":       r"^(VirtualAlloc|VirtualProtect|WriteProcessMemory|ReadProcessMemory|CreateRemoteThread|QueueUserAPC|SetThreadContext)",
        "network":      r"^(Internet|Http|WinHttp|URLDownload|WSA|socket$|connect$|send$|recv$)",
        "crypto":       r"^(Crypt|BCrypt)",
        "anti-debug":   r"^(IsDebuggerPresent|CheckRemoteDebuggerPresent|NtQueryInformationProcess)",
        "resources":    r"^(FindResource|LoadResource|LockResource|SizeofResource)",
        "dynamic":      r"^(LoadLibrary|GetProcAddress)",
    }
    
    pe = pefile.PE(sys.argv[1])
    hits, total = {}, 0
    for desc in getattr(pe, "DIRECTORY_ENTRY_IMPORT", []):
        dll = desc.dll.decode().lower()
        for imp in desc.imports:
            total += 1
            name = imp.name.decode() if imp.name else f"ord{imp.ordinal}"
            for bucket, rx in BUCKETS.items():
                if re.match(rx, name):
                    hits.setdefault(bucket, []).append(f"{name} ({dll})")
    print(f"{sys.argv[1]}: {total} imports")
    for bucket, names in hits.items():
        print(f"  {bucket:13} {', '.join(names)}")

    Avec un build MinGW récent, la sortie ressemble à ceci :

    text
    regread.exe: 45 imports
      registry      RegCreateKeyExA (advapi32.dll), RegQueryValueExA (advapi32.dll), RegSetValueExA (advapi32.dll)
      memory        VirtualProtect (kernel32.dll)
    httpget.exe: 45 imports
      memory        VirtualProtect (kernel32.dll)
      network       InternetCloseHandle (wininet.dll), InternetOpenA (wininet.dll), InternetOpenUrlA (wininet.dll), InternetReadFile (wininet.dll)
    proclist.exe: 45 imports
      process       CreateToolhelp32Snapshot (kernel32.dll), Process32First (kernel32.dll), Process32Next (kernel32.dll)
      memory        VirtualProtect (kernel32.dll)

    Les nombres exacts dépendent de votre version de MinGW et du runtime C.

  4. Lancez maintenant capa sur chaque binaire et comparez avec vos hypothèses :

    bash
    capa httpget.exe

    Pour httpget.exe, le tableau des capacités contient des entrées comme :

    text
    receive data                            communication
    create HTTP request                      communication/http/client
    allocate or change RWX memory           host-interaction/process/inject
    terminate process                       host-interaction/process/terminate
    enumerate PE sections                   load-code/pe
    parse PE header                         load-code/pe

    regread.exe ajoute « query or enumerate registry value » et « set registry value » (ATT&CK T1012, Query Registry), et proclist.exe ajoute « enumerate processes ».

  5. Examinez la surprise. Les trois programmes — et un simple « hello world » — déclenchent « allocate or change RWX memory » dans l’espace de noms inject, ainsi que « parse PE header ». Lancez capa -v httpget.exe, notez les adresses de fonctions associées à cette règle, et vérifiez dans Ghidra ou n’importe quel désassembleur qu’elles appartiennent au code de démarrage du runtime MinGW (qui appelle VirtualProtect avec PAGE_EXECUTE_READWRITE, 0x40), et non à main.

  6. Packez un binaire avec UPX et répétez les étapes 3 et 4 :

    bash
    upx -o httpget-upx.exe httpget.exe
    python3 capimports.py httpget-upx.exe
    capa httpget-upx.exe

    Les imports WinINet disparaissent de la table, et capa avertit que le fichier semble packé.

Questions : Lesquelles de vos hypothèses d’une ligne étaient justes, et lesquelles capa a-t-il affinées avec des preuves tirées du code ? Pourquoi une règle limitée à une seule fonction produirait-elle moins de faux positifs qu’une vérification de la table des imports ? Comment formuleriez-vous le constat RWX dans un rapport pour qu’un lecteur ne confonde pas le bruit du runtime avec une injection ?

À retenir

  • Les imports décrivent ce qu’un programme peut demander à Windows ; regroupez-les en catégories de comportement et rédigez une courte hypothèse de capacités.
  • Des combinaisons comme OpenProcess → VirtualAllocEx → WriteProcessMemory → CreateRemoteThread portent bien plus de signal que n’importe quelle API isolée.
  • A/W, Ex, Nt/Zw et les ordinaux modifient tous ce qu’un import vous apprend.
  • Une table des imports minuscule avec LoadLibrary/GetProcAddress signifie que les vrais imports sont cachés — par packing, résolution dynamique ou hachage d’API.
  • capa confirme les capacités avec des preuves au niveau du code et les rattache à ATT&CK et MBC, mais le code du runtime et des bibliothèques produit aussi des correspondances : vérifiez où se trouve chaque correspondance avant de la signaler.