La táctica: ejecutar donde no te miran, y volver tras el reinicio#
Dos objetivos distintos, un mismo problema de detección.
Inyección de código (Process Injection, T1055
) es hacer que otro proceso ejecute tu código. Le da al malware dos cosas: sigilo (el código malicioso corre bajo un proceso legítimo, explorer.exe, svchost.exe) y acceso (hereda los privilegios y la confianza de la víctima). La forma clásica tiene tres pasos: reservar memoria en el proceso destino, escribir tu payload en ella, y arrancar un hilo que lo ejecute.
Persistencia (Persistence, táctica TA0003) es sobrevivir al reinicio. El sistema ofrece decenas de ganchos legítimos de auto-arranque —claves del registro, servicios, tareas programadas, carpetas de inicio— y el malware secuestra uno para que el SO lo relance por él.
Lo que ambas comparten, y lo que las hace un caso de estudio perfecto, es que ninguna se reduce a una sola evidencia. “Importa VirtualAlloc” no es inyección —lo hace medio Windows—. “Contiene la cadena de la clave Run” no es persistencia —podría ser un manual—. La señal está en la combinación, y la combinación es justo lo que exige mirar dentro de una función.
inject thread, línea por línea#
Esta es, probablemente, la regla más didáctica de todo capa-rules. Su ejemplo canónico es el Lab 12-01 de Practical Malware Analysis, el ejercicio con el que se enseña inyección de hilos. Entera:
rule:
meta:
name: inject thread
namespace: host-interaction/process/inject
scopes:
static: function
dynamic: span of calls
att&ck:
- Defense Evasion::Process Injection::Thread Execution Hijacking [T1055.003]
- Defense Evasion::Reflective Code Loading [T1620]
examples:
- Practical Malware Analysis Lab 12-01.exe_:0x4010D0
features:
- and:
- or:
- match: allocate or change RWX memory
- match: allocate or change RW memory
- match: write process memory
- match: create thread
- optional:
- or:
- match: host-interaction/process/create
- match: open process
- number: 0x3000 = MEM_COMMIT or MEM_RESERVE
Regla host-interaction/process/inject/inject-thread — capa-rules (Mandiant, Apache-2.0).
La raíz es un and de tres requisitos obligatorios más un optional. Y los tres obligatorios son, todos, match: a otras reglas —es la composición que vimos en la Parte 0—. Para entender el veredicto hay que seguir la cascada. write process memory y create thread son las dos primitivas fáciles; empecemos por ellas.
Los ingredientes que sí se ven#
write process memory es, por dentro, un or limpio de APIs:
rule:
meta:
name: write process memory
scopes:
static: instruction
features:
- or:
- api: kernel32.WriteProcessMemory
- api: ntdll.NtWriteVirtualMemory
- api: ntdll.ZwWriteVirtualMemory
- api: NtWow64WriteVirtualMemory64
Regla lib write-process-memory — capa-rules (Mandiant, Apache-2.0).
Nada más: si el binario importa WriteProcessMemory (o una de sus variantes de ntdll), la primitiva está. create thread es igual de directa —un or de CreateThread, CreateRemoteThread, NtCreateThreadEx, _beginthreadex, y sus equivalentes de Linux (pthread_create) y .NET (Thread::Start)—. Ambas son imports, evidencia de fichero.
write process memory y create thread: APROXIMADAS
— capa-real las evalúa las dos, degradando el scope: no confirma que la llamada ocurra en tal instrucción o bloque, pero sí que la API está en la tabla de imports. Los ingredientes de la inyección se ven.
El ingrediente que no se ve — y arrastra a toda la regla#
El primer requisito obligatorio de inject thread es el que rompe todo. allocate or change RWX memory —reservar o rehabilitar memoria como ejecutable y escribible a la vez (RWX), el sello de “voy a meter código y correrlo”— se modela así:
rule:
meta:
name: allocate or change RWX memory
scopes:
static: basic block
features:
- or:
- basic block:
- and:
- or:
- match: allocate memory
- match: change memory protection
- or:
- number: 0x40 = PAGE_EXECUTE_READWRITE
- instruction:
- mnemonic: lea
- offset: 0x40 = PAGE_EXECUTE_READWRITE
Regla host-interaction/process/inject/allocate-or-change-RWX-memory (recortada) — capa-rules (Mandiant, Apache-2.0).
Aquí está el corazón del asunto. La regla no pide «llama a VirtualAlloc»; pide «llama a VirtualAlloc (o cambia la protección) con la constante 0x40, y las dos cosas en el mismo bloque básico». Ese 0x40 es PAGE_EXECUTE_READWRITE: la bandera que convierte una reserva de memoria corriente en una reserva sospechosa. Y 0x40, como vimos en la Parte 1, es un number —un operando inmediato dentro de una instrucción—, no una cadena ni un import. Para verlo hay que desensamblar. Además, el basic block: exige que la asignación y la bandera coincidan en el mismo tramo de código: un acoplamiento que, por definición, no existe en el binario en reposo.
allocate or change RWX memory: FUERA DE ALCANCE
— Y como es un requisito obligatorio de inject thread (primer miembro del and raíz), su imposibilidad se propaga hacia arriba:
flowchart TD
IT["inject thread
(and)"] --> RWX["allocate or change
RWX memory"]
IT --> WPM["write process memory"]
IT --> CT["create thread"]
RWX --> N["number: 0x40
= PAGE_EXECUTE_READWRITE"]
N -.->|inmediato → exige disasm| HARD["FUERA DE ALCANCE"]
HARD ==>|arrastra la regla entera| IT
WPM -.->|import| OK1["aproximable"]
CT -.->|import| OK2["aproximable"]Este es el resultado más instructivo de la serie hasta aquí: capa-real reconoce los ingredientes de la inyección —la escritura en memoria de proceso, la creación de hilo— pero no la técnica compuesta. Ve la harina y los huevos; no puede confirmar el pastel. Y no por una carencia del motor, sino porque la evidencia que distingue una inyección de un uso legítimo de esas APIs —la bandera RWX, y que todo ocurra junto— vive en un plano que el análisis estático no alcanza. (Lo mismo ocurre con spawn thread to RWX shellcode, que hace match: allocate or change RWX memory y por tanto hereda el mismo destino: fuera de alcance.)
inject APC: el contraste quirúrgico#
Ahora la lección se vuelve fina. Otra técnica de inyección, APC (Asynchronous Procedure Call, T1055.004 ), es conceptualmente hermana de la anterior: escribir en otro proceso y encolar una rutina para que se ejecute. Uno esperaría el mismo veredicto. Miren la regla:
rule:
meta:
name: inject APC
namespace: host-interaction/process/inject
scopes:
static: function
att&ck:
- Defense Evasion::Process Injection::Asynchronous Procedure Call [T1055.004]
features:
- and:
- or:
- match: write process memory
- match: create or open section object
- api: kernel32.MapViewOfFile
- or:
- api: kernel32.QueueUserAPC
- api: ntdll.NtQueueApcThread
- api: ntdll.NtQueueApcThreadEx
- optional:
- or:
- number: 0x1fffff = THREAD_ALL_ACCESS
- api: kernel32.CreateProcess
- api: kernel32.OpenProcess
Regla host-interaction/process/inject/inject-APC (recortada) — capa-rules (Mandiant, Apache-2.0).
Los dos primeros requisitos obligatorios son or de APIs (write process memory / MapViewOfFile, y QueueUserAPC / NtQueueApcThread): todos imports, todos file-scope. ¿Y la constante dura? Ahí está la clave: number: 0x1fffff (THREAD_ALL_ACCESS) —el equivalente al 0x40 de antes— vive bajo optional. Y optional, como definimos en la Parte 0, es descriptivo: documenta lo que suele acompañar a la técnica, pero no afecta al match.
Cuando capa-real compila esta regla, poda la rama optional —incluido su number imposible— y se queda con dos or de APIs, ambos evaluables. El requisito difícil no era obligatorio; era una nota al pie.
El contraste es exacto y vale como moraleja de toda la parte:
inject thread y inject APC son casi la misma técnica. La diferencia entre “fuera de alcance” y “aproximable” no está en lo que hacen, sino en dónde la regla pone su requisito duro: en inject thread, el
number: 0x40es obligatorio y hunde la regla; en inject APC, elnumber: 0x1fffffestá bajooptionaly se poda sin consecuencias. Un operador de la gramática —optional— decide de qué lado de la frontera cae la capacidad.
Es la demostración más limpia de por qué en esta serie no basta con clasificar reglas por su táctica: dos reglas de la misma carpeta, host-interaction/process/inject, con nombres casi iguales, caen a lados opuestos por un detalle de estructura. Leer capa bien es leer el árbol, no el título.
Persistencia: cuando la ruta no basta#
Cruzamos a la persistencia, y reaparece el mismo principio con otra cara. Tomemos la técnica más común: escribir tu binario en la clave Run del registro para que Windows lo lance en cada inicio de sesión. Ejemplo canónico, el Lab 06-03 del PMA:
rule:
meta:
name: persist via Run registry key
namespace: persistence/registry/run
scopes:
static: function
att&ck:
- Persistence::Boot or Logon Autostart Execution::Registry Run Keys / Startup Folder [T1547.001]
mbc:
- Persistence::Registry Run Keys / Startup Folder [F0012]
features:
- and:
- or:
- match: set registry value
- number: 0x80000001 = HKEY_CURRENT_USER
- number: 0x80000002 = HKEY_LOCAL_MACHINE
- or:
- string: /Software\\Microsoft\\Windows\\CurrentVersion\\Run/i
- string: /Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\Run/i
- string: /System\\(ControlSet\d{3}|CurrentControlSet)\\Control\\Session Manager\\BootExecute/i
Regla persistence/registry/run/persist-via-Run-registry-key (recortada) — capa-rules (Mandiant, Apache-2.0).
Léanla como un and de dos mitades. La segunda es un or de cadenas: las rutas de las claves de auto-arranque. Eso, a solas, es file-scope puro —capa-real ve esas cadenas sin problema—. Pero la primera mitad exige la evidencia de escritura: o match: set registry value, o una de las constantes HKEY_* (que son number, inmediatos → fuera de alcance). Y set registry value, aunque se apoye en APIs como RegSetValueEx, capa la modela acoplando la llamada con la clave que se abre y escribe —de nuevo, coincidencia dentro de una función—.
persist via Run registry key: FUERA DE ALCANCE — La regla no se conforma con «el binario menciona la clave Run»; exige «el binario escribe en la clave Run», y el acoplamiento entre la ruta y la primitiva de escritura, en la misma función, es lo que el file-scope no puede confirmar.
Y aquí está el matiz más honesto de toda la serie, que conviene no esquivar:
capa-real cede cobertura precisamente donde ganaría precisión. Prefiere no afirmar la persistencia antes que afirmarla sobre una evidencia parcial. Es la misma disyuntiva que atraviesa la serie Nightmare Eclipse —cazar la clase de comportamiento sin inflar el veredicto—, aplicada al detalle de una sola regla.El resto de la familia de persistencia se comporta igual, y por la misma razón. Persistir vía servicio de Windows (T1543.003
) acopla CreateService con la constante SERVICE_AUTO_START (number: 2) en un bloque, o bien la creación de proceso con las cadenas sc create / New-Service. Programar una tarea (T1053.005
) acopla la ejecución de schtasks /create (o Register-ScheduledTask) con la evidencia de que se está lanzando un proceso. En los dos casos, la cadena delatora (sc, schtasks) es file-scope, pero la regla exige además la prueba de que se ejecuta, y ese acoplamiento cae del lado difícil.
El mapa de la frontera, segunda entrega#
| Capacidad | La evidencia que la distingue | En capa-real |
|---|---|---|
| Escribir en memoria de proceso | Import (WriteProcessMemory) | Aproximada |
| Crear hilo | Import (CreateThread…) | Aproximada |
| Inyección por APC | APIs obligatorias; el number duro va en optional | Aproximada |
| Reservar/cambiar memoria RWX | number: 0x40 en un bloque | Fuera de alcance |
| Inyección de hilo | Arrastrada por el requisito RWX | Fuera de alcance |
| Persistir vía clave Run | Ruta (file) acoplada a la escritura | Fuera de alcance |
| Persistir vía servicio / tarea | Cadena (file) acoplada a la ejecución | Fuera de alcance |
El patrón de esta parte es nítido y complementa el de la anterior. En la Parte 1 vimos que el file-scope pierde lo que solo existe como forma o valor dentro de una instrucción (cripto, hashes, anti-debug). Aquí vemos la otra mitad: pierde también lo compuesto —lo que exige que dos evidencias, cada una visible por separado, coincidan en el mismo sitio—. La inyección y la persistencia son casi todas de este tipo: sus ingredientes están a la vista, pero la receta que los une no. Por eso son, de todas las tácticas, la zona donde capa-real más cede.
Para el defensor#
Qué llevarse a la práctica.
- Los imports de inyección son un indicio, no una conclusión.
WriteProcessMemory+CreateRemoteThread+VirtualAllocExen la misma tabla de imports es el trío clásico de inyección y merece triage, pero —igual quecapa-real— tu análisis estático no puede confirmar que se usen juntos y conPAGE_EXECUTE_READWRITE. Para eso hace falta un sandbox (ver la llamada con el flag0x40) o desensamblar. Trátalo como “candidato a inyección”, no como “inyecta”. - La constante
0x40es oro en dinámico. Lo que el file-scope no ve, la telemetría de ejecución sí: una llamada aVirtualAllocEx/VirtualProtectconflProtect = 0x40(RWX) contra otro proceso es una de las señales de inyección de mayor fidelidad y bajísimo ruido. Es justo el eslabón que capa modela y que el estático pierde: cázalo en tu EDR. - Para persistencia, no te quedes en la cadena. Ver la ruta de la clave Run o
schtasksen un binario es barato pero ruidoso (medio software legítimo las menciona). La señal de calidad es la acción: la escritura efectiva en...\CurrentVersion\Run, la creación de un servicioSERVICE_AUTO_START, el alta de una tarea. Correlaciona la cadena con el evento de escritura/creación —Sysmon EventID 13 (registro), 12/7045 (servicio)— y habrás reconstruido, en detección, justo el acoplamiento que la regla de capa exige y que el estático no puede.
En la Parte 3 seguimos el kill chain hacia la acción sobre el objetivo: evasión de defensas, acceso a credenciales y descubrimiento. Ahí el file-scope recupera terreno —el robo de secretos en la nube y buena parte del reconocimiento se detectan por rutas de fichero e imports— y volveremos a ver la frontera moverse a favor del análisis estático.
Referencias#
- capa-rules (Apache-2.0): https://github.com/mandiant/capa-rules
- MITRE ATT&CK — Process Injection (T1055): https://attack.mitre.org/techniques/T1055/
- MITRE ATT&CK — Boot or Logon Autostart Execution: Registry Run Keys (T1547.001): https://attack.mitre.org/techniques/T1547/001/
- Sikorski & Honig — Practical Malware Analysis, labs 12-01 (inyección de hilo) y 06-03 (persistencia por Run): https://nostarch.com/malware
- Elastic — Ten process injection techniques: https://www.elastic.co/security-labs/ten-process-injection-techniques