Import Address Table: APIs Sospechosas y Resolucion Dinamica
Analisis de la Import Address Table (IAT) en archivos PE. APIs sospechosas de Windows agrupadas por comportamiento, resolucion dinamica con GetProcAddress y LoadLibrary, API hashing y tecnicas de evasion.
La IAT como declaracion de intenciones
Si las secciones son la radiografia de un PE, la Import Address Table es su declaracion de intenciones. Cada funcion importada revela una capacidad: leer archivos, conectarse a la red, manipular procesos, modificar el registro. Un binario que importa CreateRemoteThread, VirtualAllocEx y WriteProcessMemory esta practicamente confesando que inyecta codigo.
El problema es que el malware lo sabe. Por eso las familias modernas minimizan sus imports estaticos y resuelven el resto en runtime. Entender tanto la IAT como las tecnicas de resolucion dinamica es fundamental para el analisis.
Estructura de la Import Table
La Import Table del PE se compone de varias estructuras interrelacionadas:
Import Directory Table: Un array de estructuras IMAGE_IMPORT_DESCRIPTOR, una por cada DLL importada. El array termina con una entrada llena de ceros.
Cada descriptor contiene:
| Campo | Descripcion |
|---|---|
| OriginalFirstThunk | RVA al Import Lookup Table (ILT) |
| TimeDateStamp | Timestamp de binding (0 si no bound) |
| ForwarderChain | Indice de forwarded imports (-1 si ninguno) |
| Name | RVA al string con nombre de la DLL |
| FirstThunk | RVA a la Import Address Table (IAT) |
Import Lookup Table (ILT): Array de entradas que identifican cada funcion importada, por nombre o por ordinal.
Import Address Table (IAT): Inicialmente identica a la ILT, pero cuando el PE se carga en memoria, el loader de Windows sustituye cada entrada con la direccion real de la funcion.
import pefile
pe = pefile.PE("sample.exe")
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll_name = entry.dll.decode()
print("\n[" + dll_name + "]")
for imp in entry.imports:
if imp.name:
print(" " + imp.name.decode())
else:
print(" Ordinal:", imp.ordinal)
Catalogo de APIs sospechosas por categoria
Inyeccion de procesos
La inyeccion de codigo en procesos remotos es una de las tecnicas mas comunes del malware. Las APIs clave forman patrones reconocibles:
| API | DLL | Funcion |
|---|---|---|
| OpenProcess | kernel32 | Obtener handle a proceso objetivo |
| VirtualAllocEx | kernel32 | Reservar memoria en proceso remoto |
| WriteProcessMemory | kernel32 | Escribir codigo/datos en el proceso |
| CreateRemoteThread | kernel32 | Ejecutar el codigo inyectado |
| NtCreateThreadEx | ntdll | Alternativa a CreateRemoteThread |
| RtlCreateUserThread | ntdll | Otra alternativa para crear threads |
| QueueUserAPC | kernel32 | Inyeccion via APC (asincrona) |
| NtQueueApcThread | ntdll | Version nativa de QueueUserAPC |
| SetWindowsHookEx | user32 | Inyeccion via hooks de ventana |
Patron clasico de inyeccion:
- OpenProcess para obtener handle al proceso objetivo
- VirtualAllocEx para reservar memoria con permisos PAGE_EXECUTE_READWRITE
- WriteProcessMemory para copiar el shellcode
- CreateRemoteThread para ejecutar
Manipulacion de memoria
| API | DLL | Indicador |
|---|---|---|
| VirtualAlloc | kernel32 | Reservar memoria ejecutable |
| VirtualProtect | kernel32 | Cambiar permisos de memoria (RW a RX) |
| HeapCreate | kernel32 | Crear heap privado |
| NtAllocateVirtualMemory | ntdll | Alternativa nativa a VirtualAlloc |
| NtProtectVirtualMemory | ntdll | Alternativa nativa a VirtualProtect |
La combinacion VirtualAlloc + VirtualProtect es clasica del desempaquetado: el malware reserva memoria con permisos RW, escribe el codigo desempaquetado, y luego cambia los permisos a RX para ejecutarlo.
Operaciones de archivo y persistencia
| API | DLL | Indicador |
|---|---|---|
| CreateFile | kernel32 | Crear o abrir archivos |
| WriteFile | kernel32 | Escribir contenido |
| CopyFile | kernel32 | Copiar el propio ejecutable |
| MoveFile | kernel32 | Mover archivos |
| DeleteFile | kernel32 | Eliminar archivos (cubrir rastros) |
| GetTempPath | kernel32 | Obtener directorio temporal |
| CreateDirectory | kernel32 | Crear directorios |
Registro de Windows (persistencia)
| API | DLL | Indicador |
|---|---|---|
| RegOpenKeyEx | advapi32 | Abrir clave de registro |
| RegSetValueEx | advapi32 | Escribir valor (persistencia) |
| RegCreateKeyEx | advapi32 | Crear nueva clave |
| RegDeleteKey | advapi32 | Eliminar clave (limpieza) |
Las claves de Run y RunOnce son los objetivos clasicos para persistencia:
- HKCU\Software\Microsoft\Windows\CurrentVersion\Run
- HKLM\Software\Microsoft\Windows\CurrentVersion\Run
Red y comunicacion C2
| API | DLL | Indicador |
|---|---|---|
| WSAStartup | ws2_32 | Inicializar Winsock |
| socket | ws2_32 | Crear socket de red |
| connect | ws2_32 | Conectar a servidor remoto |
| send / recv | ws2_32 | Enviar/recibir datos |
| InternetOpen | wininet | Inicializar sesion HTTP |
| InternetOpenUrl | wininet | Abrir URL |
| HttpOpenRequest | wininet | Crear request HTTP |
| HttpSendRequest | wininet | Enviar request HTTP |
| URLDownloadToFile | urlmon | Descargar archivo de URL |
| WinHttpOpen | winhttp | Alternativa a WinINet |
URLDownloadToFile es especialmente sospechosa porque descarga y escribe un archivo en una sola llamada. Es la API favorita de droppers simples.
Criptografia
| API | DLL | Indicador |
|---|---|---|
| CryptAcquireContext | advapi32 | Obtener proveedor criptografico |
| CryptEncrypt | advapi32 | Cifrar datos |
| CryptDecrypt | advapi32 | Descifrar datos |
| CryptGenKey | advapi32 | Generar clave |
| CryptImportKey | advapi32 | Importar clave publica |
| BCryptEncrypt | bcrypt | API criptografica moderna |
En ransomware, la presencia de CryptEncrypt o BCryptEncrypt junto con CryptImportKey (para importar la clave publica RSA del atacante) es una senal definitiva.
Enumeracion del sistema
| API | DLL | Indicador |
|---|---|---|
| GetSystemInfo | kernel32 | Informacion del sistema |
| GetComputerName | kernel32 | Nombre del equipo |
| GetUserName | advapi32 | Nombre del usuario |
| GetVersionEx | kernel32 | Version de Windows |
| CreateToolhelp32Snapshot | kernel32 | Enumerar procesos |
| Process32First / Next | kernel32 | Iterar procesos |
| IsDebuggerPresent | kernel32 | Detectar debugger |
| CheckRemoteDebuggerPresent | kernel32 | Detectar debugger remoto |
| GetTickCount | kernel32 | Anti-sandbox (timing) |
Ejecucion y servicios
| API | DLL | Indicador |
|---|---|---|
| CreateProcess | kernel32 | Ejecutar otro proceso |
| ShellExecute | shell32 | Ejecutar con shell |
| WinExec | kernel32 | Ejecutar comando (legacy) |
| CreateService | advapi32 | Crear servicio Windows |
| StartService | advapi32 | Iniciar servicio |
| CreateScheduledTask | taskschd | Crear tarea programada |
Resolucion dinamica: LoadLibrary y GetProcAddress
Cuando un PE importa estaticamente una funcion, esta aparece en la IAT y es visible con cualquier herramienta. El malware evita esto resolviendo funciones en runtime:
// Pseudocodigo de resolucion dinamica
HMODULE hKernel32 = LoadLibraryA("kernel32.dll")
FARPROC pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAllocEx")
FARPROC pWriteMemory = GetProcAddress(hKernel32, "WriteProcessMemory")
FARPROC pCreateThread = GetProcAddress(hKernel32, "CreateRemoteThread")
Deteccion en analisis estatico:
Si la IAT de un PE solo contiene unas pocas funciones y entre ellas estan LoadLibraryA/W y GetProcAddress, es casi seguro que resuelve el resto dinamicamente.
import pefile
pe = pefile.PE("sample.exe")
total_imports = 0
has_loadlibrary = False
has_getprocaddress = False
for entry in pe.DIRECTORY_ENTRY_IMPORT:
for imp in entry.imports:
total_imports += 1
if imp.name:
name = imp.name.decode()
if "LoadLibrary" in name:
has_loadlibrary = True
if "GetProcAddress" in name:
has_getprocaddress = True
print("Total imports:", total_imports)
print("Tiene LoadLibrary:", has_loadlibrary)
print("Tiene GetProcAddress:", has_getprocaddress)
if total_imports < 20 and has_loadlibrary and has_getprocaddress:
print("[!] Probable resolucion dinamica de APIs")
API Hashing: el siguiente nivel de ocultacion
La resolucion dinamica con GetProcAddress requiere strings con los nombres de las APIs en el binario, que pueden encontrarse con herramientas como strings o FLOSS. El API hashing elimina estos strings.
Como funciona:
- El malware calcula un hash (CRC32, djb2, ROR13, custom) de cada nombre de API que necesita.
- Almacena solo los hashes en su codigo (valores numericos de 32 bits).
- En runtime, itera sobre las exports de la DLL, calcula el hash de cada nombre y compara.
- Cuando encuentra coincidencia, obtiene la direccion de la funcion.
Algoritmos de hash comunes:
// ROR13 (usado por Metasploit y muchos frameworks)
hash = 0
for each char in function_name:
hash = rotate_right(hash, 13) + char
// djb2 (usado por multiples familias)
hash = 5381
for each char in function_name:
hash = hash * 33 + char
// CRC32 (usado por varios RATs)
hash = crc32(function_name)
Deteccion de API hashing:
- Buscar constantes conocidas: 5381 (djb2), 0xEDB88320 (CRC32 polynomial)
- Buscar patrones de rotacion (ROR, ROL) en loops
- Usar HashDB de OALabs para identificar hashes conocidos
- Buscar acceso al PEB (Process Environment Block) para resolver modulos
// Acceso al PEB en x86 para obtener lista de modulos cargados
mov eax, fs:[0x30] // PEB
mov eax, [eax + 0x0C] // PEB_LDR_DATA
mov eax, [eax + 0x14] // InMemoryOrderModuleList
Imports por ordinal: funciones sin nombre
Algunas DLLs exportan funciones solo por numero ordinal (sin nombre). El malware puede importar por ordinal para dificultar el analisis:
for entry in pe.DIRECTORY_ENTRY_IMPORT:
for imp in entry.imports:
if imp.name is None and imp.ordinal is not None:
print(
"Import por ordinal:",
entry.dll.decode(),
"#" + str(imp.ordinal)
)
Para resolver que funcion corresponde a cada ordinal, se necesita la Export Table de la DLL correspondiente o bases de datos de ordinales conocidos.
Bound imports y delayed imports
Bound imports
Los bound imports son una optimizacion donde el linker pre-resuelve las direcciones de las funciones importadas basandose en la version de la DLL disponible en tiempo de compilacion. Si la DLL no ha cambiado, el loader no necesita resolver las direcciones.
En el contexto de malware, los bound imports pueden revelar la version exacta del sistema operativo donde se compilo el binario.
Delayed imports
Los delayed imports son funciones que no se resuelven al cargar el PE, sino la primera vez que se llaman. Windows usa un stub de delayed loading que llama a LoadLibrary y GetProcAddress automaticamente.
El malware usa delayed imports para:
- Evitar errores si una DLL no existe en la version de Windows del objetivo
- Cargar DLLs sospechosas solo cuando realmente las necesita
- Dificultar el analisis estatico (las imports no aparecen en la tabla principal)
if hasattr(pe, "DIRECTORY_ENTRY_DELAY_IMPORT"):
for entry in pe.DIRECTORY_ENTRY_DELAY_IMPORT:
print("[Delayed]", entry.dll.decode())
for imp in entry.imports:
if imp.name:
print(" ", imp.name.decode())
Forwarded exports: la cadena de redireccion
Una DLL puede "forwardear" un export a otra DLL. Por ejemplo, kernel32.dll forwarda muchas funciones a ntdll.dll internamente. El malware puede abusar de este mecanismo:
- Crear una DLL que forwarda la mayoria de funciones a la DLL original pero intercepta algunas (DLL proxying/hijacking).
- Las funciones forwardeadas no contienen codigo en la DLL que las exporta, solo un string como "ntdll.RtlAllocateHeap".
Perfiles de imports por tipo de malware
Diferentes categorias de malware tienen perfiles de imports caracteristicos:
Dropper
kernel32.dll:
CreateFile, WriteFile, GetTempPath
CreateProcess, WinExec
LoadLibrary, GetProcAddress
urlmon.dll:
URLDownloadToFile
Patron: descarga un archivo, lo guarda en %TEMP% y lo ejecuta.
RAT (Remote Access Trojan)
kernel32.dll:
CreateProcess, CreatePipe, ReadFile, WriteFile
GetComputerName, GetUserName
ws2_32.dll:
WSAStartup, socket, connect, send, recv
advapi32.dll:
RegSetValueEx (persistencia)
shell32.dll:
ShellExecute
user32.dll:
GetDesktopWindow, GetForegroundWindow
keybd_event (keylogger)
Patron: comunicacion de red + ejecucion de comandos + captura de informacion.
Ransomware
kernel32.dll:
FindFirstFile, FindNextFile (enumerar archivos)
CreateFile, ReadFile, WriteFile
MoveFile, DeleteFile
advapi32.dll:
CryptAcquireContext, CryptImportKey
CryptEncrypt, CryptGenKey
RegSetValueEx
shell32.dll:
ShellExecute (ejecutar nota de rescate)
Patron: enumerar archivos + cifrar + persistencia + mostrar nota.
Infostealer
kernel32.dll:
CreateToolhelp32Snapshot, Process32First
ReadProcessMemory
crypt32.dll:
CryptUnprotectData (descifrar credenciales Chrome)
advapi32.dll:
CredEnumerate (credenciales Windows)
wininet.dll:
InternetOpen, HttpSendRequest (exfiltracion)
Patron: enumerar procesos + robar credenciales + exfiltrar via HTTP.
Script de analisis de imports con scoring
import pefile
import sys
SUSPICIOUS_APIS = dict(
# Inyeccion de procesos
VirtualAllocEx=("injection", 8),
WriteProcessMemory=("injection", 9),
CreateRemoteThread=("injection", 9),
NtCreateThreadEx=("injection", 9),
QueueUserAPC=("injection", 7),
SetWindowsHookEx=("injection", 6),
# Manipulacion de memoria
VirtualAlloc=("memory", 3),
VirtualProtect=("memory", 5),
# Red
URLDownloadToFile=("network", 8),
InternetOpen=("network", 4),
HttpSendRequest=("network", 5),
WSAStartup=("network", 3),
# Persistencia
RegSetValueEx=("persistence", 6),
CreateService=("persistence", 7),
# Criptografia
CryptEncrypt=("crypto", 6),
CryptImportKey=("crypto", 7),
# Anti-analisis
IsDebuggerPresent=("anti_analysis", 5),
CheckRemoteDebuggerPresent=("anti_analysis", 6),
# Ejecucion
CreateProcess=("execution", 4),
ShellExecute=("execution", 4),
WinExec=("execution", 7),
# Resolucion dinamica
GetProcAddress=("dynamic_resolution", 4),
LoadLibraryA=("dynamic_resolution", 4),
LoadLibraryW=("dynamic_resolution", 4),
)
def analyze_imports(filepath):
pe = pefile.PE(filepath)
findings = []
categories = dict()
total_score = 0
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
for imp in entry.imports:
if imp.name:
name = imp.name.decode()
# Buscar en nombre base (sin sufijo A/W)
base_name = name.rstrip("AW") if name.endswith(("A", "W")) else name
for api_name, info in SUSPICIOUS_APIS.items():
if api_name in name or api_name in base_name:
cat, score = info
findings.append((dll, name, cat, score))
categories[cat] = categories.get(cat, 0) + score
total_score += score
print("=== Import Analysis Report ===")
print("File:", filepath)
print()
if findings:
print("APIs sospechosas encontradas:")
for dll, api, cat, score in sorted(findings, key=lambda x: -x[3]):
print(" [" + str(score) + "] " + dll + " -> " + api + " (" + cat + ")")
print()
print("Score por categoria:")
for cat, score in sorted(categories.items(), key=lambda x: -x[1]):
print(" " + cat + ": " + str(score))
print()
print("Score total:", total_score)
if total_score > 30:
print("VEREDICTO: Altamente sospechoso")
elif total_score > 15:
print("VEREDICTO: Sospechoso, requiere analisis dinamico")
else:
print("VEREDICTO: Bajo riesgo, pero verificar contexto")
else:
print("No se encontraron APIs sospechosas en imports estaticos.")
print("[!] Verificar resolucion dinamica")
if __name__ == "__main__":
analyze_imports(sys.argv[1])
Herramientas especializadas
PEStudio: Resalta automaticamente los imports sospechosos con un sistema de puntuacion integrado.
Dependency Walker (depends.exe): Herramienta clasica para visualizar el arbol de dependencias de un PE. Muestra imports y exports de forma jerarquica.
CFF Explorer: Permite navegar la Import Directory en detalle, incluyendo bound imports y delayed imports.
IDA Pro / Ghidra: Resuelven automaticamente la mayoria de imports y muestran las llamadas a APIs en el codigo desensamblado.
x64dbg: En analisis dinamico, permite poner breakpoints en cualquier API para observar parametros y contexto de llamada.
HashDB (OALabs): Plugin para IDA y Ghidra que identifica API hashes conocidos y los resuelve automaticamente.
Conclusion
La Import Address Table es la ventana mas directa a las capacidades de un binario. Cada API importada cuenta parte de la historia: inyeccion, persistencia, comunicacion de red, cifrado, evasion. Pero el malware moderno oculta sus imports reales con resolucion dinamica, API hashing y delayed loading.
El analista efectivo combina la revision de la IAT con la busqueda de patrones de GetProcAddress, constantes de hashing y accesos al PEB. Cuando el analisis estatico llega a su limite, el analisis dinamico con breakpoints en APIs clave completa el cuadro.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Formato PE de Windows: Estructura Completa del Ejecutable
Secciones PE: .text, .data, .rsrc, .reloc y Anomalias
Tecnicas Anti-Analisis: Anti-Debug, Anti-VM y Anti-Sandbox
Analisis de Binarios .NET: dnSpy, ILSpy y Decompilacion
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.