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.
| Comportement | API typiques | Ce que cela peut indiquer |
|---|---|---|
| Système de fichiers | CreateFileW, WriteFile, DeleteFileW, MoveFileExW, FindFirstFileW, GetTempPathW | Dépôt de fichiers, préparation, énumération de fichiers (ransomwares, stealers) |
| Registre | RegCreateKeyExW, RegSetValueExW, RegQueryValueExW, RegDeleteKeyW | Stockage de configuration, persistance (clés Run), reconnaissance du système |
| Création de processus | CreateProcessW, ShellExecuteExW, WinExec | Lancement de charges utiles ou de commandes |
| Découverte de processus | CreateToolhelp32Snapshot, Process32FirstW, EnumProcesses | Recherche d’outils de sécurité, d’outils d’analyse ou d’un processus cible |
| Mémoire d’autres processus | OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread, QueueUserAPC, SetThreadContext | Injection de code (voir plus bas) |
| Réseau | WinINet (InternetOpenW, InternetOpenUrlW, HttpSendRequestW), WinHTTP (WinHttpConnect), Winsock (socket, connect, send, recv), URLDownloadToFileW | Communication C2, téléchargement d’étapes suivantes, exfiltration |
| Cryptographie | CryptAcquireContextW, CryptEncrypt, BCryptEncrypt, BCryptGenRandom | Chiffrement de la configuration ou du trafic ; un usage massif évoque un ransomware |
| Services | OpenSCManagerW, CreateServiceW, StartServiceW | Persistance ou usage de privilèges via des services |
| Anti-débogage | IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess, OutputDebugStringW | Vérifications de l’environnement qui modifient le comportement pendant l’analyse |
| Saisie et écran | SetWindowsHookExW, GetAsyncKeyState, GetClipboardData, BitBlt | Keylogging, vol du presse-papiers, captures d’écran |
| Ressources propres | FindResourceW, LoadResource, LockResource, SizeofResource | Données ou charge utile embarquées (voir Ressources et overlays) |
| Résolution dynamique | LoadLibraryW, GetProcAddress, LdrGetProcedureAddress | Imports 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 :
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é.CryptEncryptouBCryptEncrypt+FindFirstFile/FindNextFile+MoveFileEx— boucles « chiffrer puis renommer » typiques des ransomwares.RegSetValueExassocié à une chaîne contenantSoftware\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
AetW.CreateFileAprend des chaînes ANSI,CreateFileWprend de l’UTF-16. Le code moderne importe surtout les versionsW; beaucoup d’échantillons écrits à la main importent les versionsA. Le suffixe vous indique aussi quel encodage de chaînes rechercher. - Suffixes
Ex. Une version étendue avec davantage de paramètres —VirtualAllocExestVirtualAllocpour un autre processus, et c’est précisément pour cela qu’elle compte. - Fonctions
Nt/Zwdentdll.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 dentdllcommeNtAllocateVirtualMemory,NtWriteVirtualMemoryouNtQueryInformationProcesssuggè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.dllest le cas classique : l’ordinal 23 estsocket, l’ordinal 115 estWSAStartup. 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 :
KERNEL32.DLL LoadLibraryA, ExitProcess, GetProcAddress, VirtualProtectAvec 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
GetProcAddressavec des noms construits à l’exécution, souvent sous forme de stack strings — voir résolution dynamique des imports. - Le hachage d’API. Même
GetProcAddressdisparaî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 :
- 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.
- 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
VirtualProtectet 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).
-
Créez les programmes.
regread.c— lit (et crée si elle est absente) une valeur sousHKCU\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échargehttp://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; } -
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 -
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.
-
Lancez maintenant capa sur chaque binaire et comparez avec vos hypothèses :
bash capa httpget.exePour
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/peregread.exeajoute « query or enumerate registry value » et « set registry value » (ATT&CK T1012, Query Registry), etproclist.exeajoute « enumerate processes ». -
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 ». Lancezcapa -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 appelleVirtualProtectavecPAGE_EXECUTE_READWRITE, 0x40), et non àmain. -
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.exeLes 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→CreateRemoteThreadportent bien plus de signal que n’importe quelle API isolée. A/W,Ex,Nt/Zwet les ordinaux modifient tous ce qu’un import vous apprend.- Une table des imports minuscule avec
LoadLibrary/GetProcAddresssignifie 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.