capa-real, un motor file-scope que reimplementé dentro de APT115 —así se ve, en concreto, qué de todo esto se puede leer sin desensamblar y qué no—. Esta Parte 0 pone el marco: la anatomía de una regla y el mapa de la serie.Qué problema resuelve capa#
Cuando cae un binario desconocido en la mesa de triage, la primera pregunta no es “¿qué familia es?” ni “¿es malicioso?”. Es más humilde y más útil: ¿qué es capaz de hacer esto?. ¿Habla por HTTP? ¿Escribe en una clave de arranque del registro? ¿Cifra ficheros? ¿Se da cuenta de que corre en una máquina virtual? Responder eso a mano exige abrir el binario en un desensamblador y reconocer, uno por uno, los patrones de código que delatan cada comportamiento. Es trabajo experto, lento, y no escala.
capa —de capabilities— es la herramienta de Mandiant (hoy Google) que automatiza esa lectura. Le das un ejecutable y te devuelve una lista de capacidades en lenguaje humano, cada una anclada a dónde en el código aparece y etiquetada con su técnica de MITRE ATT&CK y su comportamiento de MBC:
capa suspicious.exe
+------------------------+--------------------------------------------------+
| ATT&CK Tactic | ATT&CK Technique |
|------------------------+--------------------------------------------------|
| DEFENSE EVASION | Obfuscated Files or Information::Software Packing |
| PRIVILEGE ESCALATION | Process Injection::Thread Execution Hijacking |
| ... | ... |
+------------------------+--------------------------------------------------+
capability namespace
-------------------------------------------- ------------------------------------
packed with UPX anti-analysis/packer/upx
inject thread host-interaction/process/inject
encrypt data using RC4 PRGA data-manipulation/encryption/rc4
...
Lo interesante para nosotros no es la salida. Es el motor de reglas que la produce. capa no adivina: cada línea de esa tabla salió de que una regla .yml del ruleset abierto coincidió con el binario. Ese ruleset es capa-rules, y es un objeto de estudio precioso: mil reglas escritas por analistas de malware que codifican, de forma legible y versionada, el conocimiento de “cómo se ve tal comportamiento en el código”. Leerlas es leer, destilado, el oficio del reversing.
Una regla, por dentro#
Empecemos por la más limpia posible. Esta detecta si un binario está comprimido con UPX, el packer libre más común. Es una regla real de capa-rules, entera:
rule:
meta:
name: packed with UPX
namespace: anti-analysis/packer/upx
authors:
- william.ballenthin@mandiant.com
scopes:
static: file
dynamic: file
att&ck:
- Defense Evasion::Obfuscated Files or Information::Software Packing [T1027.002]
mbc:
- Anti-Static Analysis::Software Packing::UPX [F0001.008]
examples:
- CD2CBA9E6313E8DF2C1273593E649682
- Practical Malware Analysis Lab 01-02.exe_:0x0401000
features:
- or:
- and:
- format: pe
- or:
- section: UPX0
- section: UPX1
- and:
- format: elf
- or:
- string: "UPX!"
Regla anti-analysis/packer/upx — capa-rules (Mandiant, Apache-2.0).
Toda regla de capa tiene la misma forma: un bloque meta (metadatos: qué es, quién la escribió, a qué se mapea) y un bloque features (la lógica: qué tiene que verse en el binario para que la regla coincida). Vamos por partes.
El bloque meta#
| Campo | Qué es |
|---|---|
name | El nombre humano de la capacidad. Es lo que ves en la salida de capa. |
namespace | La ruta jerárquica que la clasifica (anti-analysis/packer/upx). Ordena el ruleset como un árbol de carpetas y agrupa capacidades afines. |
authors | Quién la escribió. capa-rules es colaborativo; cada regla lleva firma. |
scopes | El campo decisivo. Dónde mira capa para evaluar esta regla: static (análisis estático) y dynamic (traza de ejecución). Aquí, ambos son file. Volvemos a esto en detalle abajo —es lo que decide qué se puede leer sin desensamblar—. |
att&ck | La técnica de MITRE ATT&CK que representa. Aquí T1027.002 Software Packing. |
mbc | El Malware Behavior Catalog: un catálogo hermano de ATT&CK, más granular para comportamientos de malware. Aquí F0001.008 Software Packing::UPX. |
references | (No presente aquí.) Enlaces a papers o artículos que documentan la técnica. |
examples | Muestras reales donde la regla coincide, en formato hash:offset. |
Deténganse en ese último campo, porque tiene un detalle encantador. Uno de los ejemplos es:
Practical Malware Analysis Lab 01-02.exe_:0x0401000
Es un binario de laboratorio de Practical Malware Analysis (Sikorski & Honig, 2012), el libro que enseñó a reversear malware a media industria. Muchísimas reglas de capa citan como ejemplo canónico un lab del PMA: el corpus de reglas y la bibliografía clásica están entretejidos. capa no solo detecta comportamientos; te deja un rastro de migas hacia el material didáctico que los explica. Es una de esas señales de que el ruleset lo escribieron docentes del oficio, no solo ingenieros.
El bloque features: un árbol lógico#
features es un árbol de lógica booleana. Las hojas son observaciones concretas sobre el binario; los nodos internos son operadores que las combinan. La regla coincide si el árbol, evaluado de la raíz hacia abajo, da verdadero. La de UPX se lee así:
flowchart TD
OR{"or"} --> A["and"]
OR --> B["and"]
A --> A1["format: pe"]
A --> A2{"or"}
A2 --> A2a["section: UPX0"]
A2 --> A2b["section: UPX1"]
B --> B1["format: elf"]
B --> B2["string: UPX!"]En prosa: «es un PE y tiene una sección llamada UPX0 o UPX1» o bien «es un ELF y contiene la cadena UPX!». Nada más. Ninguna de esas observaciones necesita seguir el flujo del programa: el formato del fichero, los nombres de sección y las cadenas se leen del binario en reposo. Por eso su scope es file —y por eso, como veremos en la serie, un motor que no desensambla puede evaluarla exacta—.
Fíjense en la economía. Un packer, por definición, deja el binario ilegible: comprime el código real y antepone un stub descompresor. capa no intenta descomprimir nada; le basta el rastro estructural que UPX olvida limpiar —los nombres de sección que crea, o su marca UPX!—. Toda buena regla de capa es un ejercicio de este tipo: encontrar la evidencia mínima e inevitable de un comportamiento, la que el atacante no puede quitar sin romper lo que quería hacer.
La gramática de las reglas#
Ese árbol usa solo or y and, pero la gramática de features tiene más operadores. Son pocos y conviene tenerlos claros, porque toda la serie los usa:
and/or— la lógica booleana de siempre.andexige todos sus hijos;or, al menos uno.not— niega. Aparece, por ejemplo, en reglas de criptografía que exigen la ausencia de ciertas instrucciones para descartar otro algoritmo parecido.N or more— coincide si al menos N de sus hijos coinciden. Sirve para contar: «dos o más llamadas amovzx», «tres o más de estas cadenas». Es lo que da robustez a las reglas frente a variantes.optional— un hijo descriptivo que documenta lo que suele acompañar al comportamiento, pero no afecta al resultado del match. Es un matiz sutil con mucho peso, y volveremos a él: cuando un requisito duro está bajooptional, deja de ser obligatorio. (En la Parte 2 veremos cómo inject APC pasa el corte precisamente porque su único requisito difícil está marcadooptional, mientras que inject thread, casi idéntica, no lo pasa.)match— composición. Una regla puede exigir que otra regla coincida como uno de sus hijos. Así capa construye capacidades complejas encadenando primitivas simples: inject thread hacematch: allocate or change RWX memory, que a su vez hacematch: allocate memory. Es la clave para entender por qué parchear una regla no rompe las que dependen de ella —y por qué una capacidad compuesta puede escaparse aunque sus ingredientes se detecten sueltos—.
Las hojas: qué puede observar una regla#
En el fondo del árbol están las features: los hechos concretos que capa extrae del binario. Son el vocabulario con el que se escribe todo. Los que importan:
| Feature | Qué observa | Necesita desensamblar |
|---|---|---|
api | Una función importada o llamada (kernel32.VirtualAlloc). | No (import) / sí (llamada interna) |
string | Una cadena, exacta o como regex /.../i. | No |
substring | Un fragmento de cadena. | No |
bytes | Una secuencia de bytes literal (una clave, una tabla-S). | No |
number | Un operando inmediato: una constante en una instrucción (0x40). | Sí |
offset | Un desplazamiento de estructura (offset 0x40 de un campo). | Sí |
mnemonic | Una instrucción por su nombre (div, movzx). | Sí |
instruction | Una instrucción con sus operandos concretos. | Sí |
characteristic | Una propiedad estructural del código: nzxor, tight loop, peb access, stack string… | Sí |
format · os · arch | El formato del fichero, el SO y la arquitectura de destino. | No |
section · export | Un nombre de sección o de símbolo exportado. | No |
La columna de la derecha es, adelantándonos, el corazón de toda la serie. Las features de la mitad de arriba —cadenas, bytes, imports, formato, secciones— se leen del binario en reposo. Las de abajo —constantes numéricas, mnemónicos, instrucciones, características estructurales— solo tienen sentido una vez reconstruido el flujo del programa: hay que saber qué es código y qué es dato, dónde empiezan las funciones, cómo se encadenan los bloques. Esa línea divisoria es física, no de dificultad: no se puede ver un 0x40 “dentro de una instrucción” sin antes decidir dónde está esa instrucción.
Los cuatro scopes: dónde mira capa#
Lo que decide de qué lado de esa línea cae una regla es su scope. capa evalúa cada regla en una de cuatro granularidades, de la más gruesa a la más fina:
flowchart LR
F["file
todo el binario"] --> Fu["function
una función"]
Fu --> BB["basic block
un tramo lineal
sin saltos"]
BB --> I["instruction
una instrucción"]file— la regla mira el binario completo: sus imports, todas sus cadenas, sus secciones, su cabecera. No necesita saber nada del flujo. packed with UPX es file-scope.function— la regla mira dentro de una función: exige que ciertas features coincidan en la misma función. Esto ya requiere haber identificado las funciones, es decir, desensamblar y reconstruir el grafo de llamadas.basic block— más fino aún: las features deben coincidir dentro de un mismo bloque básico (un tramo de código lineal, sin saltos que entren o salgan en el medio). Es donde viven muchos patrones de criptografía.instruction— el máximo detalle: la regla mira una instrucción y sus operandos.
Que una regla exija que dos cosas ocurran en la misma función o en el mismo bloque no es un tecnicismo: es lo que le da precisión. Un binario cualquiera importa VirtualAlloc y en algún lado tiene la constante 0x40; eso no dice nada. Pero VirtualAlloc llamada con 0x40 (PAGE_EXECUTE_READWRITE) en el mismo bloque ya es un indicio fuerte de que alguien reserva memoria ejecutable-y-escribible para meter shellcode. El scope es lo que convierte coincidencias sueltas en evidencia.
Cómo se reparte el corpus#
Aquí está el dato que fija el terreno de toda la serie. De las 1052 reglas con nombre del ruleset (aed45e2, 19-jun-2026), por scope estático:
| Scope | Reglas | Proporción |
|---|---|---|
function | 660 | 62,7 % |
file | 192 | 18,3 % |
basic block | 171 | 16,3 % |
instruction | 29 | 2,8 % |
Solo el 18 % de las reglas son file-scope. El otro 82 % vive dentro de funciones y bloques: para evaluarlas hay que desensamblar el binario y reconstruir su grafo de control (capa se apoya en el CFG que le da vivisect). Esa asimetría no es un defecto de capa; es un reflejo de la realidad. Los comportamientos más interesantes —inyección, criptografía, anti-debugging— no viven en una cadena suelta: viven en cómo se combinan las instrucciones. Y “cómo se combinan” es, por definición, algo que solo se ve tras reconstruir el flujo.
T####)—. MBC (Malware Behavior Catalog) responde «¿qué comportamiento de malware es, a bajo nivel?» —su vocabulario es el del analista: Software Packing, Encrypt Data, Check for Debugger (B/C/E/F####)—. MBC es más fino justo donde ATT&CK es grueso: distingue RC4 de AES, o check for VM de check for debugger, cosas que a ATT&CK le dan igual. Una regla suele llevar ambas porque contestan preguntas distintas sobre el mismo hecho.Segunda regla: el mismo esqueleto, otra intención#
Para fijar el patrón, una segunda regla real —el sello del ransomware, borrar las instantáneas de volumen para que no puedas restaurar tus ficheros—:
rule:
meta:
name: delete volume shadow copies
namespace: impact/inhibit-system-recovery
authors:
- moritz.raabe@mandiant.com
scopes:
static: function
dynamic: span of calls
att&ck:
- Impact::Inhibit System Recovery [T1490]
- Defense Evasion::Indicator Removal::File Deletion [T1070.004]
mbc:
- Impact::Data Destruction::Delete Shadow Copies [E1485.m04]
examples:
- B87E9DD18A5533A09D3E48A7A1EFBCF6:0x140006AF0
features:
- or:
- string: /vssadmin.* delete shadows/i
- string: /vssadmin.* resize shadowstorage/i
- string: /wmic.* shadowcopy delete/i
Regla impact/inhibit-system-recovery/delete-volume-shadow-copies — capa-rules (Mandiant, Apache-2.0).
El mismo esqueleto: meta + features. Dos técnicas de ATT&CK esta vez —T1490
Inhibit System Recovery y T1070.004
File Deletion— porque el acto sirve a dos tácticas a la vez. Y las features son tres regex de cadenas: los comandos que un atacante ejecuta para arrasar las copias de sombra (vssadmin delete shadows, wmic shadowcopy delete). El /i las hace insensibles a mayúsculas; el .* absorbe los argumentos intermedios.
Dos detalles la separan de la de UPX y valen oro:
- Su scope estático es
function, nofile. La regla no quiere la cadenavssadmin delete shadowsen cualquier parte del binario —eso lo tendría hasta un manual—: la quiere dentro de una función, junto al código que la ejecuta. Es la diferencia entre “el binario menciona vssadmin” y “el binario usa vssadmin”. Ese matiz de scope es, otra vez, precisión. - Su scope dinámico es
span of calls. capa no solo analiza binarios en reposo; también lee trazas de ejecución. Ahí no hay funciones sino secuencias de llamadas, y el scope se expresa en esos términos. La serie se centra en el análisis estático, pero conviene saber que cada regla declara ambos mundos.
Aunque su scope sea function, esta regla en la práctica es evaluable sin desensamblar: sus únicas hojas son cadenas, y las cadenas se leen del binario entero. Es el primer atisbo de un matiz que recorrerá toda la serie —una regla de función cuyas features son todas “de fichero” se puede aproximar— y nos lleva directo a la implementación.
Del papel al motor: la frontera file-scope#
Leer reglas está muy bien, pero la mejor forma de entender un formato es implementar su intérprete. Dentro de APT115 —mi herramienta de triage en el navegador— reimplementé un subconjunto del motor de capa al que llamo capa-real. La restricción que lo define es dura y deliberada: capa-real no desensambla nada. Solo dispone de lo que un triage estático extrae de un binario en reposo —imports, cadenas, bytes, formato, SO, arquitectura, secciones, exports— exactamente las features de la mitad de arriba de aquella tabla.
Con esa restricción, capa-real compila el ruleset y clasifica cada regla en tres cubos:
- Exacta — todas sus hojas son file-scope (cadenas, bytes, imports, secciones…) y su scope es
file. capa-real la evalúa idéntica a capa. packed with UPX cae aquí. - Aproximada — su scope es más fino (
function/basic block), pero todas sus hojas resultan ser features de fichero. capa-real la evalúa degradando el scope: exige que las features aparezcan en el binario, sin poder confirmar que están en la misma función. Gana cobertura, cede algo de precisión. delete volume shadow copies cae aquí. - Fuera de alcance — depende de al menos una feature que exige desensamblar: un
number, unmnemonic, uncharacteristic. capa-real la descarta antes de evaluar, honestamente, en vez de fingir un veredicto.
El resultado de pasar el ruleset entero por ese filtro —el gate— es el número que da forma a toda la serie:
Esa frontera —qué cae de cada lado y por qué— es el hilo conductor. En cada parte, después de desarmar un grupo de reglas, cerraré con un veredicto «En capa-real»: ¿la caza exacta, la aproxima, o necesita desensamblar? Y el porqué siempre será concreto. Ya se intuye la forma de las respuestas: los packers y los detectores de VM por cadenas caerán exactos; la inyección de hilos se escapará porque su corazón es un number: 0x40; el RC4 se escapará porque se reconoce por la forma de sus instrucciones, no por ninguna API. La Parte 5 abre el motor entero: el compilador, el evaluador del árbol y los gotchas de implementación.
El mapa de la serie#
Seis entregas, ordenadas por el kill chain. Cada una es autocontenida —puedes entrar por la que te interese— y todas comparten el tag capa.
- Parte 0 — Anatomía de una regla (esta). El formato, la gramática, los scopes y la frontera file-scope.
- Parte 1 — Anti-análisis y loaders. Detección de VM y debuggers, timing, resolución dinámica de APIs, y el ejemplo perfecto de por qué a veces hace falta desensamblar: encrypt data using RC4, un algoritmo que se reconoce por la forma de sus instrucciones.
- Parte 2 — Inyección y persistencia. inject thread desarmada línea por línea, hollowing, APC; claves Run, servicios, tareas programadas. El contraste fuerte entre detectar los ingredientes y detectar la técnica compuesta.
- Parte 3 — Evasión, credenciales y descubrimiento. AMSI/ETW tampering, borrar logs, acceso a LSASS y a credenciales de navegador y de la nube, recon del sistema.
- Parte 4 — C2, colección e impacto. HTTP/socket/DNS, keylogging, captura de pantalla, ransomware, y las huellas de compiladores y packers donde capa-real brilla al 100 %.
- Parte 5 — De la regla al motor. El interior de
capa-real: el compilador, el gate del 57,9 %, el evaluador del árbol lógico, y los límites honestos de leer comportamiento sin un CFG.
Referencias#
- capa (Mandiant/Google) — repositorio: https://github.com/mandiant/capa
- capa-rules (Apache-2.0) — el ruleset citado en esta serie: https://github.com/mandiant/capa-rules
- capa rules format — especificación del formato
.yml: https://github.com/mandiant/capa-rules/blob/master/doc/format.md - MITRE ATT&CK: https://attack.mitre.org/
- Malware Behavior Catalog (MBC): https://github.com/MBCProject/mbc-markdown
- vivisect — el motor de análisis binario en el que capa apoya el CFG: https://github.com/vivisect/vivisect
- Sikorski & Honig — Practical Malware Analysis (No Starch, 2012): https://nostarch.com/malware