crystal-palace. tradecraft link PIC y evasion de EDRs
Introducción
Esto no es un tutorial. Ya existen muchos. Esto es una inmersión arquitectónica profunda en Crystal Palace, el linker PIC y el lenguaje de script de enlazado creado por Raphael Mudge, y un examen orientado a la investigación de por qué cambia fundamentalmente la forma en que construimos payloads evasivos.
He estado trabajando extensamente con Crystal Palace durante los últimos meses, integrándolo en herramientas operativas, mapeando lo que puede y no puede hacer, e intentando entender por qué un linker de todas las cosas posibles importa tanto en la emulación de adversarios moderna. La respuesta me sorprendió. No se trata de lo que Crystal Palace te permite construir. Se trata de lo que Crystal Palace te permite evitar construir.
La mayoría de los frameworks C2 incluyen un loader reflectivo por defecto. Ese loader es el componente más firmado de cualquier implante. Todos los EDR del mercado tienen reglas para él. Todos los investigadores tienen reglas YARA para él. El análisis estático lo devora antes de que llegue siquiera a memoria. Y lo peor es que nada de esto es culpa del desarrollador del C2. Un loader por defecto tiene que ser genérico, predecible y de talla única. Esas son exactamente las propiedades que lo hacen detectable.
El consejo habitual es «escribe tu propio loader». Buen consejo, pero ¿qué significa eso en la práctica? Código Independiente de Posición (PIC), sin literales de cadena, sin variables globales, sin llamadas directas a la API de Win32, cada función resuelta mediante recorridos del PEB y comparaciones de hash, cada relocalización gestionada manualmente, sin referencias a datos basadas en pila. Si alguna vez has intentado escribir PIC a mano para algo más complejo que un MessageBox, sabes lo rápido que degenera en microgestión a nivel de ensamblador imposible de mantener.
Crystal Palace cambia esta ecuación. No es un compilador. Es un linker. Toma tus archivos objeto COFF compilados y los transforma en bloques independientes de posición en tiempo de enlazado, devolviéndote las cadenas, las variables globales, las llamadas a la API de Win32 y todo lo demás que pierdes al escribir PIC a mano. Además, incluye un conjunto de primitivas de mutación en tiempo de enlazado que aleatorizan el binario en cada build, haciendo que el desarrollo de firmas estáticas sea efectivamente imposible.
Este artículo cubre: qué es realmente Crystal Palace por dentro, por qué la distinción entre linker y compilador es importante, cómo el DSL .spec permite la composición modular de técnicas ofensivas, cómo el cóctel BTF rompe la detección estática, por qué esto es relevante para la evasión de EDR modernos, cómo se integra con frameworks C2 reales, lo que he aprendido construyendo con él, y dónde encaja en el ecosistema más amplio de herramientas de desarrollo PIC.
Sin código. Sin tutoriales paso a paso. Solo arquitectura, análisis y contexto operativo.
0. ¿Porque no e escrito esto antes?

Fuera de tecnicismos el elefante en la habitacion, esto lleva redactado un tiempo y en mi mente. Al final, es lo que más utilizo para la evasión de EDR y lo que aprendí a usar en el CRTL de Zero-Point Security.
bastante gente me preguntó si iba a hablar sobre Crystal Palace para la evasión de EDRs como Elastic o CrowdStrike. Esto también a raíz de una de mis anteriores explicaciones que trataba sobre Stardust.
Y yo les decía: "Quiero, pero no tengo dónde hacerlo que no sea un Cobalt Strike, y yo no tengo Cobalt Strike"
El problema radica en lo siguiente: no hay C2 open source que se lleven excesivamente bien con Crystal Palace.
Sliver, cómo no, siempre fastidiando con Go. Y eso que lo conseguí aplicar gracias a este proyecto, pero era tan inconsistente que simplemente no vale la pena gastar tiempo en él: https://github.com/licitrasimone/CrystalSliver (de hecho, yo pasé un vídeo en el Discord de Zero-Point usándolo y no iba mal con mis configs, pero cuando profundizas te das cuenta de todo).
AdaptixC2. Esto no sé cómo describirlo. Es raro; simplemente me parece raro en algunos sentidos. La implementación de Crystal Palace en este caso se llama StealthPalace (https://github.com/MaorSabag/Adaptix-StealthPalace) y el creador es majísimo, muy buen tío. Pero tiene un problema: no tiene post-ex. Se lo comenté al creador y me dijo que eso está hecho, pero que no sabe si liberarlo. Y yo dije: «¿De qué me sirve colarme si después no puedo hacer nada?». Me puse a investigar para hacerme mi propia post-ex y ahí vi lo raro que era el C2.
Y después vino Mythic a mi vida. Yo ya lo había usado con anterioridad (como hace 2 años jajaj), pero la interfaz es muy rara, al igual que el tema de los agentes. Sin embargo, hablando con its-a-feature (https://github.com/its-a-feature), creador de Mythic, me comentó la existencia de Xenon (https://github.com/MythicAgents/Xenon) (https://github.com/nickswink/Crystal-Kit-Xenon) y me presentó a su creador. Hablando con él, me comentó que, aunque Xenon no está acabado al 100%, sí que cubre todas mis necesidades. Y pues llevo cosa de casi un mes testeándolo y analizándolo todo.
Los resultados han sido magníficos: cubre casi al 100% mis necesidades (he notado unas pequeñas cosas en la post-ex, pero tienen solución) y procedí a probarlo contra mi Elastic 9.4.2. Para ello, también me lancé a hacer un loader bastante potente en Rust, hecho a medida para el .bin de Mythic con Crystal Palace (esto me da para otra entrada de blog, así que no entraré en detalles, pero es para que tengáis el contexto). ¿El resultado? Ni una sola detección, incluso ejecutando varias cosas. Os comparto las capturas que pasé por el Discord de Zero-Point Security; más adelante, a lo mejor, añado un vídeo de demostración. :).








1. El Problema Fundamental: PIC a Escala
Antes de poder entender qué resuelve Crystal Palace, tenemos que entender qué significa escribir código independiente de posición a mano, y por qué eso es genuinamente doloroso a cualquier escala más allá de una docena de líneas de ensamblador.
1.1 Lo Que Significa Realmente la Independencia de Posición
El código independiente de posición es código que puede ejecutarse correctamente independientemente de dónde se cargue en memoria. Sin fixups. Sin tabla de relocalización. Sin suposiciones sobre su propia dirección. Se lo entregas a un loader, se coloca en algún lugar de la memoria virtual, y simplemente funciona.
Sobre el papel esto suena como cualquier shellcode competente. En la práctica, el conjunto de instrucciones x64 depende en gran medida del direccionamiento absoluto y relativo a RIP. El código C compilado normal está lleno de referencias a direcciones absolutas las variables globales residen en un offset fijo en .data o .bss, los literales de cadena se ubican en .rdata en una VA conocida, y las llamadas a la API pasan por la Import Address Table que el loader de Windows resuelve a punteros de función absolutos.
Cuando eliminas las cabeceras PE, el loader y la IAT, todas esas direcciones pierden su significado. El código sigue intentando referenciar direcciones que tenían sentido dentro de una imagen PE correctamente cargada, pero como blob crudo en memoria ninguna de esas referencias apunta a ubicaciones válidas. El resultado es una violación de acceso, un cuelgue o peor un comportamiento silenciosamente erróneo.
Para escribir código verdaderamente compatible con PIC, tienes que eliminar cada relocalización de tu sección .text. Eso significa:
Nada de literales de cadena. Los literales de cadena en C se almacenan en .rdata en un offset fijo. El compilador genera instrucciones LEA relativas a RIP para referenciarlos, y esos offsets se calculan asumiendo que el PE está cargado en su ImageBase preferida. En un blob PIC no hay ImageBase ni .rdata o más bien, todo se aplana en una región contigua, pero el compilador no lo sabe y genera relocalizaciones de todas formas.
Nada de definiciones de variables globales o estáticas. Las globales inicializadas van a .data, las no inicializadas a .bss. Ambas secciones tienen direcciones fijas en una imagen PE. En un blob PIC esas direcciones no existen hasta tiempo de ejecución, y cualquier referencia a una variable global genera una relocalización en .text. Incluso static int counter = 0; es una definición, no una declaración, y produce una relocalización en .bss.
Nada de llamadas directas a la API de Win32. En un ejecutable normal, llamar a VirtualAlloc pasa por la IAT. La IAT es una tabla de punteros de función que el loader de Windows resuelve en tiempo de carga recorriendo los directorios de exportación de las DLL requeridas. En un blob PIC no hay loader, no hay IAT, nada que resuelva esas direcciones por ti. Tienes que hacerlo tú mismo en tiempo de ejecución recorrer el PEB, encontrar el módulo, analizar la tabla de exportación, comparar hashes.
Nada de sentencias switch. Las sentencias switch con más de unos pocos casos normalmente compilan a jump tables. Las jump tables son tablas de direcciones absolutas almacenadas en .rdata, y el dispatch del switch utiliza un salto indirecto a través de la tabla. Tanto la tabla como la referencia a ella son relocalizaciones. Esto pilla a mucha gente desprevenida.
La función go() debe ser la primera. Dado que el blob PIC no tiene cabecera PE con un AddressOfEntryPoint, el llamante simplemente salta a la dirección base del blob. Lo que esté en el byte cero se ejecuta. Esta función llamada por convención go() debe ser lo primero en el archivo de salida, sin datos de prólogo, sin relleno de alineación, nada antes de ella.
1.2 El Flujo de Trabajo Manual para PIC
El enfoque tradicional para escribir C compatible con PIC se parece a esto:
Escribes tu código. Lo compilas a un archivo objeto COFF con mingw-w64. Revisas las relocalizaciones con objdump -r. Encuentras tres relocalizaciones en .text. Las rastreas: una es un literal de cadena, otra es una llamada directa a la API, otra es una variable global. Reescribes la cadena como un array de caracteres asignado en pila construido byte a byte. Sustituyes la llamada a la API por un recorrido del PEB y una comparación de hash. Eliminas la variable global por completo, pasando todo como parámetros de función. Recompilas. Revisas las relocalizaciones otra vez. Queda una. Es una jump table de una sentencia switch. Reescribes el switch como una cadena de if-else. Recompilas. Cero relocalizaciones. El código es compatible con PIC. Te ha llevado cuarenta minutos gestionar tres relocalizaciones.
Para un loader C2 completo esto es insostenible. Un loader realista toca docenas de APIs de Win32, manipula múltiples identificadores de cadena y a menudo abarca varios archivos fuente. Hacer todo eso sin cadenas, sin globales, sin llamadas directas a la API y sin sentencias switch significa que básicamente estás escribiendo todo en un C adyacente al ensamblador con estructuras de datos primitivas. Es frágil, es tedioso, y cada nueva funcionalidad añade otra ronda de arqueología de relocalizaciones.
Este es el problema que Crystal Palace fue construido para resolver.
1.3 Los Dos Enfoques: Stardust vs. Crystal Palace
Antes de entrar específicamente en Crystal Palace, conviene entender que el ecosistema de desarrollo PIC tiene dos enfoques filosóficos dominantes:
Stardust, de Cracked5pider, adopta el enfoque de «escribe C++ con disciplina». Proporciona un script de enlazado que fuerza todas las secciones a un blob .text contiguo, un mecanismo de plantilla symbol<T> para acceso a datos independiente de posición, y atributos de sección explícitos para declarar variables globales. Escribes C++ estándar, pero debes colocar manualmente cada global en la sección .global o enfrentarte a cuelgues silenciosos. El control es absoluto; la responsabilidad es absoluta.
Crystal Palace adopta el enfoque de «transformar después de la compilación». Escribes tu código, lo compilas como un archivo objeto COFF estándar, y Crystal Palace se encarga de la transformación PIC en tiempo de enlazado. Fusiona .text y .rdata, parchea las relocalizaciones, resuelve la tabla DFR y aplica transformaciones de mutación. No escribes código PIC. Escribes código normal, y Crystal Palace lo convierte en PIC.
Ningún enfoque es mejor en términos absolutos. Resuelven problemas diferentes para operadores diferentes. La clave es que el modelo de transformación post-compilación de Crystal Palace significa que puedes tomar cualquier objeto COFF compilado incluyendo librerías de terceros, componentes precompilados y técnicas ofensivas existentes y hacerlo PIC sin tocar el código fuente. Esto tiene implicaciones que exploraremos a lo largo de este artículo.
2. Arquitectura: Linker, No Compilador
Lo más importante que hay que entender sobre Crystal Palace es que es un linker. No compila nada. Toma archivos objeto COFF compilados como entrada y produce un blob PIC como salida. Esta distinción no es académica; define todo lo que Crystal Palace puede hacer y todo lo que no puede.
2.1 La Tubería de Construcción
El flujo de trabajo típico de Crystal Palace tiene este aspecto:
┌─────────────────────────────────────────────────────────────────┐
│ TUBERÍA DE CRYSTAL PALACE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ src/*.c ──► mingw-w64 ──► bin/*.x64.o (archivos objeto COFF)│
│ │
│ src/*.asm ──► nasm ───────► bin/*.x64.bin (stubs binarios) │
│ │
│ *.spec ──► Crystal Palace ──► payload.bin (blob PIC) │
│ │
│ El archivo .spec lo orquesta todo: │
│ • Qué objetos COFF cargar y fusionar │
│ • Cómo transformarlos (PIC, mutaciones) │
│ • Qué recursos externos incrustar (DLL, mask, args) │
│ • Qué APIs interceptar (attach, addhook) │
│ • Qué sub-PICOs componer (run) │
│ • Qué evasiones aplicar en tiempo de enlazado │
│ │
└─────────────────────────────────────────────────────────────────┘
El paso de compilación produce archivos objeto COFF estándar. Estos no son PIC. Tienen relocalizaciones, secciones .data y .rdata, y referencias normales a la API estilo IAT. Tienen exactamente el mismo aspecto que la salida intermedia de cualquier build de Windows.
Crystal Palace toma estos archivos COFF y realiza una serie de transformaciones controladas por el archivo .spec:
- Fusionar secciones.
.texty.rdatase aplastan juntas en una sola región contigua. Esto es lo que te devuelve las cadenas siguen existiendo en la salida, pero están incrustadas dentro de la sección de código en offsets relativos conocidos. - Parchear relocalizaciones. Cada relocalización en los archivos COFF se resuelve contra el diseño fusionado. Las direcciones absolutas se convierten en offsets relativos. Las referencias relativas a RIP se recalculan para el diseño aplanado. El resultado es una sección
.textcon cero relocalizaciones sin resolver, que es la definición de código independiente de posición. - Construir la tabla DFR. La Resolución Dinámica de Funciones (Dynamic Function Resolution) reemplaza la IAT. En lugar de que el loader de Windows resuelva
VirtualAlloca una dirección de kernel32, Crystal Palace inserta código que lo resuelve en tiempo de ejecución recorriendo el PEB y hasheando nombres de exportación. La sintaxisMODULE$Functionen tu código fuente se convierte en una búsqueda en esta tabla de resolución en tiempo de ejecución. - Resolver hooks. Las directivas
attachyaddhookparchean los archivos objeto para redirigir llamadas específicas a la API hacia tus funciones envoltorio.attachfunciona a nivel del loader cuando escribesattach "KERNEL32$LoadLibraryA" "_LoadLibraryA", cada llamada aLoadLibraryAdentro de tu loader se redirige a tu envoltorio_LoadLibraryA.addhookfunciona a nivel de la DLL cargada registra hooks que el interceptor_GetProcAddressdevuelve en lugar de los punteros de función reales. - Incrustar recursos.
push $DLL; preplen; link "section"añade un archivo al blob y lo hace accesible en tiempo de ejecución a través de un símbolo de sección con nombre. Tu loader recupera la DLL incrustada, la máscara o los argumentos tomando la dirección de ese símbolo de sección. Sin E/S de archivo. Sin artefacto en disco. - Aplicar mutaciones. El cóctel BTF se ejecuta después de todas las transformaciones estructurales. Aleatoriza secuencias de instrucciones, baraja bloques básicos, ciega constantes, dispersa NOPs y fragmenta patrones de instrucción conocidos. La salida es estructuralmente única en cada build.
- Autocomprobación. Si el
.specincluye una directiva de regla YARA, Crystal Palace calcula la regla contra su propia salida. Si hay una coincidencia, la build falla. No envías nada que active tus propias firmas. - Exportar. El blob final se escribe a disco. No tiene cabeceras PE, ni tabla de secciones, ni directorio de importación, ni tabla de relocalización. Es código y datos crudos, con
go()garantizado en el offset cero.
2.2 Por Qué Importa la Distinción de Linker
Debido a que Crystal Palace opera en tiempo de enlazado en lugar de en tiempo de compilación, es fundamentalmente agnóstico al compilador. Puedes compilar tu fuente con mingw-w64, con MSVC, con Clang mientras produzca un archivo objeto COFF válido, Crystal Palace puede procesarlo. Esto tiene dos enormes ventajas operativas:
El código ofensivo precompilado se vuelve reusable. Puedes compilar suplantación de pila de llamadas, enmascaramiento de suspensión, hooking de IAT y rutinas de limpieza como objetos COFF separados y componerlos en tiempo de enlazado. Cada componente puede desarrollarse, probarse y mantenerse de forma independiente. Cambiar el mecanismo de suplantación de pila por una fuente de gadgets diferente es un cambio de una línea en el archivo .spec. Esto convierte la evasión de un esfuerzo de ingeniería monolítico en un problema de composición modular.
Las librerías de terceros entran en juego. La directiva mergelib fusiona un archivo de librería estática en la build. Así es como se integra la librería de código cerrado libtcg.x64.zip que proporciona PicoLoad, ParseDLL, LoadDLL, ProcessImports y otras primitivas de carga reflectiva. No necesitas el código fuente de la librería. Solo necesitas un .zip que contenga objetos COFF, y Crystal Palace se encarga del resto.
2.3 La Garantía de go()
Un detalle que importa enormemente en la práctica: +gofirst en el archivo .spec asegura que la función go() esté físicamente en el offset cero del blob de salida. Esto significa que cualquier loader stager, ejecutor de shellcode, inyector de procesos puede saltar a la dirección base sin saber nada sobre la estructura interna del PIC. No hay búsqueda de exportación, ni recorrido de ordinales, ni análisis de PE. Solo jmp base_address.
Este es el contrato universal entre la salida de Crystal Palace y cada loader que la consume. No importa si estás usando el artifact kit de Cobalt Strike, un loader personalizado en Rust, un script de Python llamando a ctypes o un driver de kernel realizando mapeo manual. Offset cero significa go(). Siempre.
3. El DSL .spec: Componiendo Técnicas Ofensivas
El archivo .spec es la superficie de integración de Crystal Palace. Es un lenguaje específico de dominio para describir cómo transformar objetos COFF en blobs PIC, qué recursos incrustar, qué hooks instalar y qué mutaciones aplicar. Entender el lenguaje .spec es entender el poder de Crystal Palace.
3.1 Las Directivas Fundamentales
El lenguaje .spec está basado en pila y es procedural. Cada línea es una directiva que opera sobre el estado actual de la build. Aquí están las directivas que definen la arquitectura:
Cargar y Transformar
load "bin/loader.x64.o"
make pic +gofirst +optimize
load lee un archivo objeto COFF y lo apila en la pila de construcción. make pic lo transforma en código independiente de posición fusionando .text y .rdata, resolviendo relocalizaciones y construyendo la tabla DFR. +gofirst asegura que go() esté en el offset cero. +optimize elimina código muerto de librerías fusionadas, lo cual importa porque mergelib incorpora archivos estáticos completos y no quieres que funciones de librería no utilizadas inflen tu salida.
Resolución Dinámica de Funciones
dfr "resolve" "ror13"
Esta directiva habilita el sistema DFR y especifica cómo funciona. El primer argumento es el nombre de la función de resolución dentro de tu código (la función que realmente recorre el PEB y llama a GetProcAddress). El segundo argumento es el algoritmo de hashing ror13 es la elección estándar, coincidiendo con la convención BOF y produciendo hashes de 32 bits a partir de nombres de DLL y función. Una vez que DFR está habilitado, cada referencia MODULE$Function en tu código genera un par de hash que se resuelve en tiempo de ejecución llamando a tu función de resolución.
La función de resolución en sí no la proporciona Crystal Palace. Debe existir en tu código, típicamente usando el patrón auxiliar resolve_eat.h que se incluye con los ejemplos de Crystal Palace. Esta separación es deliberada: tú controlas cómo funciona la resolución. Si quieres añadir recuperación de stubs enganchados, syscalls indirectas o cualquier otra lógica de resolución consciente de detección, la implementas dentro de tu resolver. Crystal Palace genera las referencias de hash; tú defines la ruta de resolución.
Fusionar y Componer
load "bin/services.x64.o" merge
load "bin/hooks.x64.o" merge
load "bin/spoof.x64.o" merge
mergelib "../libtcg.x64.zip"
merge toma un objeto COFF cargado y lo fusiona en el objetivo de build actual. Así es como compones múltiples archivos fuente en un solo blob PIC. El loader, los envoltorios de hook, las funciones de suplantación de pila, el código de limpieza cada uno vive en su propio archivo COFF, cada uno se fusiona de forma independiente, y el resultado es un PIC unificado.
mergelib fusiona un archivo de librería estática completo. Así es como se integra libtcg.x64.zip, incorporando PicoLoad, ParseDLL, LoadDLL, ProcessImports, PicoGetExport, PicoDataSize, PicoCodeSize y todas las demás primitivas de carga reflectiva sin necesidad de acceder a su código fuente.
La ventaja operativa crítica: puedes intercambiar cualquiera de estos componentes de forma independiente. Si quieres reemplazar la implementación de suplantación de pila por una que use una fuente de gadgets diferente o un cálculo de unwind diferente, cambias qué archivo .x64.o se carga bajo la directiva merge. El resto del loader no cambia.
Incrustación de Recursos
generate $MASK 128
push $DLL
xor $MASK
preplen
link "dll"
push $MASK
preplen
link "mask"
Esta secuencia incrusta la DLL del implante en el blob PIC con enmascaramiento basado en XOR:
generate $MASK 128crea una clave aleatoria de 128 bytes en tiempo de enlazado. Esta clave es diferente en cada build.push $DLLcarga el archivo DLL como un blob binario.xor $MASKcifra la DLL con XOR usando la clave aleatoria.preplenantepone el tamaño del blob cifrado como un entero de 32 bits.link "dll"adjunta el payload cifrado a una sección con nombre llamadadll.
La propia clave de máscara también se incrusta (pasos 5-7) para que el loader pueda descifrar la DLL en tiempo de ejecución. La clave es diferente en cada build, así que incluso si un analista extrae la DLL cifrada de un payload, la misma clave no funcionará en otro.
En tiempo de ejecución, tu código de loader recupera estos recursos con GETRESOURCE(_DLL_) y GETRESOURCE(_MASK_) macros que simplemente toman la dirección del símbolo de sección con nombre. Sin E/S de archivo, sin acceso a red, sin llamadas a la API. Los recursos existen en la misma región de memoria que el código en ejecución.
Composición de Sub-PICO
run "pico.spec"
link "pico"
exportfunc "setup_hooks" "__tag_setup_hooks"
exportfunc "setup_memory" "__tag_setup_memory"
run invoca otro archivo .spec como una sub-build. Produce un sub-PICO un bloque de código independiente de posición independiente y lo enlaza dentro del PIC padre en una sección con nombre. El loader padre puede entonces llamar a PicoLoad, PicoGetExport y otras funciones de gestión de PICO para cargar, ejecutar y comunicarse con el sub-PICO.
exportfunc expone una función dentro del sub-PICO al loader padre. El padre llama a PicoGetExport con el valor de etiqueta (p. ej., __tag_setup_hooks()) para obtener la dirección en tiempo de ejecución de la función. Así es como el loader inicializa los hooks del PICO después de cargarlo en memoria.
El modelo PICO permite la separación de responsabilidades a nivel de memoria. El PICO loader es una región de memoria (código + datos). El PICO de hooks es otra. Pueden liberarse de forma independiente. Se comunican a través de una interfaz definida (IMPORTFUNCS, MEMORY_LAYOUT). Esto es composición a nivel arquitectónico, no solo a nivel de build.
Hooking: attach vs. addhook
Esta es la distinción arquitectónica más importante en Crystal Palace, y merece la pena entenderla con precisión porque equivocarse causa errores difíciles de diagnosticar.
attach intercepta llamadas a la API dentro del propio loader. Cuando escribes:
attach "KERNEL32$LoadLibraryA" "_LoadLibraryA"
attach "KERNEL32$VirtualAlloc" "_VirtualAlloc"
attach "KERNEL32$VirtualProtect" "_VirtualProtect"
Cada llamada a KERNEL32$LoadLibraryA dentro de tu código de loader se parchea en tiempo de enlazado para llamar a _LoadLibraryA en su lugar. Esta es una redirección estática, en tiempo de build. No hay búsqueda en tiempo de ejecución. El resolver DFR no está involucrado. El linker simplemente reemplaza el destino de llamada en el código objeto.
addhook intercepta llamadas a la API dentro de la DLL cargada. Cuando escribes:
addhook "KERNEL32$GetProcAddress" "_GetProcAddress"
addhook "KERNEL32$Sleep" "_Sleep"
addhook "KERNEL32$VirtualAlloc" "_VirtualAlloc"
Estás registrando hooks que el interceptor _GetProcAddress del PICO devolverá en lugar de los punteros de función reales. Cuando el loader llama a ProcessImports para resolver la IAT de la DLL, el GetProcAddress controlado verifica cada nombre de función contra los hooks registrados usando __resolve_hook(ror13hash(name)). Si hay coincidencia, se devuelve el puntero del hook. La IAT de la DLL ahora apunta a tus funciones de hook, y cada llamada a Sleep, VirtualAlloc, etc. realizada por el implante pasa por tus envoltorios.
Este es el mecanismo que permite el enmascaramiento de suspensión sin modificar el código fuente del agente C2. El implante cree que está llamando a Sleep. En realidad está llamando a tu _Sleep, que cifra la imagen, duerme, descifra y retorna. El implante no tiene ni idea de que esto está ocurriendo.
Preserve: Prevención de Recursión
preserve "KERNEL32$LoadLibraryA" "init_frame_info"
Esto le dice a Crystal Palace: «la directiva attach para LoadLibraryA no se aplica a las llamadas que se originan en init_frame_info». Sin esto, cuando init_frame_info llama a LoadLibraryA para cargar un módulo de gadgets (como dfshim.dll para gadgets de suplantación de pila), la llamada es interceptada por _LoadLibraryA, que llama a spoof_call, que vuelve a llamar a init_frame_info para configurar el gadget, que llama a LoadLibraryA otra vez recursión infinita, cuelgue instantáneo.
El alcance de preserve está limitado a la función específica nombrada. Si tienes otras funciones que necesitan llamar a APIs enganchadas sin intercepción, cada una necesita su propia directiva preserve. Esto es deliberado: quieres ser explícito sobre qué sitios de llamada están exentos de hooking para que el comportamiento por defecto (todo se intercepta) proporcione el máximo control.
3.2 El Modelo de Composición
Reuniendo todo esto, el lenguaje .spec permite un enfoque fundamentalmente modular para el desarrollo de técnicas ofensivas:
┌───────────────────────────────────────────────────────────────────┐
│ COMPOSICIÓN DE TÉCNICAS OFENSIVAS │
├───────────────────────────────────────────────────────────────────┤
│ │
│ loader.c ──► loader.x64.o │
│ hooks.c ──► hooks.x64.o │
│ spoof.c ──► spoof.x64.o │
│ draugr.asm ──► draugr.x64.bin │
│ libtcg.x64.zip (librería precompilada) │
│ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │
│ │ │ EL ARCHIVO .spec │ │
│ │ │ │ │
│ │ │ load loader → make pic +gofirst +mutate │ │
│ │ │ merge hooks, spoof, draugr │ │
│ │ │ attach LoadLibraryA, VirtualAlloc, VirtualProtect │ │
│ │ │ preserve LoadLibraryA "init_frame_info" │ │
│ │ │ embed masked DLL + mask key │ │
│ │ │ run pico.spec → addhook Sleep, VirtualAlloc, ... │ │
│ │ │ export │ │
│ │ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ payload.bin (blob PIC único, go() en offset 0) │
│ │
│ Cada pieza es intercambiable de forma independiente. │
│ Cambia los hooks sin tocar el loader. │
│ Cambia la suplantación sin tocar los hooks. │
│ Cambia las mutaciones sin tocar la lógica. │
│ │
└───────────────────────────────────────────────────────────────────┘
Así no es como funciona tradicionalmente el desarrollo de loaders. Históricamente, escribes un archivo C monolítico, lo compilas, descubres que tiene quince relocalizaciones, pasas horas arreglándolas, compilas de nuevo, pruebas, iteras. Cada cambio toca todo. Crystal Palace rompe este acoplamiento. La implementación de suplantación de pila, la lógica de despacho de hooks, la rutina de enmascaramiento de suspensión y el código de limpieza son todos archivos COFF independientes compuestos en tiempo de enlazado. Si mejoras tu selección de gadgets de suplantación de pila, solo cambia spoof.x64.o.
4. El Cóctel BTF: Mutación Como Defensa
Si el lenguaje .spec es el poder arquitectónico de Crystal Palace, el cóctel BTF es su poder evasivo. BTF significa «Better Than Fortra» la afirmación jocosa de Raphael Mudge de que Crystal Palace genera salida más evasiva que el loader reflectivo por defecto de Cobalt Strike. El cóctel es un conjunto de transformaciones en tiempo de enlazado que aleatorizan el binario en cada build.
4.1 Las Primitivas de Mutación
+mutate Aleatorización a Nivel de Instrucción
+mutate modifica el ensamblador dentro de cada función. No cambia la semántica de la función las entradas, salidas y efectos secundarios permanecen idénticos. Pero reemplaza secuencias de instrucciones con alternativas equivalentes, reordena instrucciones independientes y sustituye asignaciones de registros. El resultado: cada build produce representaciones a nivel de byte diferentes de la misma lógica.
Esto es fundamental porque las firmas estáticas operan sobre patrones de bytes. Una regla YARA que coincida con 48 83 EC 20 B9 5B BC 4A 6A BA AA FC 0D 7C E8 en una build no coincidirá con la misma función en la siguiente build porque la secuencia de instrucciones ha sido mutada. La función sigue haciendo lo mismo el recorrido del PEB, la comparación de hash, la llamada a GetProcAddress pero los bytes son diferentes.
+regdance Barajado de Asignación de Registros
+regdance aleatoriza qué registros contienen qué valores a través de los límites de función. En x64, la misma computación puede realizarse con diferentes asignaciones de registros. mov rax, [rcx] y mov r8, [rcx] ambos desreferencian el mismo puntero; la diferencia es qué registro contiene el resultado. Al barajar las asignaciones de registros, +regdance hace que la relación entre secuencias de opcodes y operaciones lógicas sea opaca para el análisis estático.
+blockparty Reordenación de Bloques Básicos
+blockparty baraja el orden de los bloques básicos dentro de cada función. Una función con bloques A → B → C en orden fuente podría emitirse como C → A → B, con los saltos parcheados para mantener el flujo de control correcto. El comportamiento funcional es idéntico; la representación secuencial de bytes es completamente diferente.
Esto es particularmente efectivo contra reglas YARA que dependen de patrones de opcodes secuenciales. Cuando una regla describe un patrón como «sub rsp, 0x20; mov ecx, hash; mov edx, hash; call resolve», +blockparty asegura que esas instrucciones no sean secuenciales en el binario.
+shatter Fragmentación de Patrones de Instrucción
+shatter divide patrones de instrucción reconocidos insertando bytes basura u operaciones de registro entre ellos. Una secuencia bien conocida como mov rcx, rax; call ror13hash podría convertirse en mov rcx, rax; nop; nop; call ror13hash o mov rcx, rax; xor r8, r8; call ror13hash. Las operaciones basura no cambian el estado del programa, pero fragmentan el patrón que una firma estática espera.
+disco Desorden de Orden de Funciones
+disco aleatoriza el orden en que las funciones aparecen en el blob de salida. Combinado con +blockparty (que reordena bloques dentro de las funciones), toda la estructura binaria se baraja en cada build. Una función que estaba en el offset 0x1A4 en una build podría estar en 0x3F8 en la siguiente.
4.2 Cegado de Constantes con magic
magic "0x7FFFFFFF, 0xD34DB33F, 0xCAFEBABE, 0xDEADC0DE"
La directiva magic proporciona un conjunto de valores de 32 bits de aspecto aleatorio. Durante la fase de mutación, las constantes enteras conocidas en el código tamaños de asignación, flags de protección de memoria, tamaños de estructura, semillas de hash se reemplazan con valores extraídos de este conjunto. Las sustituciones se registran en una tabla incrustada en la salida, para que el loader pueda recuperar los valores originales en tiempo de ejecución.
Por qué esto importa: las firmas estáticas frecuentemente usan constantes mágicas como anclas. Si una regla coincide con 0x3000 (MEM_COMMIT | MEM_RESERVE) seguido de 0x40 (PAGE_EXECUTE_READWRITE) dentro de una distancia conocida, eso es un indicador de alta confianza de una operación tipo VirtualAlloc. Con el cegado de constantes, esos valores son diferentes en cada build. El 0x3000 podría ser 0xD34DB33F esta vez y 0xCAFEBABE la próxima, y la regla ya no coincide.
4.3 Dispersión de Instrucciones con ised
pack $NOP "b" 0x90
ised insert "call findModuleByHash" $NOP +safe
ised insert "call findFunctionByHash" $NOP +safe
ised insert "CALL rel32" $NULL +split +last +after
ised insert "MOV" "CALL rel32" $NULL +split +first +before
ised (instruction scatter, edit, and delete dispersión, edición y borrado de instrucciones) proporciona manipulación de grano fino basada en patrones:
pack $NOP "b" 0x90define un pack llamado$NOPque contiene un solo byte0x90(NOP en x64). Los packs son payloads de inserción reutilizables.ised insert "call findModuleByHash" $NOP +safeencuentra cada llamada afindModuleByHashe inserta el pack$NOPantes de ella.+safeasegura que la inserción sea segura (no rompe destinos de salto ni la alineación de pila).ised insert "call findFunctionByHash" $NOP +safelo mismo para llamadas afindFunctionByHash.ised insert "CALL rel32" $NULL +split +last +afterencuentra cada instrucciónCALL rel32y la divide, insertando$NULLdespués del último fragmento dividido. Esto fragmenta los patrones de llamada indirecta.ised insert "MOV" "CALL rel32" $NULL +split +first +beforeinserta antes del primer fragmento de secuencias MOV-seguido-de-CALL, ofuscando aún más la relación entre las instrucciones de configuración y la llamada real.
Estas directivas apuntan a los patrones a nivel de opcode en los que se basan las reglas YARA y las firmas de EDR. El patrón de llamada de resolución de hash (48 89 C1 E8 ?? ?? ?? ?? 89 C1) es una de las secuencias más comúnmente detectadas en loaders reflectivos porque es distintiva: mover un valor a RCX, llamar a una función de hashing, mover el resultado a ECX. ised rompe este patrón insertando NOPs antes de la llamada, dividiendo la comparación de hash y reordenando las instrucciones circundantes.
4.4 Autocomprobación YARA
rule "" 10 3 10-16
Esta directiva le dice a Crystal Palace: «calcula una regla YARA a partir de la salida, usando una longitud mínima de cadena de 10 bytes, requiriendo al menos 3 coincidencias, con ventanas de token de 10-16 bytes, y comprueba si la salida coincide con su propia regla».
Si la salida contiene algún patrón detectable, la build falla.
Esta es la red de seguridad operativa. Escribes archivos .spec que incluyen mutaciones y esperas que la salida sea única. La directiva rule lo demuestra. Si tu combinación de mutaciones es insuficiente y un patrón estable sobrevive, la build falla con un diagnóstico que te dice qué coincidió. Ajustas las mutaciones y reconstruyes.
La autocomprobación no es una garantía de evasión. Es una garantía de que la salida no coincide con la regla YARA heurística generada automáticamente. Un analista aún podría elaborar una regla más sofisticada, o el loader podría ser detectado por comportamiento. Pero para el propósito específico de romper el emparejamiento de patrones estáticos el vector de detección más común y más automatizado proporciona una verificación medible y repetible.
4.5 El Coste de la Mutación
El cóctel BTF no es gratuito. Aumenta el tamaño de salida entre un 20% y un 90% dependiendo de qué mutaciones estén activas y con qué agresividad estén configuradas. +mutate por sí solo típicamente añade un 15-30%. +blockparty y +disco añaden sobrecarga por el parcheo de saltos. +shatter e ised insertan instrucciones adicionales que no estaban en el código original. +optimize puede recuperar parte de ese inflado eliminando código de librería no utilizado, pero no puede deshacer el aumento de tamaño de las propias mutaciones.
Para la mayoría de los métodos de entrega esto es aceptable. Un loader que era de 35 KB podría pasar a ser de 55 KB. En un entorno donde el canal de entrega tiene megabytes de ancho de banda y el proceso objetivo tiene gigabytes de espacio de direcciones virtuales, 20 KB de tamaño adicional son irrelevantes. Para canales restringidos (exfiltración DNS, límites de MTU agresivos, payloads por etapas con límites de tamaño), puede importar. Puedes ajustar el cóctel: quita +shatter y reduce las inserciones de ised para obtener un binario más pequeño con una cobertura de mutación ligeramente menor.
5. Por Qué Esto Importa para la Evasión de EDR
Entender lo que Crystal Palace hace a nivel de build es necesario, pero es solo la mitad del cuadro. La otra mitad es entender lo que el EDR ve o más bien, lo que no ve cuando envías un payload construido con Crystal Palace.
5.1 La Superficie de Detección
Un EDR moderno inspecciona un proceso en ejecución a través de aproximadamente cinco dimensiones:
Análisis estático (archivo en disco). El payload reside en disco antes de su ejecución. El EDR lo escanea en busca de patrones de bytes conocidos, regiones de alta entropía, combinaciones sospechosas de importación de API y cabeceras PE incrustadas. Esta es la detección más fácil de implementar y la primera línea de defensa más común.
Análisis de memoria (tiempo de ejecución). El payload está en memoria. El EDR escanea la memoria del proceso en busca de regiones privadas ejecutables (Type = Private, Protection = Execute), páginas RWX, ausencia de archivos de respaldo y patrones de bytes conocidos en memoria que correspondan a familias de malware.
Hooking de API (userland). El EDR ha colocado trampolines en ntdll.dll, kernel32.dll y otras librerías clave del sistema. Cuando el payload llama a una API, la ejecución pasa por el código de inspección del EDR antes de llegar a la función real. El EDR ve cada llamada, cada parámetro, cada valor de retorno.
Inspección de pila de llamadas. El EDR toma una instantánea de la pila de llamadas del hilo. Recorre los frames buscando direcciones de retorno que apunten a memoria no respaldada (sin módulo, sin archivo en disco). Desensambla hacia atrás desde cada dirección de retorno buscando una instrucción call legítima. Si algún frame parece sospechoso, el hilo es marcado.
Event Tracing for Windows (ETW). El EDR se suscribe a proveedores ETW que emiten eventos para creación de procesos, asignación de memoria, inicio de hilos, carga de imágenes y docenas de otras operaciones. Estos eventos son generados por el kernel y por componentes del sistema instrumentados. No se pueden bloquear desde userland sin acceso a nivel de kernel, pero la tubería ETW de userland puede ser cegada.
Crystal Palace aborda la mayoría de estos vectores, no todos, y en diferentes grados.
5.2 Análisis Estático: Roto en Tiempo de Build
Cada blob de Crystal Palace es estructuralmente único. El cóctel BTF asegura que no haya dos builds que produzcan bytes idénticos. Esto significa:
- Las reglas YARA que coincidían con la build de ayer no coincidirán con la de hoy. Las secuencias de bytes que formaban la firma han sido mutadas, reordenadas o dispersadas.
- Sin cadenas en texto plano conocidas. La sintaxis
MODULE$Functionreemplaza las cadenas de nombres de DLL y función con hashes ROR13. Los contenidos de los recursos (la DLL incrustada, la clave de máscara) están cifrados con XOR con una clave aleatoria por build. Los mensajes de error y las cadenas de depuración pueden ocultarse tras compilación condicional. - Sin tabla de importación. No hay cabecera PE, ni directorio de importación, ni cadenas de nombres de función incrustadas en el binario para que el análisis estático las descubra. Todo el mecanismo de resolución de API está basado en hash y se ejecuta en tiempo de ejecución desde dentro del propio código.
La autocomprobación YARA del archivo .spec (rule) proporciona una red de seguridad medible: si la salida activara un patrón conocido, la build falla. No descubres la detección en producción.
5.3 Análisis de Memoria: Capa por Capa
Crystal Palace no dicta cómo se asigna la memoria, qué permisos tiene o si está respaldada por un archivo. Esas decisiones se toman en tu código de loader. Pero Crystal Palace permite que el loader tome decisiones evasivas:
Pisoteo de módulo (Module stomping). En lugar de asignar memoria privada ejecutable con VirtualAlloc, el loader puede sobrescribir la sección .text de una DLL legítimamente cargada. La región de memoria conserva su clasificación Type=Image y su referencia de archivo de respaldo. Un EDR que busque regiones Private + RX no encuentra nada. Crystal Palace soporta esto a través de su control sobre la asignación de memoria en el loader y el flujo ProcessImports + fix_section_permissions.
Transiciones de permisos. El loader puede asignar memoria como RW, copiar el payload, cambiar a RX, y nunca crear una página RWX. Crystal Palace no impone esto el código del loader lo hace pero la arquitectura de loader que Crystal Palace permite hace que sea sencillo de implementar.
Enmascaramiento de suspensión. A través de addhook, el loader intercepta las llamadas del implante a Sleep, WaitForSingleObject y funciones de bloqueo similares. Durante el ciclo de suspensión, las secciones del implante se cifran en memoria y sus protecciones se reducen a RW. Cualquier escaneo de memoria durante esta ventana encuentra datos cifrados, no código ejecutable. Al despertar, las secciones se descifran y las protecciones se restauran. El implante nunca ve que esto ocurra.
5.4 Hooking de API: Intercepción a Nivel de IAT
Aquí es donde attach y addhook cambian fundamentalmente el juego. En lugar de intentar evadir los hooks del EDR (syscalls indirectas, mapeo manual, desenganche), Crystal Palace te da control sobre qué código se ejecuta cuando se llama a la API.
Intercepción a nivel de loader (attach). Cada llamada a la API que hace el loader VirtualAlloc, VirtualProtect, LoadLibraryA, CreateFileW pasa por tu envoltorio. Puedes añadir suplantación de pila alrededor de cada llamada. Puedes añadir cegado de ETW antes de operaciones sensibles. Puedes registrar, sanear o redirigir. El EDR sigue viendo que se llama a la API, pero la pila de llamadas parece legítima y el contexto de llamada está limpio.
Intercepción a nivel de implante (addhook). Esta es la capacidad más profunda. Cuando el loader llama a ProcessImports para resolver la IAT de la DLL del implante, el hook controlado _GetProcAddress intercepta cada resolución. Si el implante quiere Sleep, el hook devuelve _Sleep. Si el implante quiere VirtualAlloc, el hook devuelve _VirtualAlloc. La IAT del implante ahora apunta enteramente a tus funciones envoltorio.
Esto significa que puedes cambiar cómo el implante asigna memoria, cómo duerme, cómo lee archivos, cómo crea procesos todo sin tocar el código fuente del agente C2. El agente fue compilado para llamar a KERNEL32$Sleep. En tiempo de ejecución, sin acción alguna por parte del agente, esa llamada aterriza en tu función _Sleep que cifra la imagen, salta a través de un frame de pila limpio, duerme, descifra y retorna.
Las implicaciones son de largo alcance:
- Sin modificación del código fuente del C2. No necesitas bifurcar Demon de Havoc, modificar el beacon de Cobalt Strike ni parchear la DLL del agente Xenon. Los hooks se aplican en tiempo de resolución de IAT, de forma transparente.
- Evasión modular. Diferentes implantes reciben diferentes configuraciones de hooks. Un beacon de corta permanencia podría recibir enmascaramiento de suspensión agresivo; uno de larga permanencia podría recibir supresión de telemetría adicional. Misma arquitectura de loader, diferente configuración de
.spec. - Hooking post-compilación. La DLL del implante es un blob binario. No tienes acceso al fuente. No puedes recompilarla. Pero puedes controlar cada API que llama a través de hooking de IAT, aplicado en tiempo de carga por Crystal Palace.
5.5 Inspección de Pila de Llamadas: Draugr en Tiempo de Enlazado
La suplantación de pila es un archivo COFF separado (spoof.x64.o) con un stub de ensamblador (draugr.x64.bin) que se fusiona en el loader en tiempo de build. Cada llamada a la API que hace el loader puede enrutarse a través de spoof_call, que construye frames de pila sintéticos antes de realizar la llamada real.
Los frames sintéticos apuntan a direcciones legítimas dentro de ntdll.dll, kernel32.dll y un módulo que contiene gadgets (típicamente archiveint.dll o dfshim.dll). Cuando un EDR recorre la pila de llamadas, ve:
ntdll!NtProtectVirtualMemory+0x14
archiveint!SomeFunction+0x42 ← gadget (jmp [rbx] con call precedente)
kernel32!BaseThreadInitThunk+0x14 ← sintético
ntdll!RtlUserThreadStart+0x21 ← sintético
0x0000000000000000 ← centinela
Cada frame está en un módulo respaldado. Cada dirección de retorno tiene una instrucción call legítima precediéndola (verificada por el escáner de gadgets en init_frame_info). El llamante real el código PICO no respaldado es invisible.
La transferencia de punto de entrada (TransferExecutionViaStack) usa el mismo mecanismo pero invertido: construye una pila que parece un nacimiento de hilo legítimo (RtlUserThreadStart → BaseThreadInitThunk → entry_point) y entrega la ejecución al implante a través de NtContinue. Sin instrucción call, sin dirección de retorno apuntando al loader, sin frame no respaldado en ninguna parte de la cadena.
5.6 Lo Que Crystal Palace NO Aborda
Para ser claros: Crystal Palace no es una bala de plata. No hace lo siguiente:
- Garantizar la indetectabilidad. Proporciona primitivas. Cómo las uses determina si eres detectado. Un loader mal escrito enlazado con Crystal Palace sigue siendo un loader mal escrito.
- Evadir callbacks de kernel. PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks y otra telemetría a nivel de kernel son invisibles para userland. Crystal Palace no puede tocarlos.
- Eliminar la detección por comportamiento. Si tu implante se conecta a un dominio C2 conocido, genera tráfico de red anómalo o realiza acciones con patrones reconocibles, el EDR te atrapará independientemente de lo limpia que esté tu pila de llamadas.
- Reemplazar la disciplina operativa. El mejor loader del mundo no te salva de un mal perfil C2, un dominio conocido como malicioso o una acción de post-explotación detectable.
6. Lecciones Prácticas: Lo Que He Aprendido
A lo largo de varios meses trabajando con Crystal Palace en un contexto operativo construyendo loaders personalizados, integrándolo con el framework Mythic y el agente Xenon, probando contra configuraciones de laboratorio de EDR e iterando sobre técnicas ofensivas de evasión ciertas lecciones se han vuelto claras. Esta sección captura las que no están en la documentación.
6.1 La Superficie de Integración: Mythic y Xenon
Crystal Palace es agnóstico al C2, lo que significa que no incluye soporte integrado para ningún framework específico. La integración consiste en entender tres cosas: qué formato espera el C2, qué produce Crystal Palace y qué aspecto tiene el contrato de punto de entrada.
Para el agente Xenon en Mythic, el formato de salida de shellcode utiliza Crystal Palace para envolver la DLL del agente Xenon en un loader reflectivo personalizado. La tubería de build de Mythic genera la DLL del agente, luego invoca a Crystal Palace con un archivo .spec y la DLL como entrada. Crystal Palace produce un archivo .bin shellcode crudo, go() en el offset cero. Este .bin es lo que el operador descarga y despliega.
El punto de integración es el archivo ZIP de loader que Mythic acepta durante la generación de payloads. El loader debe seguir un formato específico: un Makefile con un objetivo make por defecto, un loader.spec que describa la build, y cualquier archivo fuente que el loader necesite. El proyecto Crystal-Kit existente (de RastaMouse) proporciona una base; el fork compatible con Xenon ajusta el manejo del punto de entrada del loader para funcionar con la convención DllMain / StartW de Xenon.
La diferencia clave entre Xenon y Cobalt Strike en este contexto es el punto de entrada de la DLL. El beacon de Cobalt Strike exporta DllMain y espera ser llamado con el código de razón 1 (DLL_PROCESS_ATTACH) seguido de la razón 4 (una convención de Cobalt Strike para entrar en el bucle de suspensión). El agente de Xenon sigue un patrón más estándar DllMain lanza los hilos de comunicación y retorna, después de lo cual el loader debe mantener vivo el proceso anfitrión. El loader maneja esto bloqueándose en el hilo principal después de llamar al punto de entrada, en lugar de hacer una segunda invocación del punto de entrada con un código de razón mágico.
El loader de post-explotación es un par .spec / Makefile separado que sigue un contrato diferente: carga una DLL de capacidad post-explotación (para execute_assembly, powerchell, etc.), pasa argumentos de línea de comandos a través de lpvReserved y finaliza forzosamente el proceso después de la ejecución para recuperar la salida (el patrón fork-and-run). El mecanismo %ARGFILE en el .spec proporciona los argumentos de la DLL en tiempo de enlazado.
6.2 La Lección de Preserve
Si olvidas preserve "KERNEL32$LoadLibraryA" "init_frame_info", el loader se cuelga instantáneamente. Si olvidas añadirlo para cada función que llama a una API enganchada durante la inicialización, el cuelgue es intermitente depende de si esa ruta de código específica se ejecuta en una ejecución dada.
La directiva preserve no está documentada tan prominentemente como debería. Es una línea corta en el archivo .spec con consecuencias catastróficas si se omite. El patrón a recordar: cualquier función llamada durante la inicialización que a su vez llame a una API enganchada con attach debe tener preserve para esa API.
6.3 El Modelo de Memoria PICO
Los sub-PICOs (run "pico.spec") se cargan en regiones de memoria separadas. Esto tiene una consecuencia práctica que es fácil pasar por alto: el PICO loader y el PICO de hooks no comparten globales. Si el PICO de hooks necesita conocer la dirección base y el tamaño de la DLL cargada (para el enmascaramiento de suspensión), esa información debe pasarse explícitamente a través de la estructura IMPORTFUNCS o mediante una exportación dedicada setup_memory. No puedes simplemente declarar una global en hooks.c y leerla desde loader.c.
El patrón es: el PICO loader llama a PicoLoad para cargar el PICO de hooks, luego llama a PicoGetExport para resolver setup_hooks y setup_memory, luego llama a ambos para pasar la tabla de funciones y el diseño de memoria. Solo después de llamar a ambos, el PICO de hooks está listo para interceptar llamadas a la API.
6.4 El Escollo de TLS
Este es específico del runtime de Go. Cuando el implante es una DLL compilada con Go, almacena su puntero de goroutine en gs:[0x28] un slot TLS. El proceso de carga reflectiva (a través de LoadDLL en libtcg) no inicializa el directorio de Thread Local Storage. El runtime de Go arranca, intenta leer gs:[0x28], encuentra cero y se cuelga silenciosamente.
La solución reside en el loader, no en el .spec: después de llamar a LoadDLL, el loader debe llamar a TlsAlloc para asignar un slot TLS, escribir el valor apropiado (el puntero de goroutine o un sustituto) y ejecutar los callbacks TLS listados en el directorio TLS de la DLL. Esto debe ocurrir después de fix_section_permissions porque los callbacks TLS residen en .text, que está en RW durante la carga y debe convertirse en RX antes de ser llamada.
6.5 El Salvavidas de la Autocomprobación YARA
La directiva rule "" 10 3 10-16 detectó un problema real. Después de añadir una nueva rutina de limpieza al loader, la regla YARA automatizada saltó una secuencia de 14 bytes en el código de limpieza era lo suficientemente estable entre builds como para generar una auto-coincidencia. Sin la autocomprobación, ese payload habría sido enviado con un patrón estático conocido. Con ella, la build falló, el código de limpieza fue retocado, los parámetros de mutación se ajustaron y la siguiente build pasó.
Esta es la red de seguridad operativa. No previene todas las detecciones, pero previene la más embarazosa: enviar un payload que active una regla YARA que podrías haber detectado en tiempo de build.
6.6 Los Requisitos de los Gadgets
Para que la suplantación de pila resista frente a EDR conscientes de la pila de llamadas, el gadget debe satisfacer dos propiedades: debe ser una instrucción JMP [RBX] (FF 23), y debe haber una instrucción CALL legítima (E8) en los cinco bytes que la preceden. La segunda propiedad es lo que muchas implementaciones iniciales pasan por alto.
La razón: un EDR que desensambla la pila de llamadas no solo comprueba que las direcciones de retorno estén en memoria respaldada. También recorre hacia atrás desde cada dirección de retorno buscando una instrucción CALL porque una pila de llamadas legítima se construye mediante instrucciones CALL que apilan direcciones de retorno. Si el EDR encuentra una dirección de retorno pero los cinco bytes precedentes no contienen ningún E8, el frame se marca como sintético.
Esto significa que necesitas un módulo fuente de gadgets donde las instrucciones JMP [RBX] estén presentes y precedidas por instrucciones CALL. No todas las DLL califican. archiveint.dll y dfshim.dll han sido verificadas en builds actuales de Windows 10/11. Otros módulos pueden funcionar o no dependiendo de la versión específica de Windows y el nivel de parche.
7. Crystal Palace en el Ecosistema de Herramientas
7.1 Comparación: Crystal Palace vs. Alternativas
| Dimensión | Crystal Palace | Stardust | Donut / sRDI | RDI Tradicional |
|---|---|---|---|---|
| Enfoque | Linker post-compilación | C++ con script de enlazado | Envoltorio de shellcode | Código dentro de DLL |
| Punto de entrada | go() en offset 0 |
stardust() en offset 0 |
Exportación con nombre | Exportación ReflectiveLoader |
| Lenguaje | DSL .spec |
C++ + linker.ld | Go (binario donut) | C |
| Cadenas y globales | Sí (vía fusión .text+.rdata) |
Sí (vía symbol<T> y .global) |
No | Sí |
| Mutación por build | Sí (+mutate, +regdance, +blockparty, +shatter, +disco) |
No | No | No |
| Cegado de constantes | Sí (magic) |
No | No | No |
| Dispersión de instrucciones | Sí (ised) |
No | No | No |
| Autocomprobación YARA | Sí (rule) |
No | No | No |
| Hooking de IAT | Sí (attach + addhook) |
No | No | No |
| Composición modular | Sí (sub-PICOs, merge, run) |
Parcial (blob único) | No | No |
| Dependencia de C2 | Ninguna | Ninguna | Requiere binario donut | Hooks específicos del framework |
| Soporte de librerías | mergelib (cualquier archivo estático) |
Manual | N/A | N/A |
| Curva de aprendizaje | Alta (DSL, relocalizaciones, alcance de hooks) | Alta (script de enlazado, trampas .bss) |
Baja | Media |
| Formato de salida | Blob de shellcode crudo | Blob de shellcode crudo | Shellcode o EXE | DLL |
| Sobrecarga de tamaño | 20-90% por mutaciones | Mínima | Moderada (stub donut + payload) | Sobrecarga PE |
7.2 Cuándo Usar Cada Uno
Crystal Palace es la elección correcta cuando necesitas control máximo sobre el proceso de carga, mutación binaria por build, hooking transparente de IAT de la DLL cargada y la capacidad de componer técnicas ofensivas a partir de componentes desarrollados independientemente. La inversión está en aprender el lenguaje .spec y entender el modelo de transformación PIC.
Stardust es la elección correcta cuando estás escribiendo el propio implante como PIC, no solo el loader. Proporciona abstracciones en C++ con control total sobre el diseño de memoria. La inversión está en entender el script de enlazado, la plantilla symbol<T> y la distinción .bss / .global.
Donut / sRDI es la elección correcta cuando necesitas una forma rápida y sin configuración de convertir una DLL o un ensamblado .NET en shellcode. No es evasivo, está fuertemente firmado y todos los EDR tienen reglas para ello. Pero para entornos donde la detección no es una preocupación (laboratorio aislado, sin EDR), resuelve el problema de conversión con un solo comando.
RDI Tradicional es la línea base. Tiene la mayor superficie de detección, los patrones de bytes más firmados y la menor flexibilidad. No deberías usarlo en ningún contexto donde la evasión importe. Existe como punto de referencia histórico.
8. Los Límites de la Herramienta
Crystal Palace es un linker. No es un compilador, no es un ofuscador, no es un bypass de EDR y no es una garantía de seguridad operativa. Proporciona bloques de construcción. La calidad del resultado depende enteramente de cómo se ensamblen esos bloques.
El lenguaje .spec es implacable. Una directiva preserve ausente causa recursión infinita. Un objetivo attach incorrecto engancha silenciosamente la función equivocada. Una directiva link que referencia una sección inexistente produce un blob que se cuelga al acceder. El bucle de retroalimentación es lento: construyes, pruebas, se cuelga, depuras, arreglas el spec. No hay un compilador que te diga que tu cadena de hooks tiene un ciclo.
Las mutaciones aumentan el tamaño del binario. En algunos escenarios de entrega esto importa. Un payload por etapas que debe caber dentro de un buffer de tamaño fijo, un canal de exfiltración basado en DNS con límites de tamaño por consulta, o un loader inyectado en un proceso con espacio de direcciones virtuales restringido todos estos pueden romperse si el binario mutado es significativamente más grande de lo esperado. La cura es ajustar los parámetros de mutación, pero eso reduce la cobertura de mutación.
Las primitivas de mutación son heurísticas, no formales. No garantizan que cada posible regla YARA se rompa. Hacen que el desarrollo de firmas estáticas sea más difícil mucho más difícil pero un analista suficientemente motivado con suficientes muestras puede encontrar patrones estables entre builds. La autocomprobación rule proporciona una verificación automatizada; no es una prueba de indetectabilidad.
Solo userland. Crystal Palace no toca el kernel. Los callbacks de kernel, ETW-TI y la telemetría a nivel de driver están fuera de su alcance. Si tu entorno objetivo ejecuta un EDR con un driver de kernel, necesitas técnicas adicionales a niveles más bajos.
9. Conclusiones
Crystal Palace cambia el modelo de desarrollo de loaders de monolítico a modular. En lugar de escribir un gran archivo C que maneje la asignación de memoria, la resolución de API, la instalación de hooks, la suplantación de pila, el enmascaramiento de suspensión y la limpieza, escribes componentes pequeños e independientes cada uno un archivo COFF separado y los compones en tiempo de enlazado a través de un archivo .spec.
Esto tiene tres consecuencias de largo alcance:
Primero, las técnicas ofensivas se vuelven componibles. El código de suplantación de pila es un archivo. La lógica de despacho de hooks es otro. La rutina de enmascaramiento de suspensión es un tercero. Cada uno puede desarrollarse, probarse y mejorarse de forma independiente. Cambiar el algoritmo de selección de gadgets por una fuente de DLL diferente es un cambio de una línea en el .spec. Añadir una nueva API enganchada es una directiva addhook y una función envoltorio. Nada más en el loader necesita cambiar.
Segundo, el comportamiento del implante se vuelve modificable de forma transparente. A través de addhook y el _GetProcAddress controlado, puedes interceptar cada llamada a la API que hace la DLL cargada sin modificar el código fuente de la DLL, sin recompilar el agente C2 y sin que el agente sea consciente de que algo ha cambiado. Enmascaramiento de suspensión, parcheo de ETW, endurecimiento de permisos de memoria, supresión de telemetría aplicados en tiempo de resolución de IAT, transparentes para el implante.
Tercero, la detección estática se vuelve inabordable. El cóctel BTF (+mutate, +regdance, +blockparty, +shatter, +disco) combinado con el cegado de constantes (magic) y la dispersión de instrucciones (ised) asegura que cada build produzca bytes estructuralmente únicos. La autocomprobación YARA (rule) proporciona una verificación de seguridad medible: si la salida activara un patrón conocido, no se envía. Esto no hace que el payload sea indetectable, pero hace que la detección basada en firmas el mecanismo de detección más automatizado, más escalable y más comúnmente desplegado no sea fiable a escala.
Nada de esto es gratuito. El lenguaje .spec requiere un entendimiento profundo. La sobrecarga de mutación exige conciencia operativa. Las reglas de alcance de hooks (attach vs. addhook, preserve, límites de memoria PICO) crean modos de fallo sutiles. Y Crystal Palace es, en esencia, solo un linker proporciona primitivas, no soluciones.
Pero las primitivas son las primitivas correctas. En un campo donde los loaders por defecto están firmados al llegar y los loaders personalizados son un rito de iniciación para cada operador, tener una herramienta que te devuelva las cadenas, te devuelva las globales, te devuelva las llamadas directas a la API, aleatorice tu salida en cada build y te permita componer técnicas ofensivas a partir de componentes independientes eso cambia la economía del desarrollo de evasión. Dejas de luchar contra la cadena de herramientas y empiezas a construir con ella.
References
[1] Raphael Mudge, "Crystal Palace Documentation and Quick Reference," Tradecraft Garden, 2025-2026. https://tradecraftgarden.org/crystalpalace.html https://tradecraftgarden.org/docs.html
[2] RastaMouse, "Crystal-Kit: Cobalt Strike Evasion with Crystal Palace," GitHub, 2024-2026. https://github.com/rasta-mouse/Crystal-Kit
[3] c0rnbread, "Xenon Agent for Mythic C2 Evasion and User-Defined Reflective Loaders," MythicAgents Wiki, 2024-2026. https://github.com/MythicAgents/Xenon/wiki/Evasion
[4] Lorenzo Meacci, "Bypassing EDR in a Crystal Clear Way KaplaStrike," lorenzomeacci.com, March 15, 2026. https://lorenzomeacci.com/bypassing-edr-in-a-crystal-clear-way
[5] Cracked5pider, "Stardust: Modern Implant Design Position Independent Malware Development," GitHub, 2024. https://github.com/Cracked5pider/Stardust
[6] Cracked5pider, "Modern Implant Design: Position Independent Malware Development," 5pider.net, January 27, 2024. https://5pider.net
[7] The Cracked Group, "Draugr: Call Stack Spoofing," TCG Tradecraft Repository, 2024.
[8] Raphael Mudge, "Crystal Palace Crash Course PIC Programming Video Series," Vimeo, 2024. https://vimeo.com/1100089433
[9] naksyn, "Raising Beacons Without UDRLs: Teaching How to Sleep," naksyn.com, July 2, 2024. https://naksyn.com/cobalt%20strike/2024/07/02/raising-beacons-without-UDRLs-teaching-how-to-sleep.html
[10] Fortra, "Cobalt Strike 4.9: Take Me to Your Loader," cobaltstrike.com, 2024. https://www.cobaltstrike.com/blog/cobalt-strike-49-take-me-to-your-loader
[11] RastaMouse, "GadgetHunter Call Stack Spoofing Gadget Scanner," GitHub, 2024. https://github.com/rasta-mouse/GadgetHunter
[12] Stephen Fewer, "Reflective DLL Injection," Harmony Security, 2009.
[13] Adversary Fan Fiction Writers Guild, "Tradecraft Garden," 2025-2026. https://tradecraftgarden.org/
[14] c0rnbread, "Crystal-Kit-Xenon: Crystal Kit Compatible with Mythic Xenon Agent," GitHub, 2024-2026. https://github.com/nickswink/crystal-kit-xenon
[14] c0rnbread, "Crystal-Kit-Xenon: Crystal Kit Compatible with Mythic Xenon Agent," GitHub, 2024-2026. https://github.com/nickswink/crystal-kit-xenon
Para apoyar la redacion de este documento se an utilizado 2 modelos de inteliguencia artificial. En este caso gemma 4 31B y deepseek R1 14B desde ollama
Esta publicación es solo para fines educativos y de investigación. Las técnicas descritas solo deben usarse en entornos autorizados con permiso explícito. No se hacen afirmaciones sobre la evasión de EDR específicos; los resultados dependen de la configuración, el entorno objetivo y el contexto operativo.